跨域终结篇

作者:ManfredHu
链接:http://www.manfredhu.com/2019/01/15/42-cross-origin/index.html
声明:版权所有,转载请保留本段信息,谢谢大家

crossOrigin

前言

github代码地址:https://github.com/ManfredHu/cross-origin

原来的浏览器是裸奔的,会给人看光光。就算你加密了,还是有办法看到,所以各大浏览器裸奔厂商商量好了规范,就是禁止源origin访问其他源下的资源。

同源要求:同协议(http/https属于不同的协议),同域名IP,同端口。

判断是否跨域看下图:
是否同源判断

CORS

请求头带orgin到服务器,然后服务器返回Access-Control-Allow-origin到客户端,客户端检测返回跟现在的域是否相同,通过协议http,域名,端口判断。

axios开启cookie传输

我们知道cookie是会在每一个请求的request header加上的,axios默认是发送请求的时候不会带上cookie的,需要通过设置withCredentials: true来解决。
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
axios.get(url, {'withCredentials': true})
.then(function (response) {
// handle success
if(response.data.retCode === 0){
log(`收到来自${url}的返回数据`)
log(JSON.stringify(response.data))
}
})
.catch(function (error) {
// handle error
log('请求失败')
console.log(error);
})

项目例子对应启动代码如下。具体的实现可以看代码

1
2
npm run cors:node
npm run cors:web

express开启cookie传输

类似的,用到了cors中间件,就会在response header返回加上Access-Control-Allow-Credentials: true的返回头。
这个时候就可以传输cookie了。

1
2
const cors = require('cors')
app.use(cors({credentials: true, origin: 'http://localhost:9000'}))

经测试,express开启设置Access-Control-Allow-Origin: *支持跨域传输数据。但是,如果前端axios开启了cookie传输,就是上面的'withCredentials': true选项,则后端不能开启Access-Control-Allow-Origin: *,否则会报下面的错误。

1
2
3
4
5
Access to XMLHttpRequest at 'http://localhost:3000/cors' from origin 'http://localhost:9000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

index.js:19 Error: Network Error
at createError (createError.js:16)
at XMLHttpRequest.handleError (xhr.js:87)

所以如果有开启cookie的需求,则按照上面示例代码设置。

JSONP

缺点:只支持get,需要服务端支持,需要挂载全局函数(windows)。

通过script标签可以加载不同源下的文件来实现,因为在网页里,图片,css,js三种文件一般是放在cdn的,可以加快访问速度。
那其实网页也是允许图片,css,js之类的文件放在不同源上的,这样也可以加快加载效率。

所以这种方式跨域本质上是浏览器支持的,只是运用起来需要后端支持。

前端代码:

1
2
3
4
5
6
7
const jsonpScript = document.createElement('script')
jsonpScript.src = 'http://localhost:3000/jsonp?callback=jsonpCallback'
document.body.appendChild(jsonpScript)

window.jsonpCallback = (data) => {
// todo
}

后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
app.get(item, (req, res) => {
let callback = req.query.callback; //前端需要的回调函数
let obj = {
name: 'manfredhu',
age: '25'
};
const result = Object.assign({} ,obj, {retCode: 0})
res.writeHead(200, {
"Content-Type": "text/javascript"
});
res.end(callback + '(' + JSON.stringify(result) + ')');
})

这样后端不用开启任何跨域设置,直接返回参数包裹的函数和数据就好了。

项目例子对应启动代码如下。具体的实现可以看代码

1
2
npm run jsonp:node
npm run jsonp:web

服务器转发请求跨域

缺点:需要服务器支持

nginx & CDN

nginx和CDN都可以设置响应报头。所以原理上来说都跨域支持跨域,这里不细说,需要的同学自行实现。

node

node做为接入层的话,收到前端的请求,node可以去获取其他域的数据,因为服务器是不受跨域影响的。
一般的,有中间件可以用,我用的是http-proxy-middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const proxy = require("http-proxy-middleware");
const baseConfig = {
port: 3000
}
app.use(
"/",
proxy({
// 代理跨域目标接口
target: "http://localhost:3001",
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带 cookie
onProxyRes: function(proxyRes, req, res) {
res.header("Access-Control-Allow-Origin", "http://localhost:9000");
res.header("Access-Control-Allow-Credentials", "true");
},

// 修改响应信息中的 cookie 域名
cookieDomainRewrite: "localhost" // 可以为 false,表示不修改
})
);

项目例子对应启动代码如下。具体的实现可以看代码

1
2
3
npm run proxy:web
npm run proxy:node1
npm run proxy:node2

postMessage

通常,相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。

浏览器接口

1
otherWindow.postMessage(message, targetOrigin, [transfer]);
  • otherWindow
    其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
  • message
    将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。
  • targetOrigin
    通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”“(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;例如,当用postMessage传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的origin属性完全一致,来防止密码被恶意的第三方截获。如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。
  • transfer
    是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

这里有个问题要特别注意下,如下代码广播数据到指定的域时候,有时候会自己也会触发自己监听的window.onmessage。例子端口分别起在9000和9001也会,消息前后顺序不确定。所以如果要确切的收到来自某个跨域消息的话,window.onmessage要做过滤。

1
ifr.contentWindow.postMessage(data, 'http://localhost:9001');
1
2
3
4
5
6
7
const otherOrigin = 'http://localhost:9001'
window.onmessage = function (e) {
log(`收到otherPage的postMessage返回的消息`)
if(e.origin === otherOrigin){ //这里做过滤
log(JSON.stringify(e.data))
}
}

项目例子对应启动代码如下。具体的实现可以看代码

1
2
npm run postMessage:web1
npm run postMessage:web2

修改document.domain来跨子域

通常我们可以这么干,自己用JS动态添加一个iframe,一般是隐藏不显示,通过设置display:none
然后在iframe里面设置如下的代码

1
document.domain = "localhost" //这里不用端口只判断是否是子域

然后在我们的页面就可以动态载入一个存在服务器上的静态页面或者动态生成内容的页面。返回给前端,前端设置自己也在这个域内,代码如上,就可以起到跨域通信的目的。

有一点要特别注意的是,获取iframe的document对象的时候。要通过contentWindow属性,其他属性一概不行或者错误。

1
2
const ifr = document.getElementsByTagName('iframe')[0]
const ifrDoc = ifr && ifr.contentWindow && ifr.contentWindow.document

项目例子对应启动代码npm run domain。具体的实现可以看代码

window.name

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。

注意,window.name的值只能是字符串的形式,这个字符串的大小最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器,但一般是够用了。

其实跟子域的设置方式类似,就是创建一个隐藏的iframe,然后获取到window对象,拿到那个页面挂在window.name的信息。

1
window.name = JSON.stringify({"msg": "这是来自otherPage的数据", "type": "window.name"})

1
2
3
const ifr = document.getElementsByTagName('iframe')[0]
const ifrWin = ifr && ifr.contentWindow && ifr.contentWindow
log(`${ifrWin.name}`)

项目例子对应启动代码npm run windowName。具体的实现可以看代码

使用 WebSocket 实现跨域

这个不说了,WS是没有域限制的,不同域名、端口下随便连接,但是后台WS服务一般有鉴权,所以也不是随便就可以连伤的。还可以开多个WS连接链接好几个不同源(亲测3个稳定连接,在微信手Q里面)

参考


Copyright © 2015 - 2019 ManfredHu胡文峰的个人博客

All rights reserved. Designed and powered by ManfredHu.

粤ICP备18133029号