/ HTTP  

同源策略(CORS)探究

一、有哪些场景

  • XMLHttpRequest 或者 Fetch 发起跨域 HTTP 请求
  • Web FONT (CSS @font-face)
  • WebGL 贴图
  • canvas API 中的 drawImage
  • CSS 中的资源请求

二、简单的 CORS 实现

在跨域 AJAX 请求中, 浏览器端无需做任何操作,会自动在 header 信息中添加一个 Orgin 字段;
如果是 Fetch ,则需要添加字段 mode: 'cors'

1
2
3
4
5
6
7
8
9
10
11
GET http://square.10jqka.com.cn/usercenter/strategy/getrecommendlist HTTP/1.1
Host: square.10jqka.com.cn
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: */*
Origin: http://www.10jqka.com.cn
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36
Referer: http://www.10jqka.com.cn/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

这是我截的 Request Headers,第 7 行的 Origin 是 XMLHttpRequest 自动加在请求头中的 ,表示请求来自哪个站点,与 Referer 首部字段相似。

 

而对应的,服务端需要在响应头中加入字段 Access-Control-Allow-Origin, 表示该资源可以允许哪些外域访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HTTP/1.1 200 OK
Server: openresty
Date: Fri, 08 Mar 2019 05:34:03 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 48
Vary: Origin
Access-Control-Allow-Origin: *
Accept-Ranges: bytes
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
x-download-options: noopen
x-readtime: 1
VH: 210_254
X-Cache: MISS from cachemd.10jqka.com.cn
Via: 1.1 cachemd.10jqka.com.cn (squid/3.5.20)
Connection: keep-alive

如上面例子的第 7 行, Access-Control-Allow-Origin: * 表示允许任何外域访问该资源。

如果请求响应头没有包含 Access-Control-Allow-Origin ,则 XMLHttpRequest 会捕获错误,走入 onerror 回调中。

至此,一个最简单的 CORS 就可以了。

CORS 请求默认是不发送 Cookie 和 HTTP 认证信息的,如果需要携带 cookie, 浏览器端和服务端都需要再做一些操作

3.1 浏览器

浏览器端 XHR 需要打开 withCredentials 参数,Fetch 则需要设置 credentials 参数,如下

1
2
3
// Ajax
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
1
2
3
4
5
6
// Fetch
fetch(url,
mode: 'cors',
credentials: 'include'
})
.then(response => response.json()) // parses response to JSON}

这里需要注意下,有的内核实现的 XMLHttpRequest 是默认开启 withCredentials 的。

3.2 服务端

  1. 加上 Access-Control-Allow-Credentials: true, 表示服务器允许认证信息携带

  2. Access-Control-Allow-Origin 不可以是星号(*),必须指明具体的域名

1
2
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://mozilla.com

四、预检请求 Preflight Request

  • POST 请求变成 OPTIONS 了怎么办?
  • 给跨域请求写了自定义的 header 就报错了怎么回事?

也许某天你要经历或经历过这样的问题,那是因为你触发了 Preflight request,至于具体触发预检的情况,往下翻。

4.1 解决方案

服务端需要再在 header 中加入一些字段,响应 OPTIONS 请求来表明是否预检通过。

1
2
3
4
5
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://mozilla.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

(1) Access-Control-Allow-Methods

该字段为必需,表示在 preflight request 中所要访问资源允许使用的方法或方法列表

(2) Access-Control-Allow-Headers

如果在浏览器端发起 XMLHttpRequest 时写了非规范的 header 字段, 那么 Access-Control-Allow-Headers 是必须的。

表示在 preflight request 中所要访问资源允许携带的 header 字段

(3) Access-Control-Max-Age

可选这段,指定了 preflight request 的结果可以被缓存多久

这个字段从一些第三方资料看,可能存在一些bug,某些浏览器的旧版本可能有上限值;另外禁用 cache 的情况下也会无效。

4.2 触发条件

当满足下面任意一项条件,就会先发出预检请求

  1. 使用了下面任一 HTTP 方法:

    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  2. 请求头中有安全首部字段集合之外的字段

    • Accept
    • Content-Type
    • 等…

具体查官方文档,各浏览器可能有一定差异

  1. Content-Type 的值不是下述之一

    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  2. 请求中的 XMLHttpRequestUpload 对象注册了任意多个事件监听器

  3. 请求中使用了 ReadableStream 对象

五、为什么设计预检请求

Why is there a preflight request?
 
For most type of requests two resource sharing checks are performed. Initially a “permission to make the request” check is done on the response to the preflight request. And then a “permission to read” check is done on the response to the actual request. Both of these checks need to succeed in order for success to be relayed to the API (e.g. XMLHttpRequest). The “permission to make the request” check is performed because deployed servers do not expect such cross-origin requests. E.g., a request using the HTTP DELETE method. If they reply positively to the preflight request the client knows it can go ahead and perform the actual desired request.

摘自wiki-CORS,大致意思是一是认为对于大多数请求进行两次请求是可行的,二是进行预检校验是很多场景安全性上的考虑。

 

或者可以这么说,预检请求本身其实并没有做什么安全性上的检查,只是这项新的特性必须不影响各个项目,项目方必须知道有这样一项检查并针对它做些设置。

六、与 JSONP 比较?

  1. JSONP 不存在兼容问题,而 CORS 你必须确认你需要兼容的设备,必要情况需要做降级处理
  2. JSONP 只支持 GET 请求, CORS 则能支持所有 HTTP 请求类型
  3. JSONP 单次请求, CORS 触发预检则会有两次请求

七、附录