跨域方案整理

在前后端分离的大背景下,跨域是非常常见的一个问题,简单整理一下对应解决的方案。

概要

一、同源策略

同源策略是一个安全策略,限制了一个 orgin 的文档或者他加载的脚本如何与另外一个源的资源进行交互,可以杜绝恶意文档,减少可能被攻击的媒介。

怎么判断是否同源

两个 Url 的 protocol,port 和 host 都是相同的话,那么这两个 Url 是同源,反之则不同源。

二、跨域请求

当文档对不同源的服务发起数据交互,那么这个时候发的就是跨域请求。

注:1. 跨域是浏览器的一个自身的行为,出发点是 web 安全。

解决方案

一、CORS(跨域资源共享)

使用额外的 http 头告诉浏览器,让当前 orgin 的 web 应用可以访问不同源服务器上的指定资源。

**QA:***为什么有些请求在浏览器的调试工具 network 面板有多一个 options?*

**AN:***因为通常跨域请求可以非为”简单请求”以及”非简单请求”,在发起非简单请求的时候,浏览器会事先发一个预检请求(options)询问源服务器是否能访问对应的资源;*

简单请求的满足条件
  1. 请求方法为 GET、HEAD、PUT
  2. 头部字段只能包含
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(有额外限制)
    • DPR
    • Downlink
    • Save-Data
    • Vieport-Width
    • Width
  3. Content-Type 仅限于以下三者
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  4. 请求中的任意 XMLHttpRequestUpload 对象均没有主持任何监听事件
  5. 请求中没使用 ReadableStream 对象

以上简单请求的一个满足条件,只要有一条满足不了,那么就属于非简单请求,在发起跨域请求的时候,浏览器就是发送预检请求。

处理

以下是基于 nodejs express 的处理,基于 中间件 cors

普通处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var express = require('express');
var cors = require('cors');
var app = express();

/*
cors 中间件的默认配置,
defaults = {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204
};
*/
app.use(cors());

只允许 a.com 这个根域名跨域请求

1
2
3
4
5
app.use(cors({
origin:function(origin,callback){
callback(null,/^\w+?\.a\.com$/.test(orign));
}
}))

需要跨域带 cookie 的情况

  1. 动态设置 origin
  2. credentials 设置为 true
  3. 客户端
    1. ajax 需要带 credentials:true 的头部
    2. cookie 共享必须基于同根域名且 cookie 的 domain 配置成根域。
1
2
3
4
5
6
7
8
app.use(
cors({
origin:function(origin,callback){
callback(null,true)
},
credentials:true
})
);

设置特殊的头部信息(例如:header 添加 token 字段[CSRF 的一种解决方案])

  1. 增加自定义的头部需要先在后端添加对应的 allowedHeadres
  2. alloweHeadres 不能直接设置为 *
1
2
3
4
5
6
7
8
9
app.use(
cors({
allowedHeaders:'X-Requested-With,Cache-Control,Content-Language,Content-Type,deviceType,appID,subAppID,deviceId,clientVersion,sessionKey'
origin:function(origin,callback){
callback(null,true)
},
credentials:true
})
);

当发送复杂请求的时候,不想每次都发 options,例如:轮询的一个场景

1
2
3
4
5
6
7
8
9
10
app.use(
cors({
allowedHeaders:'X-Requested-With,Cache-Control,Content-Language,Content-Type,deviceType,appID,subAppID,deviceId,clientVersion,sessionKey'
origin:function(origin,callback){
callback(null,true)
},
credentials:true,
maxAge:600 //单位:秒
})
);

二、Nginx

Nginx 需要处理的是对 options 做处理,以及对其他请求添加对应的头部信息,然后转发给服务器

查看 nginx.conf 的 include xxxx/*.conf,到 xxxx 目录下面添加 abc.conf,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen abc.com
location/api {
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE';
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'appId,Token,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,X_Requested_With,If-Modified-Since,Cache-Control,Content-Type,appId,clientVersion,deviceId,deviceType,subAppId';
add_header 'Access-Control-Max-Age' 600;
if ($request_method = 'OPTIONS'){
return 204;
}
proxy_pass http:127.0.0.1:8080
}
}

在需要带 cookie 的情况,除了设置 allow-credentials 为 true 之外,allow-origin 也不能设置为 * ;对应部分配置如下

1
2
3
4
5
6
location/api {
...
add_header 'Access-Control-Allow-Origin' $http_origin
add_header 'Access-Control-Allow-Credentials' 'true';
}

三、jsonP(json with padding)

利用网页可以访问跨域静态资源的特性,以 script callbackfn 的形式来实现跨域数据交互。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//客户端
function handleCallback(result) {
console.log(result.message);
}

var jsonp = document.createElement('script');
var ele = document.getElementById('demo');
jsonp.type = 'text/javascript';
jsonp.src = 'http://localhost:8080?callback=handleCallback';
ele.appendChild(jsonp);
ele.removeChild(jsonp);


//node
router.get('/',(req,res)=>{
let {callback} = req.query;
let data = {
test:1111
};
if(callback){
res.type('text/javascipt');
res.send(`${callback}(${JSON.stringify(data)})`);
}
res.send(`${callback}(${JSON.string(data)})`)
});

四、postMessage & Iframe

window.postMessage()方法可以安全地实现跨域通信。通过获取对应窗口的引用,在窗口上调用 targetWindow.postmessage 的方法发送一个 messageEvent 消息,接收方通过监听 message 事件来捕获 message

场景:一个运营后台需要预览编辑的问卷在移动端 web 页面显示的情况,在后台跟移动端 web 不同域名的情况下,用 postMessage 来解决数据通信。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//后台
...
async handlePreView(){
let data = await this.$form.validateFields(),
{
rules=[],
questions=[]
} = data;
if(rules.length < 1 || questions < 1){
window.message.error('题目和计分规则不能为空');
return;
}
this.$iframe.contentWindow.postMessage(JSON.stringify(data),'*');
}


//移动端
window.addEventListener('message',(event)=>{
if(event.data !== 'string') return;
const data = JSON.parse(event.data);
console.log(data);
},false)

参考资料

  • MDN 文档