Skip to content
字数
2833 字
阅读时间
12 分钟

概念

从一个源(协议 + 端口 + 域名)加载的文档或脚本,要想与另一个源的资源进行交互,必须保证 协议、域名和端口号 相同

它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、script 脚本请求都不会有跨域的限制

限制的三个方面

同源策略又分为以下三种:

  1. DOM 同源策略: 禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的 (比如一个恶意网站的页面通过 iframe 嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的 javascript 脚本就可以在用户登录银行的时候获取用户名和密码)
  2. XMLHttpRequest 同源策略: 禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求 (这一点里面其实包括了 ajax)。
  3. 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,它的总体思路是:如果浏览器要跨域访问服务器的资源,需要获得服务器的允许

首先判断请求分类

  1. 请求方法属于下面的一种:
    • get
    • post
    • head
  2. 请求头仅包含安全的字段,常见的安全字段如下:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  3. 请求头如果包含 Content-Type,仅限下面的值之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

如果以上 三个条件同时满足,浏览器判定为简单请求

简单请求

浏览器会自动在请求中添加 Origin 字段

  • 服务器接收后判断这个 Origin 值是否在请求范围内,如果在,则返回给客户端的响应内容中添加 Access-Control-Allow-Origin 字段,浏览器发现这个字段后就将响应内容正常显示出来

  • 如果服务器判断这个 Origin 值不在请求范围内,返回给客户端的响应内容就没有这个字段,浏览器发现没有这个字段就会拦截响应内容并抛出错误

js
// 服务端需设置
'Access-Control-Allow-Origin'

非简单请求

  1. 浏览器发送预检请求,询问服务器是否允许
  2. 服务器允许
  3. 浏览器发送真实请求
  4. 服务器完成真实的响应

浏览器会先发送一个 OPTION 请求作为预检请求(添加三个请求头字段:)

js
'Origin'  当前请求域名
'Access-Control-Allow-Methods' 用来列出浏览器的CORS请求会用到哪些HTTP方法
'Access-Control-Allow-Headers' 指定浏览器CORS请求会额外发送的头信息字段非必须

请求当前网页所在域名是否在服务器的许可名单之中,以及可以使用哪些 http 动词 和 头信息字段,服务器根据请求头信息的三个字段判断是否通过,若通过则在响应内容中添加 Access-Control-Allow-Origin 字段,如果没有则不添加

浏览器只要通过了预检请求,在之后的 CORS 请求中会自带一个 Origin 头信息字段,服务器的响应也都会带一个 Access-Control-Allow-Origin 字段

js
// 服务端需设置
'Access-Control-Allow-Origin'  
'Access-Control-Allow-Methods'
'Access-Control-Allow-Headers'

附带身份凭证的请求

默认情况下,ajax 的跨域请求并不会附带 cookie,这样一来,某些需要权限的操作就无法进行

不过可以通过简单的配置就可以实现附带 cookie

js
// 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代码 进行执行),函数的传参也就是接口请求的数据

具体流程(原理

前端预先定义一个带参数的回调函数用来接收接口请求数据

后端将接口请求数据封装到回调函数中以字符串的形式返回给前端

js
// 前端部分
<script>
    // 1 callback
    // 2 后端 callbackName(数据)
    function onResponse(posts) {
        console.log(posts);
    }
    // 前端没有调用
</script>
<!-- 后端返回结果 -->
<!-- 调用 -->
<script src="http://localhost:9090/api"></script>
js
//后端部分
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)
})

封装后

上述方式需要前后端约定好回调函数名,我们可以对这点进行优化,前端将回调函数名以请求参数的形式发送给后端

html
// 前端
<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>
js
// 后端返回数据
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: 协议 + 主机 + 端口号,也可以设置为 "*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为 "/"。

用例

html
// 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>
html
<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 机制

js
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 同一个,所以页面与代理接口之间不再跨域

js
// 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 一种新的协议。

它实现了浏览器与服务器全双工通信,同时允许跨域通讯

html
// 客户端
<!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>
js
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 跨域

参考

Site Unreachable

window.postMessage - Web API 接口参考 | MDN

一篇文章让你搞懂如何通过Nginx来解决跨域问题

node服务端解决socket.io跨域问题express,koa

前端面试必会网络之跨域问题解决

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写