前端跨域解决方案整理

323 阅读5分钟

什么是同源策略

如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。

浏览器默认两个相同的源之间是可以相互访问资源和操作 DOM 的。两个不同的源之间若想要相互访问资源或者操作 DOM,那么会有一套基础的安全策略的制约,我们把这称为同源策略。

具体来讲,同源策略主要表现在 DOM、Web 数据和网络这三个层面。

第一个,DOM 层面。同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。

第二个,数据层面。同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。由于同源策略,我们依然无法通过第二个页面的 opener 来访问第一个页面中的 Cookie、IndexDB 或者 LocalStorage 等内容。你可以自己试一下,这里我们就不做演示了。

第三个,网络层面。同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。

实际情况下仍有需要跨域获取数据,发送请求等需求,那么该如何实现跨域呢?下面总结下较为常用的跨域方式。

跨域方案

相关代码请参考 github.com/mcuking/blo…

  • JSONP

  • CORS

  • postmessage

  • nginx反向代理接口跨域

  • document.domain + iframe

  • window.name + iframe

  • location.hash + iframe

其中 window.name + iframe 和 location.hash + iframe 因实际工作中不怎么应用(有更好的方式),因此暂不讲解。

JSONP

JSONP 主要是利用了 script 标签(还有 img 等标签)可以加载其他域的资源的特点,通过前后端约定的某种方式:

前端将已经定义的某个函数的名字传递给后端,例如 show,一般通过 url 后面添加 query 参数方式,后端获取到前端的请求,从请求中解析到该函数名 show,然后返回的结果中就包含了 show(arg) 的代码,其中 arg 就是前端需要跨域获取的数据,最后前端 js 环境就可以通过执行 show 获取到数据了。

示例代码如下:

前端部分

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 };    let arrs = [];    for (let key in params) {      arrs.push(`${key}=${params[key]}`);    }    script.src = `${url}?${arrs.join('&')}`;    document.body.appendChild(script);  });}jsonp({  url: 'http://localhost:3000/say',  params: { wd: '我爱你' },  cb: 'show'}).then(data => {  const divDom = document.createElement('div');  divDom.innerText = data;  document.body.appendChild(divDom);});

后端代码如下:

let express = require('express');let app = express();app.get('/say', function(req, res) {  let { wd, cb } = req.query;  console.log(wd);  res.end(`${cb}('我不爱你')`);});app.listen(3000);

JSONP 的缺陷主要在于只能通过 script 或 img 等标签的 src 属性发送请求,即只能发送 GET 请求。

CORS

CORS 主要是需要后端配置,和浏览器配合即可,无需前端介入。

下面简单阐述下原理:

当跨域发送请求时,浏览器会自动在请求头上添加 Origin 字段,用于说明本次请求来自哪个源(协议 + 域名 + 端口)。当后端发现发送该请求的页面所在域不在白名单中,则会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被 XMLHttpRequest 的 onerror回调函数捕获。如果在白名单中,则会在响应头添加下面字段:

// 值可以是请求头的 Origin, 或者是 *,表示允许所有域
// 当然还有其他相关字段
Access-Control-Allow-Origin: http://api.bob.com

当然这里还要区分简单请求和非简单请求,针对非简单请求,一般会在请求前发一个预检请求 OPTION。更多细节请参考阮一峰 跨域资源共享 CORS 详解

这里以 node 框架 express 为例展示下后端的相关配置:

let express = require('express');let app = express();let whitList = ['http://localhost:3001'];app.use(function(req, res, next) {  let origin = req.headers.origin;  if (whitList.includes(origin)) {    res.setHeader('Access-Control-Allow-Origin', origin);    // 设置请求头中可以携带的字段    res.setHeader('Access-Control-Allow-Headers', 'name');    // 设置哪些请求方法可以访问    res.setHeader('Access-Control-Allow-Methods', 'PUT');    // 指定本次预检请求的有效期,单位为秒,,在此期间不用发出另一条预检请求    res.setHeader('Access-Control-Max-Age', 6000);    // 允许请求头中可以携带 cookie    res.setHeader('Access-Control-Allow-Credentials', true);    // 设置返回头中可以获取的字段    res.setHeader('Access-Control-Expose-Headers', 'name');    if (req.method === 'OPTIONS') {      res.end(); // OPTIONS 请求不做任何处理    }  }  next();});app.put('/getData', function(req, res) {  res.setHeader('name', 'lili');  res.end('我不爱你');});app.listen(3002);

postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递

  • 多窗口之间消息传递

  • 页面与嵌套的iframe消息传递

用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

示例代码如下:

a.html 代码

<iframe  src="http://localhost:3004/b.html"  frameborder="0"  id="frame"  onload="load()"></iframe><script>  function load() {  let frame = document.getElementById('frame');  frame.contentWindow.postMessage('我爱你', 'http://localhost:3004');  }  window.onmessage = function(e) {  console.log(e.data);  };</script>b.html 代码<script>  window.onmessage = function(e) {    console.log(e.data);    e.source.postMessage('我不爱你', e.origin);  };</script>

nginx反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口。

nginx 配置

#proxy服务器
server {    listen       81;    server_name  www.domain1.com;    location / {        proxy_pass   http://www.domain2.com:8080;  #反向代理        index  index.html index.htm;    }}

前端代码

const xhr = new XMLHttpRequest();// 访问nginx中的代理服务器xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);xhr.send();

document.domain + iframe

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

实例代码:

a.html 代码

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe><script>    document.domain = 'domain.com';    var user = 'admin';</script>

b.html 代码

<script>    document.domain = 'domain.com';    // 获取父窗口中变量    alert('get js data from parent ---> ' + window.parent.user);</script>