在前后端分离的大背景下,跨域是非常常见的一个问题,简单整理一下对应解决的方案。
概要
一、同源策略
同源策略是一个安全策略,限制了一个 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
普通处理
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 的情况
- 动态设置 origin
- credentials 设置为 true
- 客户端
- ajax 需要带 credentials:true 的头部
- 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 的一种解决方案])
- 增加自定义的头部需要先在后端添加对应的 allowedHeadres
- 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)
|
参考资料