07月02, 2018

跨域的一些总结

前几天同事分享一个关于跨域的主题,文章在这里:前端跨域问题总结。写的相当赞。

此篇文章算是那篇的拓展篇,会更加详细和丰满一些。

现在市面上流行的方案大抵是以下几种:

  • jsonp
  • CORS
  • postMessage
  • document.domain
  • location.hash
  • window.name
  • WebSocket
  • http-proxy
  • Nginx

一些准备

学一下简单的express使用,能写一个简单的get/post请求,知道如何获取参数,如何输出结果。举个简单的例子:

const express = require("express");
const server = express();
server.get("/hello", function(req, res) {
    res.setHeader("Content-Type", "text/html; charset=utf-8");
    res.end("你好啊");
})
server.listen(3333);

下载一个postman软件,大概会用就OK了。

jsonp

它的原理就是插入一个script,向服务器发起请求,在URL中拼接上前后端约定的回调参数,服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

前端实现可以这样来做:

function jsonp({ url, params, cb }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script');
    window[cb] = function (data) {
      resolve(data);
      document.body.removeChild(script);
    }
    params = { ...params, cb } // wd=b&cb=show
    let arrs = [];
    for (let key in params) {
      arrs.push(`${key}=${encodeURIComponent(params[key])}`);
    }
    script.src = `${url}?${arrs.join('&')}`;
    document.body.appendChild(script);
  });
}

// 使用:
jsonp({
  url: 'http://localhost:3333/hello',
  params: { word: '你好' },
  cb: 'show'
}).then(data => {
  console.log(data);
});

服务端代码:

server.get("/hello", function (req, res) {
    let params = req.query;
    console.log(params.word);
    res.setHeader("Content-Type", "text/html; charset=utf-8");
    res.end(`${params.cb}("你好啊")`);
})

jsonp的缺陷:只能发送get请求 不支持post put delete,不安全(容易被XSS),所以一般现在不怎么采用了。

CORS

这个主要是后端进行处理,前端基本上不用做啥处理。

很多人都知道后端只要设置一个Access-Control-Allow-Origin的header头,就OK了。

这里有两个问题出来了:

  • 设置具体的域名和设置*,有没有什么本质上的区别?而不是简单的安全问题
  • 除了这个请求头,还有什么请求头需要考虑的么?

在express中可以写一个中间件,让所有的请求都能流过这个中间件。我们可以在该中间件中处理相应的请求头。

let whitList = ['http://localhost:3336'];
server.use(function (req, res, next) {
    let origin = req.headers.origin;
    if (whitList.includes(origin)) {
        // 设置哪个源可以访问我
        res.setHeader('Access-Control-Allow-Origin', origin);
    }
    next();
});

前端上的实现(因为上面的白名单是3336端口,所以我们可以起一个静态服务,如http-server)

 fetch("http://localhost:3333/getData").then(res => res.text()).then(data => {
    console.log(data);
  })

服务端的代码:

server.get("/getData", function(req, res) {
    res.end("123");
})

似乎没啥问题?

涉及到复杂场景就有问题了:

alt

method

譬如我们发一下PUT请求(当然服务端也得写一个PUT请求)

前端代码改成:

fetch("http://localhost:3333/getData", {
  method: "put"
}).then(res => res.text()).then(data => {
  console.log(data);
})

服务端新增一个put请求:

server.put("/getData", function(req, res) {
    res.end("123");
})

alt

因此,需要在中间件里面,再加一个请求头:

 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT'); // 注意大写

PUT会发两个请求,一次是OPTION请求,一个是正常请求。

与它相关联的,还有一个请求头:Access-Control-Max-Age,它表示以秒为单位的缓存时间,可以用它来缓存上面的OPTION请求

res.setHeader('Access-Control-Max-Age', 10); // 10s内不再发option请求

alt

cookie

还是上面的页面,我们可以在控制台手动写入一些cookie。

因为fetch默认是不支持发送cookie的,需要手动加一个参数才行:

fetch(url, {
    credentials: "include"
})

然后我们就可以得到这样的一个错误:

alt

解决的方式,也是要配一个请求头:Access-Control-Allow-Credentials

res.setHeader('Access-Control-Allow-Credentials', true);

然后服务端可以通过req.cookies,来拿到cookie对象。需要注意的是这个写法,在4.x版本后不再有效,需要加一个cookie-parser的中间件才行。

另外,如果加了这个cookie请求头,那么上面的origin请求头,就不能设置*,即代码如下:

res.setHeader('Access-Control-Allow-Origin', "*");

不然就会报一个错误:

alt

当然出于安全考虑,我们也不会简单粗暴地设置为*

自定义请求头

比如我们前端需要发一个自定义的请求头:

fetch("http://localhost:3333/getData", {
  method: "get",
  credentials: "include",
  headers: {
    name: "123"
  }
}).then(res => res.text()).then(data => {
  console.log(data);
})

alt

同样需要在服务端再配一个请求头:

// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers','name');

多个值用,分隔,需要注意的是不能是*

这里有一个headers比较特殊:Content-Type,当它的值为application/json,也需要配置,因为默认是不支持这种格式的。

自定义响应头取值

比如我们服务端写了一个头:

res.setHeader("name", "xxx");

前端应该是可以通过下面的代码来取到name的:

fetch("http://localhost:3333/getData", {
  method: "get",
  credentials: "include",
  headers: {
    age: "123",
    "Content-Type": "application/json",
  }
}).then(res => {
  console.log(res.headers.get("name")); // 这里获取服务端的头
  return res.text()
}).then(data => {
  console.log(data);
})

然而打印却是null

这个其实也要在服务端设置一个和上面类似的请求头:

// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name');

总结

跨域要设置的请求头大概示例如下:

res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT');
res.setHeader('Access-Control-Max-Age', 10);
res.setHeader('Access-Control-Allow-Headers',' Content-Type, name, age');
res.setHeader('Access-Control-Expose-Headers', 'name');

在实际面试中,我觉得得基本能回答出前两个才算过关。

WebSocket

它是基于ws的协议。

服务端代码:

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });
wss.on('connection', function (ws) {
    ws.on('message', function (data) {
        console.log(data);
        ws.send('你好啊')
    });
})

前端实现:

let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
  socket.send('你好'); // 在打开的时候,就发送
}
socket.onmessage = function (e) {
  console.log(e.data);
}

http-proxy

http-proxy

这个MS没啥可以展开的。

nginx

一般线上环境很多都是用了nginx,利用的就是它的反向代理能力。

刚好今天同事在使用nginx的时候,碰到一个问题:nodejs 通过nginx后出现响应慢,有些js文件、css文件请求都要1min多。

查了一下,在网上搜索到一篇文章:nodejs 通过nginx后出现响应慢的解决方法

server {
    listen 80;
    server_name mysite.com;
    location / {
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
        proxy_set_header    Host                $http_host;
        proxy_set_header    X-NginX-Proxy       true;
        proxy_set_header    Connection          "";
        proxy_http_version  1.1;
        proxy_connect_timeout 1; 
        proxy_send_timeout 30; 
        proxy_read_timeout 60;
        proxy_pass          http://localhost:3333;
    }
}

用了这个后,基本可以了。这里面配置比较多,有些我还不是太理解,所以等未来哪天nginx功力精进一些,再来详细说说。

iframe

上面提到的解决跨域的方案,剩下的都是页面之间的跨域解决。

老实讲,现在iframe的应用场景不多,不过在下面的两种场景倒是比较适合:

  • 登录页:多个平台通用
  • 用来检测是否页面响应成功

有多少人还记得jquery的ajaxFileUpload的实现方案,它的实现思路就是通过把页面提交到一个隐藏的iframe,监听iframe的onload

比如说一个下载的功能,前端请求发过去,后端要处理一些业务逻辑,可能需要好久才能得到响应,我们可以通过iframe做一个loading的功能。

postmessage

父页面,搞一个A文件夹,里面放一个index.html,然后起一个端口号,如:3000。

子页面,搞一个B文件夹,里面放一个index.html,然后起一个端口号,如:4000。

父页面里面的HTML内容,有一个iframe,src指向子页面

<iframe src="http://localhost:4000/index.html" frameborder="0" id="frame" onload="load()"></iframe>
<script>
    function load() {
        let frame = document.getElementById('frame');
        frame.contentWindow.postMessage('你好', 'http://localhost:4000');
        window.onmessage = function (e) {
            console.log(e.data);
        }
    }
</script>

子页面的html:

<script>
    window.onmessage = function (e) {
        console.log(e.data);
        console.log(e.source);
        e.source.postMessage('你好啊', e.origin)
    }
</script>

document.domain

这个方案有一定的局限性,它要求跨域的两个页面,主域必须一致。

比如说a.test.comb.test.com,它俩的主域都是test.com

随后在两边的页面的js中,都加上以下代码即可:

document.domain = 'test.com'

window.name

window对象拥有name属性,它有一个特点:相同协议下,在一个页面中,不随URL的改变而改变。

比如我们在百度页,在控制台输入:

window.name = "123";
location = "http://www.google.com";
console.log(window.name);

得到也是123。

现在有两个页面,同上:http://localhost:3000/index.htmlhttp://localhost:4000/index.html

具体实现:

在端口号3000的页面下,动态创建iframe,iframe的src指向4000的index.html,在iframe.onload成功之后,将iframe的src指向同源的一个页面(比如说在3000端口下,新建一个other.html)

var url = "http://localhost:4000/index.html"; 
var iframe = document.createElement('iframe')
var state = true;
iframe.onload = function(){
    if(state === true){
        iframe.src = 'other.html';
        state = false;
    }else if(state === false){
        state = null
        var data = iframe.contentWindow.name
        console.log(data)
    }
}
iframe.src = url
document.body.appendChild(iframe)
<!-- 端口号 4000的html -->
<script>
      window.name = "ssss";
</script>

实际中,我们也可以用它来和后端做数据处理,利用window.name+iframe跨域获取数据详解

location.hash

实现思路其实和上面的window.name差不多。

【跨域】location.hash

利用location.hash+iframe跨域获取数据详解

结语

个人觉得跨域这一块,只要学会以下几个,就差不多够用了:

  • nginx
  • cors
  • postmessage
  • websocket

其他的可以作为一种补充知识。

本文链接:www.my-fe.pub/post/cross-domain-note.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。