跨域资源共享的方法和CORS

514 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第24天,点击查看活动详情

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。

解决跨域有很多种方法,其本质就是搞定同源策略。常用的有 jsonpiframecorsimgHTML5 postMessage等等。其中还有用到 html 标签进行跨域的原理就是 html 不受同源策略影响。但只是接受 Get 的请求方式,这个需要注意。

我们知道同源策略并不是禁止跨域请求,而是在请求后拦截了请求的回应。但是引用图片、css、js 文件等资源的时候就允许跨域。

1. JSONP[利用script标签]

JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的url并提供一个回调函数拼在url中来接收数据当需要通讯时。这种方式需要后端接口根据实际情况配合,约定好回调函数的函数名。

<!-- 接口传参并指定回调执行函数名为jsonp -->
<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    // 回调函数
    function jsonp(data) {
        console.log(data)
    }
</script>

script跨域的优缺点

  • 优点:可以直接返回json格式的数据,方便处理
  • 缺点:只接受GET请求方式

2. postMessage

H5中新增的postMessage()方法,可以用来做跨域通信。

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息。

// 发送消息端
window.parent.postMessage('message', 'http://blog.poetries.com');

// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
    var origin = event.origin || event.originalEvent.origin;
    if (origin === 'http://blog.poetries.com') {
        console.log('验证通过')
    }
});

场景示例:窗口 A (http:A.com)向跨域的窗口 B (http:B.com)发送信息。步骤如下

// 窗口A(http:A.com)向跨域的窗口B(http:B.com)发送信息
Bwindow.postMessage('data', 'http://B.com'); //这里强调的是B窗口里的window对象  

// 在窗口B中监听 message 事件
Awindow.addEventListener('message', function (event) {   //这里强调的是A窗口里的window对象
  console.log(event.origin);  //获取 :url。这里指:http://A.com
  console.log(event.source);  //获取:A window对象
  console.log(event.data);    //获取传过来的数据
}, false);

3. document.domain + iframe

  • 此方案仅限主域相同,子域不同的跨域应用场景。比如 a.test.comb.test.com 适用于该方式。
  • 只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

具体示例操作

  1. 父窗口:http://www.test.com/a.html
<iframe id="iframe" src="http://child.test.com/b.html"></iframe>
<script>
    document.domain = 'test.com';
    var user = 'admin';
</script>
  1. 子窗口:http://child.test.com/b.html
<iframe id="iframe" src="http://child.test.com/b.html"></iframe>
<script>
    document.domain = 'test.com';
    var user = 'admin';
</script>

优缺点

  • 优点:跨域完毕之后DOM操作和互相之间的JavaScript调用都是没有问题的

  • 缺点:

    1.若结果要以URL参数传递,这就意味着在结果数据量很大的时候需要分割传递,比较麻烦。

    2.还有一个是iframe本身带来的,母页面和iframe本身的交互本身就有安全性限制。

4. Hash

  • url#后面的内容就叫HashHash的改变,页面不会刷新。这就是用 Hash 做跨域通信的基本原理。

补充:url?后面的内容叫SearchSearch的改变,会导致页面刷新,因此不能做跨域通信。

场景举例:页面 A 通过iframeframe嵌入了跨域的页面 B后,A页面想给B页面发消息,如何操作?

  1. A页面
// A中伪代码
var B = document.getElementsByTagName('iframe');
    B.src = B.src + '#' + 'jsonString';  //我们可以把JS 对象,通过 JSON.stringify()方法转成 json字符串,发给 B
  1. B页面
// B中的伪代码
window.onhashchange = function () {  //通过onhashchange方法监听,url中的 hash 是否发生变化
  var data = window.location.hash;
};

5. webSocket

WebSocket:不受同源策略的限制,支持跨域。

WebSocket的用法如下:

 var ws = new WebSocket('wss://echo.websocket.org'); //创建WebSocket的对象。参数可以是 ws 或 wss,后者表示加密。

//把请求发出去
ws.onopen = function (evt) {
  console.log('Connection open ...');
  ws.send('Hello WebSockets!');
};
//对方发消息过来时,我接收
ws.onmessage = function (evt) {
  console.log('Received Message: ', evt.data);
  ws.close();
};
//关闭连接
ws.onclose = function (evt) {
  console.log('Connection closed.');
};

6. CORS

CORS:不受同源策略的限制,支持跨域。一种新的通信协议标准。可以理解成是:同时支持同源和跨域的Ajax

  • CORS需要浏览器和后端同时支持
  • 浏览器会自动进行 CORS 通信,实现CORS通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 后端在头部信息里面设置允许跨域访问的安全域名:服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符*则表示所有网站都可以访问资源

fetch是一个比较新的API,用来实现CORS通信。用法如下:

// url(必选),options(可选)
fetch('/some/url/', {
  method: 'get',
}).then(function (response) {  //类似于 ES6中的promise
  // dosomething
}).catch(function (err) {
  // 出错了,等价于 then 的第二个参数,但这样更好用更直观
});

CORS为什么支持跨域的通信?因为跨域时,浏览器会拦截Ajax请求,并在http头中加Origin

7. img

new Image, 设src 的时候,图片需要设置Cors

cors需要后台配合设置HTTP响应头,如果请求不是简单请求,浏览器会先发送option预检请求,后端需要响应option请求,然后浏览器才会发送正式请求,cors通过白名单的形式允许指定的域发送请求

简单请求的情况:

  1. 请求method为:get,post,head

  2. content-type:三种表单自带的content-type

    text/plain

    multipart/form-data

    application/x-www-form-urlencoded

  3. 没有自定义的HTTP header

以上情况有任意一点不满足,就不算简单请求。比如我们常发起的请求的content-typeapplication/json就不是简单请求。

优缺点

  • 优点:可以访问任何url,一般用来进行点击追踪,做页面分析常用的方法
  • 缺点:不能访问响应文本,只能监听是否响应

8. webpack中进行反向代理

react的umi脚手架或vue的vue-cli的框架中都可以通过proxy来配置

其本质是配置在了webapckdevServer中的proxy中,提供了反向代理能力

devServer: {
  proxy: {
    '/api': {
      target: 'http://www.example.com', // your target host
      changeOrigin: true, // needed for virtual hosted sites
      pathRewrite: {
        '^/api': ''  // rewrite path
      }
    },
  }
}

proxy工作原理实质上是利用http-proxy-middleware 这个http代理中间件,实现请求转发给其他服务器

webpack内部需要配合 http-proxy-middleware 插件对 api 请求地址进行代理

const express = require('express');
const proxy = require('http-proxy-middleware');
// proxy api requests
const exampleProxy = proxy(options); // 这里的 options 就是 webpack 里面的 proxy 选项对应的每个选项

// mount `exampleProxy` in web server
const app = express();
app.use('/api', exampleProxy);
app.listen(3000);

然后再用 nginx 把允许跨域的源地址添加到报头里面即可。

9. nginx代理跨域

nginxCORS 配置,大致如下:

location / {
  if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' '*';  
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Headers' 'DNT, X-Mx-ReqToken, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type';  
    add_header 'Access-Control-Max-Age' 86400;  
    add_header 'Content-Type' 'text/plain charset=UTF-8';  
    add_header 'Content-Length' 0;  
    return 200;  
  }
}

10. 使用nodejs中间件代理

可以自己写一段nodejs同时启一个服务端做中间层

此时的环境中(如本地开发时)本地的前端页面发起ajax请求到本地的node服务端是不跨域的,此时在node中进行一个转发,node服务端到真实服务端的访问也是不跨域的(同源策略是浏览器拦截了ajax的相应),这样就可以绕过跨域。

实现与webpack的proxy解决跨域原理的相同。