什么是跨域
由于浏览器的同源策略,所谓同源策略,指的是浏览器对不同源的脚本或者文本的访问方式进行的限制。比如源 a 的 js 不能读取或设置引入的源 b 的元素属性。
产生跨域原因:
- 浏览器限制
- 请求是跨域的
- 请求是XHR(XMLHttpRequest)请求
什么是同源?
所谓同源,就是指两个页面具有相同的协议,主机(也常说域名),端口,三个要素缺一不可。例如:
URL1 | URL2 | 说明 | 是否允许通信 |
---|---|---|---|
http://www.foo.com/js/a.js | http://www.foo.com/js/b.js | 协议、域名、端口都相同 | 允许 |
http://www.foo.com/js/a.js | http://www.foo.com:8888/js/b.js | 协议、域名相同,端口不同 | 不允许 |
https://www.foo.com/js/a.js | http://www.foo.com/js/b.js | 主机、域名相同,协议不同 | 不允许 |
http://www.foo.com/js/a.js | http://www.bar.com/js/b.js | 协议、端口相同,域名不同 | 不允许 |
http://www.foo.com/js/a.js | http://foo.com/js/b.js | 协议、端口相同,主域名相同,子域名不同 | 不允许 |
同源策略限制了不同源之间的交互,但是有人也许会有疑问,我们以前在写代码的时候也常常会引用其他域名的 js 文件,样式文件,图片文件什么的,没看到限制啊,这个定义是不是错了。其实不然,同源策略限制的不同源之间的交互主要针对的是 js 中的 XMLHttpRequest 等请求,下面这些情况是完全不受同源策略限制的:
- 页面中的链接,重定向以及表单提交是不会受到同源策略限制的
- 跨域资源嵌入是允许的,例如
<img src=XXX>
,当然,浏览器限制了 Javascript 不能读写加载的内容。
同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
跨域解决方案
1.CORS
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器「不同的域、协议或端口」请求一个资源时,资源会发起一个「跨域 HTTP 请求」。
而在 cors 中会有简单请求和复杂请求的概念。
简单请求是满足以下所有条件的请求:
- 只允许 GET、POST 和 HEAD 方法
- 不能自定义请求头,除了代理自动设置的请求头外(Connection,User-Agent等),只允许 CORS 安全列出的请求头,它们是:
- Accept
- Accept-Language
- Content-Language
- Content-Type
- Last-Event-ID
- 其它名称是不区分字节大小写的匹配(DPR、Downdlink、Save-Data、Viewport-Width、Width等,没试过~)
- Content-Type:只限于三个值
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 于XMLHttpRequestUpload:
- 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器
- XMLHttpRequestUpload 对象可以使用XMLHttpRequest.upload 属性访问
- 请求中没有使用 ReadableStream 对象
除了简单请求情况之外的就是复杂请求。
CORS 预检请求首先通过 OPTIONS 方法向另一个域上的资源发送 HTTP 请求,用来确定实际请求是否跨域安全的发送。预检请求通过后才会发送真实请求。
Node 中的解决方案
原生:
1 | app.use(async (ctx, next) => { |
中间件:
1 | // koa 用 koa-cors |
关于 cors 的 cookie 问题
想要传递 cookie 需要满足 3 个条件:
- web 请求设置 withCredentials
这里默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie.
1 | // 原生 xml 的设置方式 |
Access-Control-Allow-Credentials 为 true
Access-Control-Allow-Origin 为非 *
这里请求的方式,在 chrome 中是能看到返回值的,但是只要不满足以上其一,浏览器会报错,获取不到返回值。
2.Node 正向代理
代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。
Webpack (4.x)
1 | devServer: { |
利用 node 作为中间件代理(两次跨域)
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
代理服务器,需要做以下几个步骤:
- 接受客户端请求
- 将请求转发给服务器。
- 拿到服务器响应数据。
- 将响应转发给客户端。
1 | var app = express() |
这里用 node 重写了2个接口,利用 node 去请求真实的服务器 https://c.y.qq.com
,带上需要的 headers 请求头。
再看一个完整的简单案例:
本地文件 index.html 文件,通过代理服务器 http://localhost:3000 向目标服务器 http://localhost:4000 请求数据
1 | / index.html(http://127.0.0.1:5500) |
1 | // server1.js 代理服务器(http://localhost:3000) |
1 | // server2.js(http://localhost:4000) |
上述代码经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略,最后在 index.html 文件打印出 {"title":"fontend","password":"123456"}
3.Nginx 反向代理
实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。
将 nginx 目录下的 nginx.conf 修改如下:
1 | // proxy服务器 |
nginx 使用配置大全请看nginx 最全操作总结
4.JSONP
利用 <script>
标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。 JSONP 请求一定需要对方的服务器做支持才可以。
JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持 get 方法具有局限性,不安全可能会遭受 XSS 攻击。
jsonp 实现:
1 | function jsonp ({url, params, callback}) { |
使用
1 | function show(data) { |
上面这段代码相当于向 http://localhost:3000/say?wd=I love you&callback=show
这个地址请求数据,然后后台返回 show('I love you too')
,最后会运行 show()
这个函数,打印出’I love you too’
1 | // server.js |
5.Websocket
Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket 和 HTTP 都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了,因此也没有跨域的限制。
1 | // socket.html |
1 | // server.js |
6.window.postMessage
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
- postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
1 | otherWindow.postMessage(message, targetOrigin, [transfer]); |
- message: 将要发送到其他 window的数据。
- targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”*”(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
- transfer(可选):是一串和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
- 接下来我们看个例子: http://localhost:3000/a.html 页面向 http://localhost:4000/b.html 传递“我爱你”,然后后者传回”我不爱你”。
1 | // a.html |
1 | // b.html |
7.window.name + iframe
window.name 属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
其中 a.html 和 b.html 是同域的,都是 http://localhost:3000;而 c.html 是 http://localhost:4000
1 | // a.html(http://localhost:3000/b.html) |
1 | // c.html(http://localhost:4000/c.html) |
总结:通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
8.location.hash + iframe
实现原理: a.html 欲与 c.html 跨域相互通信,通过中间页 b.html 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。
具体实现步骤:一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。
同样的,a.html 和 b.html 是同域的,都是 http://localhost:3000;而 c.html 是 http://localhost:4000
1 | // a.html |
1 | // b.html |
1 | // c.html |
9.document.domain + iframe
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给页面添加 document.domain =’test.com’ 表示二级域名都相同就可以实现跨域。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
我们看个例子:页面 a.zf1.cn:3000/a.html 获取页面 b.zf1.cn:3000/b.html 中 a 的值
1 | // a.html |
1 | // b.html |
总结
CORS 支持所有类型的 HTTP 请求,是跨域 HTTP 请求的根本解决方案
JSONP 只支持 GET 请求,JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
不管是Node中间件代理还是 nginx 反向代理,主要是通过同源策略对服务器不加限制。
日常工作中,用得比较多的跨域方案是 cors 和 nginx 反向代理
参考链接: