什么是跨域,以及有哪几种跨域方式
参考
什么是跨域
浏览器同源策略,是浏览器最核心和最基本的安全功能,如果缺少同源策略,浏览器很容易受到XSS
、CSRF
等攻击。
同源:协议相同,域名相同,端口相同
即便是两个不同的域名指向同一 IP 地址,也是非同源的。
同源策略限制内容有:
Cookie
,LocalStorage
,indexDB
等存储性内容DOM
节点AJAX
请求
允许跨域加载资源的三个标签:
<img src=XXX/>
<link href=XXX/>
<script src=XXX/>
常见的跨域场景
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。 不同域之间相互请求资源,就算作“跨域”。
URL | 说明 | 是否允许通信 |
---|---|---|
http://www.a.com/a.js http://www.a.com/b.js |
同一域名下 | 允许 |
http://www.a.com/lab/a.js http://www.a.com/script/b.js |
同一域名,不同文件夹 | 允许 |
http://www.a.com:8000/a.js http://www.a.com/b.js |
同一域名,不同端口 | 不允许 |
http://www.a.com/a.js https://www.a.com/b.js |
同一域名,不同协议 | 不允许 |
http://www.a.com/a.js http://10.11.12/b.js |
域名和域名对应的 IP | 不允许 |
http://www.a.com/a.js http://script.a.com/b.js |
一级域名相同,二级域名不同 | 不允许 |
http://www.a.com/a.js http://a.com/b.js |
一级域名相同,二级域名不同 | 不允许(cookie 也不会允许访问) |
http://www.a.com/a.js http://www.b.com/b.js |
不同主域名 | 不允许 |
特别说明
- 如果是协议和端口造成的跨域问题“前台”是无能为力的。
- 在跨域问题上,仅仅是通过“URL 的首部”来识别而不会根据域名对应的 IP 地址是否相同来判断。“URL 的首部”可以理解为“协议, 域名和端口必须匹配”。
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
通过表单的方式可以发起跨域请求,为什么Ajax
就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax
可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF
,因为请求毕竟是发出去了。
跨域解决方案
JSONP
原理
网页通过添加一个<script>
元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。然后在 src
中函数会立即执行。因此需要服务器的支持才可以。
JSONP 和 AJAX 对比
都是客户端向服务器端发送请求,从服务端获取数据。
AJAX 属于同源策略,JSONP 属于非同源策略(跨域请求)。
优缺点
优:兼容性好,可以用于解决主流浏览器的跨域数据访问的问题。
缺:仅支持get
方法,具有局限性,不安全,可能会遭到XSS
攻击。
实现流程
- 声明一个回调函数
callFun
,函数名作为参数传递给服务器,形参就是要获取的数据 - 创建一个
script
标签,把跨域的 API 接口地址赋值给script
的src
属性,还要在地址中向服务器传递上一步创建的函数参数,比如<script src='http://192.168.0.1:8001/get_data?callback=callFun'></script>
; - 服务器接收到请求后,获取请求中的 callback 函数名(
callFun
)拼接上返回的数据,比如callFun({msg:'返回数据'})
; - 服务器把返回值通过 HTTP 协议返回给客户端,客户端调用执行声明的回调函数,对返回的数据进行操作。
封装 JSONP 函数
1 | //index.html |
使用
1 | jsonp({ |
上面的请求相当于向http://localhost:8001/get_data?msg=lalala&callback=show
服务端
1 | let express=require('express'); |
jQuery 的 jsonp 形式
JSONP 都是 GET 和异步请求的,不存在其他的请求方式和同步请求,且 jQuery 默认就会给 JSONP 的请求清除缓存。
1 | $.ajax({ |
CORS
需要浏览器和后端同时支持,IE8 和 9 需要通过XDomainRequest
来实现。
浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置响应头 Access-Control-Allow-Origin
配置:
*
表示服务器设置任意域都同意设置允许一个跨域域名
node1
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
koa2
1
2
3origin: function(ctx) { //设置允许来自指定域名请求
return 'http://localhost:3000'; //只允许http://localhost:3000这个域名的请求
},设置允许多个跨域域名
node1
res.header('Access-Control-Allow-Origin', 'http://weipxiu.com,http://localhost:8001,http://localhost:3000');
koa2
1
2
3
4
5
6
7
8
9origin: function (ctx) {
//多个域
const whiteList = ["http://weipxiu.com","http://localhost:8081","http://localhost:3000"]; //可跨域白名单
let url = ctx.header.origin;
if (whiteList.includes(url)) {
return url; //注意,这里域名末尾不能带/,否则不成功,所以在之前我把/通过substr干掉了
}
return "http://localhost:8001"; //默认允许本地请求8001端口可跨域
},如果服务器返回的信息中没有该字段信息,这个数据将会被浏览器拦截,无法到达请求方,浏览器并会报错。
postMessage
postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
解决以下问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间信息传递
- 页面与嵌套的
iframe
消息传递 - 上面三个场景的跨域数据传递
1 | otherWindow.postMessage(message,targetOrigin,[transfer]); |
- message:将要发送到其他 window 的数据
- targetOrigin:通过窗口的
origin
属性来指定哪些窗口能接收到消息事件,可以是*
(表示无限制)或一个URL
。在发送消息的时候回,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin
提供的值,消息就不会被发送,只有三者完全匹配,消息才会被发送。 - transfer(可选):一串和
message
同时传递的Transferable
对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
例子
接下来我们看个例子: http://localhost:3000/a.html
页面向http://localhost:4000/b.html
传递“我爱你”,然后后者传回”我不爱你”。
a.html 运行在 localhost:3000
可以使用express
创建服务器
1 | <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> |
b.html 运行在 localhost:4000
1 | window.onmessage = function (e) { |
WebSocket
WebSocket
是 HTML5 的持久化的协议,实现浏览器和服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket
和HTTP
都是应用层协议,都是基于TCP
协议。但是WebSocket
是一个双工通信协议,建立连接之后,server
和client
都能主动向对方发送或接收数据。WebSocket
在建立连接时需要借助 HTTP
协议,连接建立好了之后client
与server
之间的双向通信就与HTTP
无关。
Socket.io
很好地封装了WebSocket
接口,提供了更简单、灵活的接口,也对不支持WebSocket
的浏览器提供了向下兼容。
例子:本地文件 socket.html 向 localhost:8000 发生数据和接受数据
socket.html
1 |
|
server.js
1 | var WebSocketServer = require("ws").Server, |
Node 中间件代理(两次跨域)
原理:同源策略是浏览器需要遵循的标准,而如果服务器向服务器请求就无需遵循同源策略。
代理服务器,需要以下步骤:
- 接收客户端请求
- 将请求转给服务器
- 拿到服务器响应的数据
- 将数据转发给客户端
例子:本地 index.html 文件,通过代理服务器http://localhost:3000
向目标服务器http://localhost:4000
请求数据
index.html(http://127.0.0.1:5500)
1 | <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> |
server1.js 代理服务器(http://localhost:3000)
1 | // server1.js 代理服务器(http://localhost:3000) |
server2.js(http://localhost:4000)
1 | const http = require('http') |
经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略,最后在index.html
文件打印出{"title":"fontend","password":"123456"}
nginx
反向代理
实现原理类似于Node
中间件代理,需要你搭建一个中转nginx
服务器,用于转发请求。
使用nginx
反向代理实现跨域,是最简单的跨域方式。只需要修改nginx
的配置即可解决跨域问题,支持所有浏览器,支持 session
,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx
配置一个代理服务器(域名与domain1
相同,端口不同)做跳板机,反向代理访问domain2
接口,并且可以顺便修改cookie
中domain
信息,方便当前域cookie
写入,实现跨域登录。
先下载 nginx,然后将 nginx 目录下的 nginx.conf 修改如下:
1 | server { |
window.name+iframe
window.name 属性的独特之处:在一个窗口(window)的生命周期内,name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
例子,页面 a.htmlhttp://localhost:3000/a.html获取页面 b.htmlhttp://localhost:4000/b.html的数据
b.html
1 | <script> |
a.html
1 | <script> |
location.hash+iframe
具体实现步骤:一开始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
。
a.html
1 | <!-- 路径后面的hash值可以用来通信 --> |
b.html
1 | <script> |
c.html
1 | <script> |
document.domain+iframe
该方法只能用于二级域名相同的情况下,比如a.test.com
和b.test.com
适用于该方法。
实现原理:两个页面都通过js
强制设置document.domain
为基础主域,就实现了同域。
例子:页面a.zf1.cn:3000/a.html
获取页面b.zf1.cn:3000/b.html
中的a
的值
a.html
1 | <body> |
b.html
1 | <body> |
总结
CORS
支持所有类型的HTTP
请求,是跨域HTTP
请求的根本解决方案;JSONP
只支持GET
请求,JSONP
的优势在于支持老式浏览器,以及可以向不支持CORS
的网址请求数据;- 不管是
Node
中间件代理还是nginx
反向代理,主要是通过同源策略对服务器不加限制; - 日常工作中,用得比较多的跨域方案是
CORS
和nginx
反向代理。