CORS 详解

跨域资源共享

Posted by John Mactavish on March 13, 2022

同源策略(Same-origin Policy)

同源策略是现代浏览器都具备的安全措施,它不允许从一个源(Origin)加载的网页脚本访问另一个源。 这里的同源(Same-origin)指的是协议、域名与端口都相同。

下表列出哪些 URL 与 URL http://www.example.com/dir/page.html 同源:

URL 结果 原因
http://www.example.com/dir2/other.html 只有路径不同
http://www.example.com:8080/dir/page.html 不同端口(若未标明,http 默认端口号为 80)
https://www.example.com/dir/page.html 不同协议(https 和 http)
http://en.example.com/dir/page.html 不同域名
http://example.com/dir/page.html 不同域名(需要完全匹配)
http://v2.www.example.com/dir/page.html 不同域名(需要完全匹配)

值得注意的是同源策略仅适用于脚本,这意味着网站可以通过 HTML 标签访问不同来源网站上的图像、CSS 和动态加载脚本等资源。 而跨站请求伪造(Cross-site request forgery,缩写为 CSRF 或 XSRF), 就是利用了同源策略不适用于 HTML 标签的缺陷。 假如一家银行用以执行转账操作的 URL 地址为:https://bank.example.com/withdraw?account=AccoutName&amount=1000&for=PayeeName, 那么,一个恶意攻击者可以在另一个网站上放置代码:<img src="https://bank.example.com/withdraw?account=Alice&amount=1000&for=Badman"/>, 如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 元。

跨域资源共享(Cross-origin resource sharing,缩写为 CORS)

同源策略的限制过于严格了:一家大公司常常拥有一系列域,并且希望能在这些域之间交换数据。 为了绕过该限制,业界提出了一系列解决该问题的方法,例如更改 document.domain 属性,跨文档消息,JSONP 以及 CORS 等。 这里仅仅介绍常用的 CORS。

简单请求(simple request)和非简单请求(not-so-simple request)

浏览器将 CORS 请求分成两类:简单请求和非简单请求。

一个请求只要同时满足以下两大条件,就属于简单请求:

  • 请求方法是以下三种方法之一:
    • HEAD
    • GET
    • POST
  • HTTP 的 Header 不超出以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain

否则属于非简单请求。

简单请求的流程

GET /users/34 HTTP/1.1
Origin: http://api.example.com

对于简单请求,浏览器直接发出 CORS 请求,但在 HTTP Header 中增加一个 Origin 字段。 Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口),服务器据此决定是否同意这次请求。 浏览器如果发现,Response 的 Header 没有包含 Access-Control-Allow-Origin 字段,就知道出错了。 而如果 Origin 指定的源在许可范围内,服务器返回的响应,会多出几个 Header 字段,都以 Access-Control 开头。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://api.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Content-Encoding, X-Kuma-Revision
  1. Access-Control-Allow-Origin 字段是必须的。 它的值要么是请求时 Origin 字段的值,要么是一个 *(通配符),表示接受任意源的请求;
  2. Access-Control-Allow-Credentials 字段是可选的。这个字段的值只能设为 true,表示允许发送 Cookie。 默认情况下,Cookie 不包括在 CORS 请求之中,即该字段不存在;
  3. Access-Control-Expose-Headers 字段也是可选的。它指示浏览器可以将 Response 中其他哪些字段暴露给脚本, 未提到的字段将被限制读取。

非简单请求的流程

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为预检请求(preflight)。 浏览器先询问服务器,当前网页所在的源是否在许可名单中,以及可以使用哪些 HTTP Method 和 Header 字段。 只有得到肯定答复后,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

OPTIONS /users/34 HTTP/1.1
Origin: http://api.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header

除了 Origin 字段,预检请求的 Header 还包括两个特殊字段。

  1. Access-Control-Request-Method 字段是必须的,用来列出 CORS 请求会用到哪些 HTTP Method;
  2. Access-Control-Request-Headers 字段是一个逗号分隔的字符串,列出 CORS 请求会额外发送的 Header 字段。

服务器收到预检请求后,如果允许 CORS 请求,就可以做出回应:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://api.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header

除了 Access-Control-Allow-Origin 字段,预检的 Response 的 Header 还包括两个特殊字段。

  1. Access-Control-Allow-Methods 字段必需,它的值是逗号分隔的一个字符串,列出服务器支持的所有跨域请求的方法。 注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法,这是为了避免多次预检;
  2. Access-Control-Allow-Headers 在浏览器请求包括 Access-Control-Request-Headers 字段时是必需的。 它也是一个逗号分隔的字符串,列出服务器支持的所有 Header 字段,也不限于浏览器在预检中请求的字段

而如果服务器否定了预检请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的 Header 字段。 这时,浏览器就会认定,服务器不同意预检请求。

一旦服务器通过了预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 字段。 服务器的 Response,也都会有一个 Access-Control-Allow-Origin 字段。


参考资料:

阮一峰的“跨域资源共享 CORS 详解”

同源策略的 Wiki

CORS 的 Wiki

如果你喜欢我的文章,请我吃根冰棒吧 (o゜▽゜)o ☆

contribution

最后附上 GitHub:https://github.com/gonearewe