CORS(跨域资源共享)浏览器安全机制
在页面中经常会加载一些静态资源例如图片、css、js文件, 这些文件并不都是来自同一个域的。他们并不受浏览器同源策略的限制。浏览器的同源策略限制的是从js脚本里发出的http请求, 例如fetch XMLHttpRequest等,这里的限制并不是不能发出,而是响应的结果会被阻止。除非有正确的响应头。
浏览器的同源策略:只允许协议、域名、端口相同的时候才可以在脚本里发送http请求
实现跨域的方法有很多种:

  1. JSONP
  2. WebSocket
  3. CORS
  4. postMessage
  5. document.domain
  6. window.name
  7. location.hash
  8. http-proxy
  9. nginx

下面我们一个一个的来介绍

JSONP

jsonp的原理就是因为新建一个script标签,通过这个标签来发送get请求。 因为不是在js脚本内发出的,所以不受浏览器同源策略的限制
优点:兼容性好
缺点:只支持get请求,容易被xss攻击,不安全所以现在基本上都不使用这种方式了
function addScriptTag(src){
var script = document.createElement('script');
script.setttribute('type','text/javascript');
script.src = src;
}
window.onload = function(){
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data){
console.log('your public ip address is'+ data.ip)
}
服务器收到请求后会把data放进foo函数里当做实参,类似与
foo({ip:0.0.0.0})
上面的结果会被当做script执行

下面我们来使用promise来封装jsonp <br />

  1. //=> 解构赋值
  2. function jsonp({url, params, cb}) {
  3. return new Promise((resolve, reject) => {
  4. // 手动创建一个 script,插入到 body 后面
  5. let script = document.createElement('script');
  6. // 拼接参数
  7. params = {...params, cb};
  8. let arr = [];
  9. for (let k in params) {
  10. arr.push(`${k}=${params[k]}`);
  11. }
  12. script.src = `${url}?${arr.join('&')}`;
  13. document.body.appendChild(script);
  14. // 创建一个全局 cb 回调函数
  15. // 数据请求回来之后,会执行这个函数,触发 resolve,完成使命,移除这个 script 标签
  16. // 继而执行 then 中定义的回调函数
  17. window[cb] = function (data) {
  18. resolve(data);
  19. document.body.removeChild(script);
  20. }
  21. })
  22. }

如果后端使用node express 那么后端代码怎么写呢

  1. let express = require('express')
  2. let app = express()
  3. app.get('/say',function(){
  4. let {wd,cb} = req.query
  5. res.end(`${cb}(data)`)
  6. })
  7. app.listen(3000)

CORS

CORS 是跨域资源共享的缩写 ,相比与JOSNP只能发送get请求, CORS没可以发起任何类型的请求,
目前主流的浏览器都支持,但是IE10以下的浏览器不支持
整个CORS通信过程都是由浏览器自动完成的,不需要前端代码的参与,对于前端开发这来说CORS和普通的AJAX并没有区别, 代码完全一样,浏览器一旦发现是一个跨源请求就会自动的添加一些头信息,又是还会出现附加一次的请求,但是用户并不会感知。
所以实现CORS的关键是服务器,只要浏览器实现了CORS接口,就可以实现跨源通信。
两种请求
浏览器将CORS请求分为两类,简单请求和非简单请求
只有同时满足以下两种条件就属于简单请求

  1. 请求方式是GET POST HEAD 中的一种
  2. 请求头不超过以下几种字段:
  • Accept
  • Accept-language
  • Content-language
  • Last-Event-ID
  • Content-Type:只限于以下三种字段 application/x-www-form-urlencoded multipart/form-data text/plain

凡是不满足以上两个条件的都属于非简单请求
浏览器对于两种请求方式的处理是不一样的

简单请求

基本流程

对于简单请求,浏览器直接发出CORS请求, 不需要客户端操作,具体来首就是在头信息增加一个Origin字段

  1. GET /cors HTTP/1.1
  2. Origin: http://api.bob.com
  3. Host: api.alice.com
  4. Accept-Language: en-US
  5. Connection: keep-alive
  6. User-Agent: Mozilla/5.0...

以上的信息中,Origin字段用来说明,本次请求来自哪个源,服务器根据这个值来决定是否同意这次请求

如果Origin指定的源头不在服务器的徐可可范围内, 服务器就会返回一个正常的response. 然后浏览器发现这个response头里没有Access-Control-Allow-Origin的字段就知道出错了,从而跑出一个错误, 这个错误就会被XMLHttpRequest的onerror函数捕获。

如果Origin 在服务器的许可范围内,服务器的response就会多出几个响应头

  1. Access-Control-Allow-Origin: http://api.bob.com
  2. Accesss-Control-Allow-Credentials:true
  3. Access-Control-Expose-Headers:FooBar
  4. Content-Type:text/html;charset=utf-8

上面的头信息之中有三个是CORS请求有关,都是以Access-Control开头
Access-Control-Allow-Origin
这个字段是必须的,他要么是请求的Origin字段,要么是一个 , 就便是就收任意域名的请求
Access-Control-Allow-Credentials
这个字段可选, 它的值是一个布尔值 ,表示是否允许发送cookie,
默认情况下, Cookies不包括在CORS中,设置为true,表示服务器明确许可Cookie可以包含在请求中一起发送给服务器,这个值只能设置为true,如果不发送Cookies,删除该字段即可

Access-Control-Expose-Headers
该字段是可选的
CORS请求时,XMLHttpRequest对象的getResponseHeader() 方法值能拿到6个字段,

  • Cache-Control
  • Content-language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面制定 多个值需要用逗号隔开

WithCredentials 属性

CORS 请求默认是不发送Cookies 和 HTTP认证信息, 如果要把Cookie发送到服务器,首先要服务器同意,制定Access-Control-Allow-Credentials字段为true
然后开发者必须在AJAX请求中打开withCredentials属性 ,否则浏览器也不会发送。后者服务器要求

  1. var xhr = new XMLHttpRequest();
  2. xhr.withCredentials = true;

**
否则 即使服务器同意发送Cookies,浏览器也不会发送, 或者服务器要求设置Cookie,浏览器也不会处理
但是,如果省略withCredentials设置, 有时候浏览器还是会一起发送Cookies. 这时 可以显示关闭withCredentials

xhr.withCredentials = false

需要注意事,如果要发送Cookies. Access-Control-Allow-Origin 就不能设置为*,必须制定明确的的origin,
同时Cookies依然遵循同源策略,只有服务器制定设置的域名才会上传,其他的域名的Cookie并不会上传,并且跨域源网页中英文无法使用document.cookie来读取服务器域名下的Cookie

非简单请求

预检请求

非简单请求是那种对于浏览器有特殊要求的请求。 比如说put delete 或者字段类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次http OPTIONS请求, 成为预检 preflight

浏览器先询问服务器,当前页面所载的域名是否在服务器许可的名单之中,以及可以使用那些请求方法和头信息字段。只有达到肯定答复,浏览器才会正式的发出ajax请求,否则报错
下面是浏览器的ajax脚本

  1. var url = 'http://api.alice.com/cors';
  2. var xhr = new XMLHttpRequest();
  3. xhr.open('PUT', url, true);
  4. xhr.setRequestHeader('X-Custom-Header', 'value');
  5. xhr.send();

上面的代码中 http请求的方法是put 并且发送个自定义的头字段 X-Custom-Header

浏览器发现 这是一个非简单请求 就自动发出一个preflight, 要求服务器确认可以这样发出请求,下面是预请求的头

  1. OPTIONS /cors HTTP/1.1
  2. Origin: http://api.bob.com
  3. Access-Control-Request-Method: PUT
  4. Access-Control-Request-Headers: X-Custom-Header
  5. Host: api.alice.com
  6. Accept-Language: en-US
  7. Connection: keep-alive
  8. User-Agent: Mozilla/5.0...

预检的请求方法是OPTIONS, 表示这个请求时用来询问的, 头信息里面,关键字段是Origin,表示请求来自哪个源

除了Origin字段, 预检请求的头信息里面包括两个特殊的字段
Access-COntrol-Request-Method
该字段是必须的 用来列出浏览器的CORS方法会用到哪些http方法
Access-Control-Request-Headers
该字段是一个逗号的分割的字符串,制定浏览器的CORS请求会额外发送的头字段,上面的例子总是X-Custom-header

预检请求的回应

服务器收到预检请求以后,检查了Origin,Access-Control-Request-Method,Access-Control-Request-Headers等字段以后,确认允许跨域请求,就可以做出回应

  1. HTTP/1.1 200 OK
  2. Date: Mon, 01 Dec 2008 01:15:39 GMT
  3. Server: Apache/2.0.61 (Unix)
  4. Access-Control-Allow-Origin: http://api.bob.com
  5. Access-Control-Allow-Methods: GET, POST, PUT
  6. Access-Control-Allow-Headers: X-Custom-Header
  7. Content-Type: text/html; charset=utf-8
  8. Content-Encoding: gzip
  9. Content-Length: 0
  10. Keep-Alive: timeout=2, max=100
  11. Connection: Keep-Alive
  12. Content-Type: text/plain

我们可以看到在回应的头字段中,关键的事Access-Control-Allow-Origin, 他表示允许http://api.bob.com这个请求数据了。该字段设置为*, 表示同意任意跨域请求

如果浏览器否定预检请求,会发挥一个正常的http请求,但是没有任务CORS相关的头字段, 这时浏览器就会认定服务器不同意预检请求,因此会触发一个错误信息,被XMLHttpRequest对象的onError函数捕获,控制台就会打印如下信息
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

如果浏览器同意了预检请求, 那么response的头信息除了Access-Control-Allow-Origin就会包括以下字段
Access-Control-Allow-Methods
该字段表示允许浏览器使用的请求方法,包括并且不限于预检请求中的方法
Access-Control-Allow-Headers
这个字段表示服务器所支持的请求头信息, 包括并不限于预检请求中的Access-Control-Request-Headers中的字段
Access-Control-Allow-Credentials
该字段预简单请求时的含义相同
Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位是秒,在此期间不用发送另一条预检请求

浏览器的正常请求和回应

一旦预检请求通过, 浏览器就会发送正常的CORS请求, 就是跟简单的请求一样,会有一个Origin头字段,服务器的回一个也会有个Access-Control-Allow-Origin头字段信息

下面的预检请求之后,浏览器发出的正常的CORS请求

  1. PUT /cors HTTP/1.1
  2. Origin: http://api.bob.com
  3. Host: api.alice.com
  4. X-Custom-Header: value
  5. Accept-Language: en-US
  6. Connection: keep-alive
  7. User-Agent: Mozilla/5.0...

头信息的Origin字段是浏览器自动添加的
下面是服务器的正常回应
Access-Control-Allow-Origin: [http://api.bob.com](http://api.bob.com)
Content-Type: text/html; charset=utf-8

上面的头信息中,Access-Control-Allow-Origin 字段是每次回应都必须包含的

下面是代码

  1. let xhr = new XMLHttpRequest();
  2. document.cookie = 'name=aa';
  3. xhr.withCredentials = true;
  4. xhr.open('put', 'http://localhost:4000/getData');
  5. xhr.setRequestHeader('name', 'aa');
  6. xhr.onreadystatechange = function () {
  7. if (xhr.readyState === 4) {
  8. if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
  9. console.log(xhr.responseText);
  10. console.log(xhr.getResponseHeader('name'));
  11. }
  12. }
  13. }
  14. xhr.send();

服务端代码

  1. let express = require('express');
  2. let app = express();
  3. // 建立可以访问白名单
  4. let whiteList = ['http://localhost:3000'];
  5. app.use(function (req, res, next) {
  6. let origin = req.headers.origin;
  7. if (whiteList.includes(origin)) {
  8. //=> 允许哪个源可以访问,如果是 '*' 号,那么就所有都可以访问,但是不能与携带 cookie 一起使用
  9. res.setHeader('Access-Control-Allow-Origin', origin);
  10. //=> 允许携带哪个头访问我,多个则在字符串拼接即 'name, age'
  11. res.setHeader('Access-Control-Allow-Headers', 'name');
  12. //=> 允许哪个方法访问,get 方法不设置也可以
  13. res.setHeader('Access-Control-Allow-Methods', 'PUT');
  14. //=> 预检的存活时间,设置 options 探测请求的间隔时间,减少 options 请求的发送
  15. res.setHeader('Access-Control-Max-Age', 6);
  16. //=> 允许携带 cookies
  17. res.setHeader('Access-Control-Allow-Credentials', true);
  18. //=> 设置哪个头信息是安全的,客户端可以获取的
  19. res.setHeader('Access-Control-Expose-Headers', 'name');
  20. if (req.method === 'OPTIONS') {
  21. res.end(); //=> 会有一个试探的 options 请求,直接结束即可
  22. }
  23. }
  24. next();
  25. })
  26. app.get('/getData', function (req, res) {
  27. console.log(req.headers);
  28. res.end("中文");
  29. });
  30. app.put('/getData', function (req, res) {
  31. console.log(req.headers);
  32. res.setHeader('name','aa');
  33. res.end("中文");
  34. });
  35. //=> 以当前目录作为静态文件目录
  36. app.use(express.static(__dirname));
  37. app.listen(4000);

和JSONP比较的话

  • JSONP的兼容性比较好,不仅兼容所有的浏览器,而且还可以像不支持CORS的服务发起请求,但是只能发送GET请求
  • CORS支持所有类型的请求,但是兼容性不太好 IE10以下的不支持

postMessage

a 页面可以通过iframe 和 postMessage 向b 页面发送消息,其中a,b不在同一个域下

  1. <iframe src="http://localhost:4000/b.html"
  2. frameborder="0"
  3. id="frame"
  4. onload="load()">
  5. </iframe>
  6. <script>
  7. function load() {
  8. let frame = document.getElementById('frame');
  9. //=> 接受两个参数,数据以及传递的地址
  10. frame.contentWindow.postMessage('aa', 'http://localhost:4000/b.html');
  11. }
  12. window.onmessage = function (e) {
  13. console.log(e.data);
  14. }
  15. </script>

当iframe加载完毕以后向b也买呢发送一个消息,此时需要通过一个frame.contentWindow 获取b页面,使用启postMessage方法

  1. <script>
  2. window.onmessage = function (e) {
  3. console.log(e.data);
  4. e.source.postMessage('bb', e.origin);
  5. }
  6. </script>

b页面通过window.onmessage 监听到这个发送过来的数据,通过e.data获取数据
然后b页面获取到数据之后,通过e.source获取a页面,通过其postMessage方法向a页面发送消息
a页面中也是通过window.onmessage来监听发过来的数据 通过e.data拿到数据
另外比较奇怪的是 我测试中接受到两次b页面发过来的数据,第二次为空

document.domain

用于解决一级域名和二级域名之间的跨域问题,只需要连个也买呢的document.domain都设置为定级域名即可
思路:

  1. 假设a页面是http://a.baidu.com:3000 b页面是http://b.baidu.com:3000
  2. a页面没办法直接通过 iframe.contentWindow.a 获取b页面window中的a属性
  3. 两个也买呢都设置document.domain 为http://baidu.com即可
    1. <iframe src="http://b.destiny.cn:3000/b.html"
    2. frameborder="0"
    3. onload="load()"
    4. id="iframe">
    5. </iframe>
    6. <script>
    7. document.domain = 'destiny.cn';
    8. function load() {
    9. console.log(iframe.contentWindow.a);
    10. }
    11. </script>
    1. <script>
    2. document.domain = 'destiny.cn';
    3. var a = 100;
    4. </script>
    记住定义 a 的时候要使用var 不要使用let 因为let不会挂在到window上
    如果要模拟一级域名 二级域名可以设置host
    127.0.0.1 b.baidu.com
    127.0.0.1 a.baiud.com