前言
跨域是什么,为什么会有跨域?跨域的解决方法是什么?常用的是什么?原理是什么?
什么是跨域
跨域是指从一个域名去请求另一个域名的资源。
严格来说,只要域名,协议,端口任何一个不同,就视为跨域。
跨域场景
以下这种看上去再相似也没有用,都是跨域。
主域不同
http://www.chrome.cn/index.html
http://www.chomper.cn/server.php
子域名不同
http://abc.chomper.cn/index.html
http://def.chomper.cn/server.php
端口不同
http://www.chomper.cn:8080/index.html
http://www.chomper.cn/server.php
协议不同
https://www.chomper.cn/index.html
http://www.chomper.cn/server.php
localhost 调用 127.0.0.1 也属于跨域
非跨域场景
以下协议、域名、端口一致,所以是非跨域。
http://www.chomper.cn/index.html
http://www.chomper.cn/server.php
跨域提示
当跨域时会收到以下错误
为什么会出现跨域
为了网络安全起见,浏览器设置了同源策略,规定只有域名,端口,协议全部相同,就叫做同源。
如何解决跨域
跨域资源共享
跨域资源共享 CORS
是一种机制,准确的说是一个 W3C
标准,可以克服同源策略的限制,另外 CORS
是目前主流的跨域解决方案。
整个资源共享过程,不需要开发者参与,都是浏览器自动完成,对于开发者来说,同源或是非同源的异步通信是没有差别的,代码完全一样。
过程中,浏览器一旦发现异步请求跨源,就会自动添加一些附加的头信息,因此,实现资源共享的关键是服务器。只要服务器实现了 CORS
接口,就可以跨源通信。
浏览器在发送跨域请求的时候,会先判断下是 简单请求
还是 非简单请求
,因为浏览器对这两种请求方式的处理方式是不同的。
简单请求
如果是 简单请求
,就先执行服务端程序,然后浏览器才会判断是否跨域。
浏览器会在头信息中,增加一个 Origin
字段(协议 + 域名 + 端口),服务器根据 Origin
,决定是否同意这次请求。
如果不在许可范围内,服务器会返回一个正常的 HTTP
回应,浏览器发现,这个回应的头信息中没有包含 Access-Control-Allow-Origin
字段,就会抛出一个错误。
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
只要满足下列几种情况,就是 简单请求
,反之为 非简单请求
。
情况一,使用以下请求方式为简单请求,其他请求均为非简单请求。
- GET
- POST
- HEAD
情况二,设置头部信息不能超过以下几种字段。
- Accept
- Accept-Language
- Content-Language
- Content-Type (需要注意额外的限制)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
情况三,Content-Type 的值仅限于下列三者之一 (例如 application/json 为非简单请求)。
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
非简单请求
除以上情况外,就是 非简单请求
。
如果是非简单请求,浏览器会先发送 OPTIONS
请求,进行预检,这一次的请求称为“预检请求”,服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
浏览器会在发送 OPTIONS
请求时会自动添加 Origin
字段,还会包括两个特殊字段。
Access-Control-Request-Method (会用到哪些 HTTP 方法)
Access-Control-Request-Headers (一个逗号分隔的字符串,指定额外发送的头信息字段)
服务器响应 OPTIONS
请求,会在响应头里添加 Allow-Origin
Allow-Methods
Allow-Headers
,如果服务器的 OPTIONS
响应不合你的要求,你可以手动在服务器配置 OPTIONS
响应,以应对带预检的跨域请求。
注意: 服务端可以通过设置 Access-Control-Max-Age 告诉浏览器在一定时间内无需再次发送预检请求,但是如果浏览器禁用缓存则无效。
浏览器收到 OPTIONS
响应,会比较真实请求的 method
是否属于返回的 Allow-Methods
的值之一,还有 origin
, head
也会进行比较是否匹配。
注意:如果通过,浏览器就继续向服务器发送真实请求,否则就会报预检错误。
服务端处理
由于出现跨域的问题,所以服务端需要设置同意任意跨源请求:
Access-Control-Allow-Origin: *
Allow-Methods
和 Allow-Headers
只在响应 options
请求时有作用,Allow-Origin
在响应 options
请求和响应真实请求时都是有作用的,两者必须同时包含要跨域的源。
虽然可以通过设置响应头和响应方式等支持非简单请求,但是不到万不得已的情况,不能允许客户端发送非简单请求。因为非简单请求会使服务器比简单请求的多一倍的压力。
最后,当你使用 IE<=9, Opera<12, or Firefox<3.5 或者更加老的浏览器,这个时候请使用 JSONP 。
关于cookie
CORS
请求默认不发送 Cookie
,想要传递 Cookie
需要满足 3 个条件:
-
Access-Control-Allow-Credentials
为true
,代表服务器同意发送Cookie
。
// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 设置方式
axios.defaults.withCredentials = true;
- 请求必须设置
withCredentials
,否则,即便服务器要求设置Cookie
,浏览器也不会处理。 -
Access-Control-Allow-Origin
为非*
,须指定明确的、与请求网页一致的域名。
浏览器看不到 options
在新版的 chrome
中,如果你发送了复杂请求,你却看不到 options
请求。可以在这里设置 chrome://flags/#out-of-blink-cors
设置成 disbale
,重启浏览器。对于非简单请求就能看到 options
请求了。
JSONP
由于 script
标签从不同域名下加载静态资源文件是被浏览器允许的,所以 JSONP
就是利用了这个“犯罪漏洞”来进行跨域。
虽然这种方式非常好用,但是最大的缺陷就是,仅支持 GET
方法,如果想使用完整的 REST
接口,请使用 CORS
或者其他代理方式。
动态创建 script 标签
let script = document.createElement('script');
script.src = 'http://www.chomper.cn/login?username=chomper&callback=callback';
document.body.appendChild(script);
function callback(res) {
console.log(res);
}
其他跨域解决方案:
通过jsonp跨域
document.domain + iframe跨域
location.hash + iframe
window.name + iframe跨域
postMessage跨域
nginx代理跨域
nodejs中间件代理跨域
WebSocket协议跨域
- 通过jsonp跨域
通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一*立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。
原生实现:
<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>
服务端返回如下(返回时即执行全局函数):
handleCallback({"status": true, "user": "admin"})
2、 document.domain + iframe跨域
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
父窗口:
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>
子窗口:
<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
3、location.hash + iframe
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
4、 window.name + iframe跨域
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
5、 postMessage跨域
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的iframe消息传递
d.) 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
6、 nginx代理跨域
7、 nodejs中间件代理跨域
8、 WebSocket协议跨域
WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
原理:
就是让服务器和客户端能够发送 Ping/Pong Frame(RFC 6455 - The WebSocket Protocol)。这种 Frame 是一种特殊的数据包,它只包含一些元数据而不需要真正的 Data Payload,可以在不影响 Application 的情况下维持住中间网络的连接状态。