概念
从一个源(协议 + 端口 + 域名)加载的文档或脚本,要想与另一个源的资源进行交互,必须保证 协议、域名和端口号 相同
它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、script 脚本请求都不会有跨域的限制
限制的三个方面
同源策略又分为以下三种:
- DOM 同源策略: 禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的 (比如一个恶意网站的页面通过 iframe 嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的 javascript 脚本就可以在用户登录银行的时候获取用户名和密码)
- XMLHttpRequest 同源策略: 禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求 (这一点里面其实包括了 ajax)。
- Cookie、LocalStorage、IndexedDB 等存储性内容同源策略: js 中无法访问不属于同个源的 cookie、LocalStorage 中存储的内容。
具体来说,cookie 和 LocalStorage 在控制哪些源可以访问的问题上还是细微的差别,父域在设置 cookie 的时候可以设定允许子域访问这段 cookie,同时 Cookie 只和域名以及路径关联,如果是同个域名不同端口的源依然是共享同个域名下的 Cookie 的,而 LocalStorage 则是以源为单位进行管理,相互独立,不同源之间无法相互访问 LocalStorage 中的内容
不加限制
但是有三个标签是允许跨域加载资源:
<img src=XXX><link href=XXX><script src=XXX>
CORS 机制(纯后端)
http1.1,它的总体思路是:如果浏览器要跨域访问服务器的资源,需要获得服务器的允许
首先判断请求分类
- 请求方法属于下面的一种:
- get
- post
- head
- 请求头仅包含安全的字段,常见的安全字段如下:
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- 请求头如果包含 Content-Type,仅限下面的值之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
如果以上 三个条件同时满足,浏览器判定为简单请求
简单请求
浏览器会自动在请求中添加 Origin 字段
服务器接收后判断这个 Origin 值是否在请求范围内,如果在,则返回给客户端的响应内容中添加
Access-Control-Allow-Origin字段,浏览器发现这个字段后就将响应内容正常显示出来如果服务器判断这个 Origin 值不在请求范围内,返回给客户端的响应内容就没有这个字段,浏览器发现没有这个字段就会拦截响应内容并抛出错误
// 服务端需设置
'Access-Control-Allow-Origin'非简单请求
- 浏览器发送预检请求,询问服务器是否允许
- 服务器允许
- 浏览器发送真实请求
- 服务器完成真实的响应
浏览器会先发送一个 OPTION 请求作为预检请求(添加三个请求头字段:)
'Origin' 当前请求域名
'Access-Control-Allow-Methods' 用来列出浏览器的CORS请求会用到哪些HTTP方法
'Access-Control-Allow-Headers' 指定浏览器CORS请求会额外发送的头信息字段,非必须请求当前网页所在域名是否在服务器的许可名单之中,以及可以使用哪些 http 动词 和 头信息字段,服务器根据请求头信息的三个字段判断是否通过,若通过则在响应内容中添加 Access-Control-Allow-Origin 字段,如果没有则不添加
浏览器只要通过了预检请求,在之后的 CORS 请求中会自带一个 Origin 头信息字段,服务器的响应也都会带一个 Access-Control-Allow-Origin 字段
// 服务端需设置
'Access-Control-Allow-Origin'
'Access-Control-Allow-Methods'
'Access-Control-Allow-Headers'附带身份凭证的请求
默认情况下,ajax 的跨域请求并不会附带 cookie,这样一来,某些需要权限的操作就无法进行
不过可以通过简单的配置就可以实现附带 cookie
// xhr
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// fetch api
fetch(url, {
credentials: 'include',
});这样一来,该跨域的 ajax 请求就是一个附带身份凭证的请求
当一个请求需要附带 cookie 时,无论它是简单请求,还是预检请求,都会在请求头中添加 cookie 字段
而服务器响应时,需要明确告知客户端:服务器允许这样的凭据
告知的方式也非常的简单,只需要在响应头中添加:Access-Control-Allow-Credentials: true 即可
对于一个附带身份凭证的请求,若服务器没有明确告知,浏览器仍然视为跨域被拒绝。
另外要特别注意的是:对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为 *。这就是为什么不推荐使用 * 的原因
JOSNP(前后端结合)
原理是利用 script 标签没有跨域限制,通过 script 标签的 src 属性发送一个带有 callback 参数的 get请求 ,后端接受请求后将返回数据拼凑到 callback 指定的函数名中进行返回,浏览器解析执行这个函数(因为 script 标签的原因把返回数据当作 js代码 进行执行),函数的传参也就是接口请求的数据
具体流程(原理
前端预先定义一个带参数的回调函数用来接收接口请求数据
后端将接口请求数据封装到回调函数中以字符串的形式返回给前端
// 前端部分
<script>
// 1 callback
// 2 后端 callbackName(数据)
function onResponse(posts) {
console.log(posts);
}
// 前端没有调用
</script>
<!-- 后端返回结果 -->
<!-- 调用 -->
<script src="http://localhost:9090/api"></script>//后端部分
const http = require('http');
http.createServer((req, res) => {
if (req.url === '/api') {
let posts = ['js', 'php'];
res.end(`onResponse(${JSON.stringify(posts)})`);
}
})
.listen(9090, () => {
console.log(9090)
})封装后
上述方式需要前后端约定好回调函数名,我们可以对这点进行优化,前端将回调函数名以请求参数的形式发送给后端
// 前端
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>// 后端返回数据
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
var params = querystring.parse(req.url.split('?')[1]);
var fn = params.callback;
// jsonp返回设置
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');优缺点
优点:
- 它不像 XMLHttpRequest 对象实现的 Ajax 请求那样受到同源策略的限制
- 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要 XMLHttpRequest 或 ActiveX 的支持
缺点:
- 具有局限性, 仅支持 get 方法
- 不安全,可能会遭受 XSS 攻击
postMessage()
HTML 5 新增的 API ,是 window 对象的一个属性
用于解决以下问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间的消息传递
- 页面与嵌套的 iframe 消息传递
用法:postMessage(data,origin) 方法接受两个参数:
- data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用 JSON.stringify() 序列化。
- origin: 协议 + 主机 + 端口号,也可以设置为 "*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为 "/"。
用例
// a.html:(domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
<!-- 等到iframe中的子页面加载完成后才发送消息 -->
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script><script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false);
</script>Nginx 代理跨域(纯后端)
同源策略主要是浏览器的限制策略,而对于服务器是没有这一限制的,服务器可以调用 http 请求访问其他服务器的资源
实现思路:通过 Nginx 配置一个代理服务器,域名和客户端一致,反向代理访问 真正的服务器的接口,代理服务器拿到真正的请求结果后进行 CORS配置(添加请求头字段), 从而实现 CORS 机制
server {
listen 22222;
server_name localhost;
location / {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
add_header Access-Control-Allow-Headers '*';
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Credentials 'true';
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://localhost:59200;
}
}node 中间件
非 vue 框架的跨域
使用 node + express + http-proxy-middleware 搭建一个 proxy 服务器,类似 nginx,使用一个代理服务器实现数据的转发
Vue 框架的跨域
node + vue + webpack + webpack-dev-server 搭建的项目,跨域请求接口,直接修改 webpack.config.js 配置。开发环境下,vue 渲染服务和接口代理服务都是 webpack-dev-server 同一个,所以页面与代理接口之间不再跨域
// webpack.config.js
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}webSocket 协议跨域
WebSocket protocol 是 HTML5 一种新的协议。
它实现了浏览器与服务器全双工通信,同时允许跨域通讯
// 客户端
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://localhost:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
</body>
</html>var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});Iframe 跨域
参考
window.postMessage - Web API 接口参考 | MDN
node服务端解决socket.io跨域问题express,koa