再谈跨域资源共享 CORS
之前有总结到前端跨域请求,其中一个解决方案是利用CORS
,因此有必要详细了解一下CORS
。CORS
即 Cross-origin resource sharing
,跨域资源共享 ,是由 W3C 官方推广的允许通过 AJAX 技术跨域获取资源的规范 。
CORS简介
跨源资源共享 (CORS
) (或通俗地译为跨域资源共享)是一种基于HTTP
头的机制,该机制通过允许服务器标示除了它自己以外的其它origin
(域,协议和端口),这样浏览器可以访问加载这些资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有HTTP方法和真实请求中会用到的头
。
跨源HTTP请求的一个例子:运行在 http://www.devpoint.cn
的JavaScript代码使用XMLHttpRequest来发起一个到 https://www.devpoint.cn/data.json
的请求。
出于安全性,浏览器限制脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest
和Fetch API
遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。
跨源域资源共享( CORS )机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch )使用 CORS,以降低跨源 HTTP 请求所带来的风险。
什么情况下需要 CORS ?
这份 cross-origin sharing standard 允许在下列场景中使用跨站点 HTTP 请求:
- 前文提到的由 XMLHttpRequest 或 Fetch 发起的跨源 HTTP 请求。
- Web 字体 (CSS 中通过 @font-face 使用跨源字体资源),因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。
- WebGL 贴图
- 使用 drawImage 将 Images/video 画面绘制到 canvas
怎么使用CORS?
CORS
的使用的关键在服务端,浏览器发送请求,服务端接收到客户端请求做一些判断(请求方是否在自己的“白名单”里?),如果没问题就返回数据,否则拒绝。
浏览器将 CORS 请求分成两类:
- 简单请求(
simple request
) - 非简单请求(
not-so-simple request
)
简单请求(simple request
)
只要同时满足以下两大条件,就是简单请求:
-
1、请求方法是以下三种方法之一:
HEAD
GET
POST
-
2、HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
下面来看看这两种请求,CORS 是怎么处理?
基本流程
对于简单请求,浏览器直接发出CORS请求,在头信息之中,增加一个Origin
字段。
下面是来看一个例子,浏览器判断这次跨源AJAX
请求是简单请求,就会自动在头信息之中,添加一个Origin
字段。
GET /cors HTTP/1.1
Origin: http://api.doweb.me
Host: api.doweb.me
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
接下来服务端收到浏览器请求,首先检测请求报头的 Origin
是否在自己的许可范围内,
如果确实是许可的域,会响应的时候,在响应头额外增加如下字段:
Access-Control-Allow-Origin
(必选) :这个字段用来告知浏览器,服务端能够接受的发送 AJAX 请求的域,因为此次请求得到许可,所以这里返回与先前请求报头中Origin
匹配的http://api.doweb.me
。当然,也可以返回 ,表示接受任何域的 AJAX 请求( 是通配的意思)。Access-Control-Allow-Credentials
(可选):告知浏览器,是否允许浏览器发送请求的时候携带Cookie
,true
表示允许,false
表示禁止,出于安全问题考虑,CORS
默认不允许跨域 AJAX 请求携带Cookie
。Access-Control-Expose-Headers
(可选):该字段为可选字段。CORS
请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。
如果不是许可的域,这时候不会返回 Access-Control-Allow-Origin
这个响应头,而浏览器会捕获这次错误,如下图所示
虽然禁止跨域
AJAX
请求携带Cookie
是为了安全考虑,但由于它在身份验证中的重要性,我们有时候还是得携带Cookie
的。 具体方法是:
如果需要安全携带Cookie,需要另外一个属性:withCredentials
。
CORS
请求默认不发送Cookie
和HTTP
认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials
字段。
-
客户端配置
withCredentials
属性:var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
-
服务端配置
Access-Control-Allow-Credential
为 true,配置Access-Control-Allow-Origin
为指定的域(而不是 * ),Access-Control-Allow-Credentials: true
如果省略withCredentials
设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials
。
xhr.withCredentials = false;
另外需要注意的是,如果要发送Cookie
,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie
依然遵循同源政策,只有用服务器域名设置的Cookie
才会上传,其他域名的Cookie
并不会上传,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的Cookie
。
非简单请求(not-so-simple request
)
非简单请求包括两次请求,第一次请求是 preflight request
,也就是预检/查询
请求,这次请求试探性地“询问”服务端,自己打算进行的非简单请求是否合法 —— 不管是否合法,服务端都会通过某种方式通知客户端,客户端基于这个结果,判断是否进行第二次真正的请求。
预检请求
首先是客户端的角度,发送请求时浏览器检测到这是一个非简单请求,所以事先向服务端发送一个预检请求:
OPTIONS /cors HTTP/1.1
Origin: https://www.doweb.me
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Custom-Header1,Custom-Header2
Host: target.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
- 注意,这里这个预检请求的类型是
OPTIONS
。 - 像之前的简单请求一样,这里浏览器会追加一个
Origin
,表示请求代码所在的源 - 前面我们说过,非简单请求会多出额外的请求头字段,这里多出来的就是
Access-Control-Request-Method
和Access-Control-Request-Headers
,这其实是告诉服务端,“我待会要进行的真正请求,类型是这里Access-Control-Request-Headers
指定的类型,然后自定义请求头是这里Access-Control-Request-Headers
指定的值,你看看行不行,给我个回应“。
预检请求的回应
服务器收到"预检"请求以后,检查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出回应。
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2020 01:15:39 GMT
Server: Apache/2.0.61(Unix)
Access-Control-Allow-Origin: https://www.doweb.me
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Custom-Header1,Custom-Header2
Access-Control-Max-Age: 1728000
Content-type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
Access-Control-Allow-Origin
:这里和之前一样,可以是https://www.doweb.me
或者*
,也就是告诉客户端,“我给你的域下了许可证“Access-Control-Allow-Methods
:这里告诉客户端,服务端允许的跨域AJAX
请求的类型,”虽然你刚才告诉我你准备进行的是PUT
请求,不过你要进行GET
或者POST
请求,我也是允许的“Access-Control-Allow-Headers
:如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。Access-Control-Max-Age
: 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器的正常请求和回应
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin
头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin
头信息字段。
下面是"预检"请求之后,浏览器的正常CORS请求。
PUT /cors HTTP/1.1
Origin: http://api.doweb.me
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面头信息的Origin
字段是浏览器自动添加的。
下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.doweb.me
Content-Type: text/html; charset=utf-8
上面头信息中,Access-Control-Allow-Origin
字段是每次回应都必定包含的。
和JSONP的差异?
CORS
与JSONP
的使用目的相同,但是比JSONP
更强大。
JSONP
只支持GET
请求,CORS
支持所有类型的HTTP
请求。JSONP
的优势在于支持老式浏览器,以及可以向不支持CORS
的网站请求数据。