一文系列企图通过一篇简短的文章来梳理一个知识点,在杂碎的时间片段中给自己带来一点点提升。
为什么有跨域这个问题
简单的说,是因为浏览器的同源策略。
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。(引用于 MDN 定义)
如果两个链接的协议、域名、端口都一致,那么这两个 URL 同源,否则不同源。
假设 A 站点的链接为 https://news.a.com/www/index.html,B 站点为下列链接时其同源检测如下表
URL
结果
原因
https://news.a.com/www/.html
同源
https://news.a.com/mmm/index.html
同源
https://music.a.com/www/index.html
不同源
域名不一致
http://news.a.com/www/index.html
不同源
协议不一致
https://news.a.com:6666/www/index.html
不同源
端口不一致
同源策略分为以下两种:
DOM 同源策略,禁止对不同源的 DOM 元素进行操作。
XHR 同源策略,禁止使用 XHR 对象向不同源的服务器发起请求。
举个栗子,Jarry 登陆了 A 站点准备购物,与此同时 Jarry 正在 B 站点网上冲浪。如果没有同源策略,那么 B 站点的脚本可以轻轻松松的修改 A 站点的 DOM 结构或者向 A 站点的服务器发起不恰当的请求,导致存在安全隐患。
IE 浏览器有两个意外:
授信范围:两个相互之间高度互信的域名,不受同源策略的限制。
端口:IE 没有将端口号加入到同源策略的组成部分之中。
什么是跨域
当 A 站点与 B 站点不同源(只要协议、域名、端口三者其一不一致)时,A 站点无法获取到 B 站点的服务或者数据,此时就产生了跨域。
上图中,站点 https://www.jarrychung.com 企图向不同源站点 https://www.baidu.com 发起 GET 请求,导致报错。
如何解决跨域
1 JSONP
需要服务端支持。
JSON 是一种常用的数据交换格式,而 JSONP 是 JSON 的一种使用模式,可以通过这种模式来进行跨域获取数据。重要的是,JSONP 使用简便,没有兼容性问题。
同源策略下,不同源的站点无法相互获取到数据,但 img/iframe/script 标签是个例外,这些标签可以通过 src 属性获取到不同源的服务器。当正常的请求一个 JSON 数据时,服务端会返回 JSON 格式的数据。当使用 JSONP 模式发送请求时,服务端返回的是一段可执行的 JavaScript 代码。
// 举个例子
// 正常请求服务器(https://news.a.com/news?id=666)时,数据如下:
{“id”: 666,”text”:”Jarry Chung”}
// JSONP 模式请求(https://news.a.com/news?id=666?callback=fn)时,数据如下:
fn({“id”: 666,”text”:”Jarry Chung”})
// 然后使用回调函数便可以处理获得的数据
注意:JSONP 只支持 GET 请求,服务端可能在 JSONP 响应中夹带恶意代码,判断是否请求成功是困难的。
2 跨域资源共享(CORS)
需要服务端支持。
CORS 经常被称为现代化版本的 JSONP,能够发起所有种类的 HTTP 请求,以及拥有良好的错误处理。
跨源资源共享标准的工作原理是添加自定义的 HTTP 头部,允许服务器描述允许使用 Web 浏览器读取该信息的起源集。此外,对于可能对服务器数据产生副作用的 HTTP 请求方法,规范要求浏览器“预检”请求,从而请求支持的方法。服务器使用 HTTP OPTIONS 请求方法,然后,在服务器“批准”后,使用实际的 HTTP 请求方法发送实际请求。服务器还可以通知客户端是否让“凭据”(包括 Cookie 和 HTTP 认证数据)与请求一起发送(翻译自 MDN)。
CORS 的基本思想是使用自定义的 HTTP 头部允许服务端和浏览器互相认识,从而让服务端决定是否允许请求以及响应。
Access-Control-Allow-Origin: 指定授权访问的域
Access-Control-Allow-Methods:授权请求的方法(GET, POST, PUT, DELETE,OPTIONS 等)
// 举个例子
var exp = require(‘express’);
var app = exp();
app.all(‘*’, function(req, res, next) {
// 设置允许的源
res.header(“Access-Control-Allow-Origin”, “*”);
// 设置允许的 HTTP 请求方式
res.header(“Access-Control-Allow-Methods”,”PUT,POST,GET,DELETE,OPTIONS”);
res.header(“Access-Control-Allow-Headers”, “X-Requested-With”);
res.header(“Content-Type”, “application/json;charset=utf-8″);
next();
});
app.get(‘/user/:id/:pw’, function(req, res) {
res.send({id:req.params.id, password: req.params.pw});
});
app.listen(8000);
注意:是最为推荐的方案,但古董级浏览器不支持 CORS,如 IE8 以下的浏览器。
3 Nginx 反向代理
主要在服务端上实现。
浏览器有同源策略,但是服务端没有这个限制,因此可以将请求发送给反向代理服务器,由服务器去请求数据,然后再将数据返回给前端。而前端几乎不需要做任何处理。
server {
# 监听 80 端口,可以改成其他端口
listen 80;
# 当前服务的域名
server_name www.a.com;
location / {
proxy_pass http://www.a.com:81;
proxy_redirect default;
}
# 添加访问目录为 /api 的代理配置
# 目录为 /api 开头的请求将被转发到 82 端口
# 还记得吗,端口不同也是不同源
location /apis {
rewrite ^/apis/(.*)$ /$1 break;
proxy_pass http://www.a.com:82;
}
4 window.name + iframe
在浏览器实现。
window.name 的值是当前窗口的名字,要注意的是每个 iframe 都有包裹它的 window,而这个 window 是 top window 的子窗口,因此它自然也有 window.name 的属性。window.name 属性,如果没有被修改,那么其值在不同的页面(甚至不同域名)加载后依旧存在。另外,其值大小通常可达到 2MB。
其思想为:在一个页面中内嵌一个 iframe 标签,由这个 iframe 进行获取数据,将获取到的数据赋值给 window.name 属性,然后由页面获取该属性的值。既巧妙的绕过了同源策略,同时该操作也是安全的。
但这里有一个问题,即页面和该页面下的 iframe src 不同源的话,这个页面是无法操作 iframe 的,因此导致取不到 name 值。
name 属性的特性在这时候就很好用了,当前页设置的值, 在页面重新加载 (非同域也可以) 后, 只要没有被修改,值依然不变。可以让 iframe 的 location 指向为与页面相同的域,等 iframe 加载完后页面就可以取到 name 值了。
<body>
<script type=”text/javascript”>
// 代码参考自:https://www.cnblogs.com/zichi/p/4620656.html
function crossDomain(url, fn) {
iframe = document.createElement(‘iframe’);
iframe.style.display = ‘none’;
var state = 0;
iframe.onload = function() {
if(state === 1) {
// 处理数据
fn(iframe.contentWindow.name);
// 清楚痕迹
iframe.contentWindow.document.write(”);
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if(state === 0) {
state = 1;
// proxy.html 为与页面同级的空白页面
iframe.contentWindow.location = ‘http://localhost:81/proxy.html’;
}
};
iframe.src = url;
document.body.appendChild(iframe);
}
// 调用
// 服务器地址
var url = ‘http://localhost:82/data.php’;
// 处理数据 data 就是 window.name 的值(string)
crossDomain(url, function(data) {
var data = JSON.parse(iframe.contentWindow.name);
console.log(data);
});
</script>
</body>
5 location.hash + iframe
主要在浏览器实现,需要服务端支持。
在 https://www.a.com/news#JarryChungIsSoCool 这个 URL 中,location.hash 的值为 JarryChungIsSoCool。因为改变 hash 值不会导致刷新页面,因此可以利用 location.hash 属性来传递数据。缺点是数据容量以及类型受到限制、数据内容直接暴露出来。
其思想为:若 index 页面要获取不同源服务器的数据,那么动态插入一个 iframe,将 iframe 的 src 属性指向该服务器地址。由于同源策略,此时 top window 和包裹这个 iframe 的子窗口仍是无法通信的,因此改变子窗口的路径,将数据当作 hash 值添加到改变后的路径,然后就能够进行通信(这一点与利用 window.name 跨域的原理几乎一致),能够通信后可以在 iframe 中将页面的地址改变,将数据加在 index 页面地址的 hash 值上。index 页面监听地址的 hash 值变化便能够取得数据。
6 document.domain + iframe
在浏览器实现。
该方案适用于主域名一致,子域名不一致的情况。两个页面使用 JavaScript 将 document.domain 设置为相同主域名,从而实现跨域。
<!– 主页面 a.html –>
<iframe src=”http://child.domain.com/b.html”></iframe>
<script>
document.domain = ‘domain.com’;
var user = ‘Jarry Chung’;
</script>
<!– 子页面 b.html –>
<script>
document.domain = ‘domain.com’;
// 获取父窗口中 user 变量
alert(window.parent.user); // ‘Jarry Chung’
</script>
7 postMessage()
在浏览器实现。
postMessage()是 HTML5 新增的方法,可以实现跨文本档通信、多窗口通信、跨域通信。示意图如下:index.html 将需要的数据请求发送给 iframe 或者另一个页面,iframe 或另一个页面监听到 message 后响应,取得数据后利用 postMessage()接口将数据返回给 index.html。
postMessage()有两个参数:
data:需要传递的数据,HTML5 规范中该参数的类型可以是 JS 的任意基本类型或者可复制的对象,但有部分浏览器只支持传递字符串,因此可能需要将该值处理成字符串后再传递。
origin:字符串参数,指明目标窗口的源,协议 + 主机 + 端口号 [+URL],URL 会被忽略,可以不写。postMessage() 方法只会将 message 传递给指定窗口,当然如果愿意也可以把参数设置为 *,这样可以传递给任意窗口。
8 WebSocket 协议
需要服务端支持。
WebSocket 协议是 HTML5 一种新的协议,实现了浏览器与服务器全双工通信,同时允许跨域通讯。用法如下:
var ws = new WebSocket(‘wss://echo.websocket.org’);
// 连接打开后发送消息
ws.onopen = function (evt) {
console.log(‘Connection open …’);
ws.send(‘Hello WebSockets!’);
};
// 接受消息后关闭连接
ws.onmessage = function (evt) {
console.log(‘Received Message: ‘, evt.data);
ws.close();
};
// 监听关闭连接
ws.onclose = function (evt) {
console.log(‘Connection closed.’);
};
写在最后
个人经验表示在实战中碰见跨域的情况并不多,但是如果碰见了往往都会掉坑里面。
作为一线开发者,做好知识储备是在需要的时候迅速解决问题的必要条件。