一篇文章讲清楚 CORS
2024-08-07
CORS 是 Cross-origin resource sharing 的简称,中文名叫:跨域资源共享。
这是一个在 Web 安全领域很重要的模型。
它解决的问题是,如何在不同域下安全地共享资源。
1. 背景
前端的编写十分灵活,这得益于 JavaScript 语言表达的灵活性。但是,正所谓能力越大,责任越大,这种灵活性也会给 Web 安全带来诸多挑战。默认情况下,网页主动发起的 HTTP 请求(链接跳转不算),例如 XMLHttpRequest
和 Fetch API
,只支持 同源策略(Same Origin Policy)🔗,即只能访问当前域下的资源。
http://example-a.com/
访问 http://example-b.com/
下的资源就是跨域,请求会失败。
这在一定程度上缓解了恶意代码执行的风险。
同样也会带来一些问题:如果确实有些资源在另一个域下,怎么获取?
CORS 就是用来解决这个问题的。
2. 功能
当一个域下的网页想要访问另一个域下资源时,如果对方服务端响应的报头(HTTP header)包含正确的 CORS 信息,浏览器便不会拦截,跨域资源顺利获取。
简单来说,CORS 是一组协议,允许服务端指定哪些主机可以访问本地资源。
这便是 CORS 的全部内容。
CORS 是 HTTP 标准的一部分,因此得到了现代浏览器的广泛支持。

老的浏览器可以用 JSONP🔗 的方式实现跨域访问。这是一个很有意思的 workaround,借助浏览器本身提供的特性“绕”过了同源限制。但这不是一个规范实现,容易引发安全问题,因此在现代浏览器上,最好使用 CORS。
2.1 CORS 标头
HTTP 标头中有一系列 CORS 相关的。
Access-Control-Allow-Origin指示响应的资源是否可以被给定的来源共享。
Access-Control-Allow-Credentials指示当请求的凭证标记为 true 时,是否可以公开对该请求响应。
Access-Control-Allow-Headers用在对预检请求的响应中,指示实际的请求中可以使用哪些 HTTP 标头。
Access-Control-Allow-Methods指定对预检请求的响应中,哪些 HTTP 方法允许访问请求的资源。
Access-Control-Expose-Headers通过列出标头的名称,指示哪些标头可以作为响应的一部分公开。
Access-Control-Max-Age指示预检请求的结果能被缓存多久。
Access-Control-Request-Headers用于发起一个预检请求,告知服务器正式请求会使用哪些 HTTP 标头。
Access-Control-Request-Method用于发起一个预检请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。
Origin指示获取资源的请求是从什么源发起的。
Timing-Allow-Origin指定特定的源,以允许其访问 Resource Timing API 功能提供的属性值,否则由于跨源限制,这些值将被报告为零。
参考:跨源资源共享 - MDN Web 文档术语表:Web 相关术语的定义 | MDN🔗。
当检测到跨域请求时,浏览器会默认添加相应的请求头,响应头则需要开发者在服务端设置。
2.2 工作流程
主要的阶段分为三个:
- 浏览器发起跨域请求
- 服务端响应
- 浏览器处理响应
对于跨域访问,浏览器会填充 Origin
头,例如:
Origin: https://plantree.me
服务端收到请求,就会加上 Access-Control-Allow-Origin
指定允许的源,或者 *
允许所有访问。
浏览器拿到响应,判断 Access-Control-Allow-Origin
里的字段是否匹配,匹配即可拿到结果,否则报错。
2.3 场景
使用的场景主要有三个:
- 简单请求
- 预检请求(preflight)
- 附带身份凭证
简单请求通常需要满足两个条件。
- 使用的方法是下列之一
- GET
- HEAD
- POST
- 允许设置的 HTTP 头字段
- Accept
- Accept-Language
- Content-Language
- Content-Type(text/plain, multipart/form-data, application/x-www-form-urlencoded)
- Range(只允许简单的范围标头值 如 bytes=256- 或 bytes=127-255)
一次简单请求:
const xhr = new XMLHttpRequest();const url = "https://bar.other/resources/public-data/";
xhr.open("GET", url);xhr.onreadystatechange = someHandler;xhr.send();
请求报文:
GET /resources/public-data/ HTTP/1.1Host: bar.otherUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-us,en;q=0.5Accept-Encoding: gzip,deflateConnection: keep-aliveOrigin: https://foo.example
服务器响应:
HTTP/1.1 200 OKDate: Mon, 01 Dec 2008 00:23:53 GMTServer: Apache/2Access-Control-Allow-Origin: *Keep-Alive: timeout=2, max=100Connection: Keep-AliveTransfer-Encoding: chunkedContent-Type: application/xml
因为 Access-Control-Allow-Origin: *
允许所有跨域访问,于是这次请求会成功响应。
与简单请求不同,预检请求需要两次访问。这些工作是浏览器自动做的。
第一次是先发送 OPTIONS
请求到服务端,检查是否允许,允许的话才会继续发送真实请求。
预检请求的例子:
const xhr = new XMLHttpRequest();xhr.open("POST", "https://bar.other/resources/post-here/");xhr.setRequestHeader("X-PINGOTHER", "pingpong");xhr.setRequestHeader("Content-Type", "application/xml");xhr.onreadystatechange = handler;xhr.send("<person><name>Arun</name></person>");
因为请求的 Content-Type
是 application/xml
,所以会触发预检。
首次交互结果:
OPTIONS /doc HTTP/1.1Host: bar.otherUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-us,en;q=0.5Accept-Encoding: gzip,deflateConnection: keep-aliveOrigin: https://foo.exampleAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 204 No ContentDate: Mon, 01 Dec 2008 01:15:39 GMTServer: Apache/2Access-Control-Allow-Origin: https://foo.exampleAccess-Control-Allow-Methods: POST, GET, OPTIONSAccess-Control-Allow-Headers: X-PINGOTHER, Content-TypeAccess-Control-Max-Age: 86400Vary: Accept-Encoding, OriginKeep-Alive: timeout=2, max=100Connection: Keep-Alive
允许访问后发送实际请求:
POST /doc HTTP/1.1Host: bar.otherUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-us,en;q=0.5Accept-Encoding: gzip,deflateConnection: keep-aliveX-PINGOTHER: pingpongContent-Type: text/xml; charset=UTF-8Referer: https://foo.example/examples/preflightInvocation.htmlContent-Length: 55Origin: https://foo.examplePragma: no-cacheCache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OKDate: Mon, 01 Dec 2008 01:15:40 GMTServer: Apache/2Access-Control-Allow-Origin: https://foo.exampleVary: Accept-Encoding, OriginContent-Encoding: gzipContent-Length: 235Keep-Alive: timeout=2, max=99Connection: Keep-AliveContent-Type: text/plain
预检请求之所以会有两次,是因为这里的请求可能改变服务端状态,因此需要一次试探性地询问。有点像是 两阶段提交🔗。
默认的跨域访问,不会携带身份信息,不过允许设置,指定 withCredentials
为 true
,请求时会附带 Cookies
。
const invocation = new XMLHttpRequest();const url = "https://bar.other/resources/credentialed-content/";
function callOtherDomain() { if (invocation) { invocation.open("GET", url, true); invocation.withCredentials = true; invocation.onreadystatechange = handler; invocation.send(); }}
交互过程如下:
GET /resources/credentialed-content/ HTTP/1.1Host: bar.otherUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-us,en;q=0.5Accept-Encoding: gzip,deflateConnection: keep-aliveReferer: https://foo.example/examples/credential.htmlOrigin: https://foo.exampleCookie: pageAccess=2
HTTP/1.1 200 OKDate: Mon, 01 Dec 2008 01:34:52 GMTServer: Apache/2Access-Control-Allow-Origin: https://foo.exampleAccess-Control-Allow-Credentials: trueCache-Control: no-cachePragma: no-cacheSet-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMTVary: Accept-Encoding, OriginContent-Encoding: gzipContent-Length: 106Keep-Alive: timeout=2, max=100Connection: Keep-AliveContent-Type: text/plain
返回结果中如果缺少 Access-Control-Allow-Credentials: true
,响应会失败。
3. 实际使用
在前端开发中,多数情况都是使用本域下的资源,安全,可控。
但不排除有时需要访问第三方资源,此时就需要格外注意 CORS 可能带来的问题。
对数据保护严格的网站,会设置苛刻的 CORS 策略,从而导致请求失败。
这个在 Console 窗口里很容易观察到。
尽可能少的使用外部资源,出现问题了能轻松应对,了解到这个层面,差不多就够了。
4. 总结
在计算机领域里,权衡(trade-off)无处不在。
允许访问资源,但需要满足条件,提高了安全性的同时,也不可避免地降低了开发效率。
任务解决方案都是一体两面,或许这就是技术世界的辩证。
(完)
参考
- 本文作者:Plantree
- 本文链接:https://plantree.me/blog/2024/cors-intruction/
- 版权声明:所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
最后更新于: 2024-11-20T09:44:17+08:00