跨域方案整理

288 阅读2分钟

跨域方案整理

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

概要

一、同源策略

同源策略是一个安全策略,限制了一个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

普通处理

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的情况

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

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

  1. 增加自定义的头部需要先在后端添加对应的allowedHeadres
  2. 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文档