1 背景
出于安全考虑,浏览器限制在脚本内(如 XMLHttpRequest 和 Fetch)进行跨域访问。
CORS(跨域资源共享)是一种可以让浏览器进行跨域访问的机制。这个机制的本质是,当请求和响应的头部分别配置了特定的 HTTP 头部时,浏览器将判定此跨域请求是安全的,进而允许进行跨域访问。
2 使用 CORS 的场景
- 使用 XMLHttpRequest 或 Fetch 发起的跨域请求
- Web 字体(CSS 通过 @font-face 使用跨域字体资源)
- WebGL 贴图
- 使用 drawImage 将 Image/video 画面绘制到 canvas
3 CORS 的使用方式
不同的请求类型,会使用不同的方式使用 CORS。
3.1 简单请求
3.1.1 定义
满足下面所有条件时,被定义为 简单请求,该请求不会触发 CORS 预检请求:
- 使用下面方法之一:
- 请求头不超过如下集合:
Accept
Accept-Language
Content-Language
Content-Type
(需要注意额外的限制)
DPR
Downlink
Save-Data
Viewport-Width
Width
- Content-Type 为下面三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
4、请求中的任意XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用 XMLHttpRequest.upload
属性访问。
5、请求中没有使用 ReadableStream
对象。
3.1.2 代码示例
站点 http://foo.example 的网页应用访问 http://bar.other 的资源 js 代码如下:
1 2 3 4 5 6 7 8 9 10
| var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/public-data/'; function callOtherDomain() { if(invocation) { invocation.open('GET', url, true); invocation.onreadystatechange = handler; invocation.send(); } }
|
浏览器发送的 HTTP 请求如下,第10行 的请求首部字段 Origin
表明该请求来源于 http://foo.example
。:
1 2 3 4 5 6 7 8 9 10
| GET /resources/public-data/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Referer: http://foo.example/examples/access-control/simpleXSInvocation.html Origin: http://foo.example
|
服务器返回的 HTTP 请求如下,服务端返回的 Access-Control-Allow-Origin: *
表明,该资源可以被任意外域访问。
1 2 3 4 5 6 7 8
| HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 00:23:53 GMT Server: Apache/2.0.61 Access-Control-Allow-Origin: * Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: application/xml
|
3.1.3、总结
简单请求使用 Origin
和 Access-Control-Allow-Origin
完成 CORS 的访问控制。
Access-Control-Allow-Origin
的值应当为 * 或者包含 Origin 首部字段所指明的域名,来使得此跨域请求成功返回。
3.2 需预检请求
3.2.1 定义
需预检请求 指浏览器在发送实际请求前,先发送一个 OPTION 请求,当服务器响应为允许时,才发送的实际请求。当条件不满足 简单请求 时,为 需预检请求。
3.2.2 代码示例
下面的代码使用 POST 请求包含了一个自定义的请求首部字段(X-PINGOTHER: pingpong),并且该请求的 Content-Type 为 application/xml。因此,该请求需要首先发起“预检请求”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/post-here/'; var body = '<?xml version="1.0"?><person><name>Arun</name></person>'; function callOtherDomain(){ if(invocation) { invocation.open('POST', url, true); invocation.setRequestHeader('X-PINGOTHER', 'pingpong'); invocation.setRequestHeader('Content-Type', 'application/xml'); invocation.onreadystatechange = handler; invocation.send(body); } }
|
浏览器发起预检请求(OPTIONS 方法):
1 2 3 4 5 6 7 8 9 10 11
| OPTIONS /resources/post-here/ HTTP/1.1----> 1、预检请求的方法 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Origin: http://foo.example Access-Control-Request-Method: POST--->2、告知服务器实际请求的方法为 POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type--->3、告知服务器实际请求包含这两个自定义头部
|
服务器返回对预检请求的响应:
1 2 3 4 5 6 7 8 9 10 11 12 13
| HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://foo.example-->1、告知浏览器跨域请求允许的源客户端 Access-Control-Allow-Methods: POST, GET, OPTIONS->2、告知浏览器跨域请求允许的方法 Access-Control-Allow-Headers: X-PINGOTHER, Content-Type-->3、告知浏览器跨域齐全允许的自定义头部 Access-Control-Max-Age: 86400-->告知浏览器该跨域请求被允许的有效时间,在此有效时间内,浏览器不会对此请求再次发起预检请求 Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
|
得到服务器允许后,浏览器才会发送下面实际请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| POST /resources/post-here/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive X-PINGOTHER: pingpong Content-Type: text/xml; charset=UTF-8 Referer: http://foo.example/examples/preflightInvocation.html Content-Length: 55 Origin: http://foo.example Pragma: no-cache Cache-Control: no-cache
|
下面是服务器对实际请求的响应:
1 2 3 4 5 6 7 8 9 10
| HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:40 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://foo.example Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 235 Keep-Alive: timeout=2, max=99 Connection: Keep-Alive Content-Type: text/plain
|
3.2.3 总结
预检请求会使用 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 告知服务器实际请求的来源、方法、自定义头部。
预检响应通过 Access-Control-Allow-Origin、Access-Control-Allow-Methods 、Access-Control-Allow-Headers 头部来告知浏览器,请求是否被允许。
当预检请求被重定向时,大多浏览器会报错,可通过如下方法规避:
1、在服务端去掉预检请求的重定向
2、将实际请求变成简单请求
4 附带身份凭证的请求
4.1 定义
对于跨域的 XMLHttpRequest 或 Fetch 请求,浏览器默认不会发送身份凭证,当需要发送身份凭证时,需要给请求添加特殊标志位。
4.2 示例
下面示例是配置了 cookie 的请求,将 XMLHttpRequest
的 withCredentials
标志设置为 true
,从而告知浏览器此请求发送给服务器时需要携带 Cookies。
1 2 3 4 5 6 7 8 9 10 11
| var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/credentialed-content/'; function callOtherDomain(){ if(invocation) { invocation.open('GET', url, true); invocation.withCredentials = true; invocation.onreadystatechange = handler; invocation.send(); } }
|
下面是浏览器向服务器发送的携带 cookie 的跨域请求:
1 2 3 4 5 6 7 8 9 10
| GET /resources/access-control-with-credentials/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Referer: http://foo.example/examples/credential.html Origin: http://foo.example Cookie: pageAccess=2
|
下面是服务器对携带 cookie 的跨域请求的响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:34:52 GMT Server: Apache/2 Access-Control-Allow-Origin: http://foo.example--》1、对于附带凭证的请求,此字段值需为请求发起者,而不是*,否则请求将失败 Access-Control-Allow-Credentials: true-->2、此字段为 true 时,浏览器才会将响应返回给请求发起者 Cache-Control: no-cache Pragma: no-cache Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT-->3、将用户将浏览器端设置为拒绝第三方 cookie 时,此设置会失败 Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 106 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
|
4.3 总结
请求中需要设置 withCredentials
为 true,浏览器才会将 cookie 携带给服务器。
响应中不能缺失 Access-Control-Allow-Credentials
: true(第 17 行),否则响应内容不会返回给请求的发起者。
服务器必须将 Access-Control-Allow-Origin
的值设置为请求发起者,而不是“*
”,必否则请求将失败。
5 CORS 相关头部总结
5.1 请求头部
5.1.1 Origin
Origin 表明实际请求的来源,它不包含任何路径信息,只是服务器名称。
无论是否跨域此头部都会被发送。
5.1.2 Access-Control-Request-Method
该首部字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。
1
| Access-Control-Request-Method: <method>
|
该首部字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。
1
| Access-Control-Request-Headers: <field-name>[, <field-name>]*
|
5.2 响应头部
5.2.1 Access-Control-Allow-Origin
1
| Access-Control-Allow-Origin: <origin> | *
|
此字段指定了允许访问该资源的跨域 URI,对于没携带身份凭证的请求,服务器可以指定该字段的值为通配符 “*”,表示允许来自所有域的请求。
如果服务端指定了具体的域名而非 “*”,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。
在跨域访问时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置此响应头。
下面的响应使得请求发起者就能够通过 getResponseHeader 访问 X-My-Custom-Header
和 X-Another-Custom-Header
响应头。
1
| Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
|
5.2.3 Access-Control-Allow-Credentials
该响应字段头指定了当浏览器的 credentials
设置为 true 时,是否允许请求发起者读取 response的内容。
1
| Access-Control-Allow-Credentials: true
|
5.2.4 Access-Control-Max-Age
该响应字段指定了该预检请求响应的有效期为多少秒。
1
| Access-Control-Max-Age: <delta-seconds>
|
5.2.5 Access-Control-Allow-Methods
该响应字段其指定了实际请求所允许使用的 HTTP 方法。
1
| Access-Control-Allow-Methods: <method>[, <method>]*
|
该响应字段指定了实际请求中允许携带的首部字段。
1
| Access-Control-Allow-Headers: <field-name>[, <field-name>]*
|
参考链接:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
http://arunranga.com/examples/access-control/