跨域方案整理
在前后端分离的大背景下,跨域是非常常见的一个问题,简单整理一下对应解决的方案。
概要
一、同源策略
同源策略是一个安全策略,限制了一个orgin的文档或者他加载的脚本如何与另外一个源的资源进行交互,可以杜绝恶意文档,减少可能被攻击的媒介。
怎么判断是否同源
两个Url的 protocol,port和host都是相同的话,那么这两个Url是同源,反之则不同源。
二、跨域请求
当文档对不同源的服务发起数据交互,那么这个时候发的就是跨域请求。
注:1. 跨域是浏览器的一个自身的行为,出发点是web安全。
解决方案
一、CORS(跨域资源共享)
使用额外的http头告诉浏览器,让当前orgin的web应用可以访问不同源服务器上的指定资源。
**QA:**为什么有些请求在浏览器的调试工具network面板有多一个options?
**AN:**因为通常跨域请求可以非为“简单请求”以及“非简单请求”,在发起非简单请求的时候,浏览器会事先发一个预检请求(options)询问源服务器是否能访问对应的资源;
简单请求的满足条件
- 请求方法为 GET、HEAD、PUT
- 头部字段只能包含
- Accept
- Accept-Language
- Content-Language
- Content-Type(有额外限制)
- DPR
- Downlink
- Save-Data
- Vieport-Width
- Width
- Content-Type仅限于以下三者
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 请求中的任意XMLHttpRequestUpload 对象均没有主持任何监听事件
- 请求中没使用ReadableStream对象
以上简单请求的一个满足条件,只要有一条满足不了,那么就属于非简单请求,在发起跨域请求的时候,浏览器就是发送预检请求。
处理
以下是基于nodejs express 的处理,基于 中间件 cors
普通处理
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这个根域名跨域请求
app.use(cors({
origin:function(origin,callback){
callback(null,/^\w+?\.a\.com$/.test(orign));
}
}))
需要跨域带cookie的情况
- 动态设置origin
- credentials设置为true
- 客户端
- ajax 需要带 credentials:true 的头部
- cookie共享必须基于同根域名且cookie的domain配置成根域。
app.use(
cors({
origin:function(origin,callback){
callback(null,true)
},
credentials:true
})
);
设置特殊的头部信息(例如:header添加token字段[CSRF的一种解决方案])
- 增加自定义的头部需要先在后端添加对应的allowedHeadres
- alloweHeadres不能直接设置为 *
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,例如:轮询的一个场景
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,内容如下
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也不能设置为 * ;对应部分配置如下
location/api {
...
add_header 'Access-Control-Allow-Origin' $http_origin
add_header 'Access-Control-Allow-Credentials' 'true';
}
三、jsonP(json with padding)
利用网页可以访问跨域静态资源的特性,以script callbackfn的形式来实现跨域数据交互。
如下:
//客户端
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来解决数据通信。 代码如下:
//后台
...
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文档