最近在为高性能开源API网关apisix写跨域插件,发现该功能对协议要求要比较熟悉,借此机会重新复习下跨域协议,以及简要写下跨域功能的设计
跨来源资源共享(Cross-Origin Resource Sharing(CORS))是一种使用额外HTTP标头来让目前浏览网站的user agent能获得访问不同来源(网域)服务器特定资源之权限的机制。当user agent请求一个不是目前文件来源——来自于不同网域(domain)、通信协定(protocol)或通信端口(port)的资源时,会建立一个跨来源HTTP请求(cross-origin HTTP request)。
基于安全性考虑,浏览器和WebView发出的HTTP请求会有限制。例如,XMLHttpRequest及Fetch皆遵守同源政策(same-origin policy)。这代表网络应用程序所使用的这些API只能请求来自和应用程序相同网域的HTTP资源,除非使用了CORS标头。
CORS需要浏览器和服务器同时支持。当前桌面和移动浏览器对CORS的支持情况如下:
桌面浏览器:
浏览器 | Chrome | Edge | FireFox | IE | Opera | Safari |
---|---|---|---|---|---|---|
支持CORS最低版本 | 4 | 12 | 3.5 | 10 | 12 | 4 |
移动浏览器:
浏览器 | Android webview | Chrome for Android | Edge mobile | Firefox for Android | IE mobile | Opera Android | iOS Safari |
---|---|---|---|---|---|---|---|
支持CORS最低版本 | 2.1 | ALL | ALL | 4 | ALL | 12 | 3.2 |
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源通信没有差别,代码完全一样。浏览器一旦发现HTTP请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求(复杂请求),但用户不会感知。
支持CORS的主要改动点在server端
浏览器将CORS请求分成两类:简单请求(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
(3)没有事件监听器被注册到任何用来发出请求的 XMLHttpRequestUpload 上(经由 XMLHttpRequest.upload 方法取得)上。
(4)请求中没有 ReadableStream 类型的内容被用于上传。
PS:虽然这些都是网页目前已经可以送出的跨站请求,除非后端服务器回复正确CORS标志,否则不会有内容传回来,因此不允许跨域请求的网站无须担心会受到新的HTTP 存取控制的影响。
通常情况下主要涉及条件(1)和条件(2)
如果不满足上述条件任何一个,那么它就是预检请求。
浏览器发现自己发送的是简单跨域请求,则会只发送一次HTTP请求。相较于同源请求,CORS简单请求会在头信息中额外增加一个Origin字段。
下图是一个简单跨域请求例子:浏览器发现本次请求是跨域请求,就会自动在请求头信息中增加Origin字段(依赖于浏览器机制/或者JS解释器的实现)
请求和响应内容如下:
假定是从apigw.qcloud.com去请求qcloud.com的资源
请求
GET /resources/public-data/ HTTP/1.1
Host: qcloud.com
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://apigw.qcloud.com
响应
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
HTTP请求中的Origin字段表示该请求是来自于http://apigw.qcloud.com的请求
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: application/xml
本次请求示例中响应是携带了Access-Control-Allow-Origin: *,或者是Access-Control-Allow-Origin: http://apigw.qcloud.com
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
不满足简单请求条件之一的即是非简单请求。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
「预检(preflighted)」请求会先用HTTP 的OPTIONS 方法请求另一个域名资源,确认后续实际(actual)请求能否可安全送出。由于跨域请求可能会携带使用者的信息,所以要先进行预检请求。
下图是一个预检请求例子:
请求和响应内容如下:
假定是从apigw.qcloud.com去请求qcloud.com的资源
第一次是预检请求/响应:
OPTIONS /resources/post-here/ HTTP/1.1
Host: qcloud.com
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://apigw.qcloud.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
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://apigw.qcloud.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
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
等到预检请求完成后,浏览器才会发送真正的响应:
POST /resources/post-here/ HTTP/1.1
Host: qcloud.com
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
Content-Length: 55
Origin: http://apigw.qcloud.com
Pragma: no-cache
Cache-Control: no-cache
Arun
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://apigw.qcloud.com
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some GZIP'd payload]
先看请求,Access-Control-Request-Method告诉服务器发的请求是POST请求,Access-Control-Request-Headers通知自己带有X-PINGOTHER自定义header
再看响应,Access-Control-Allow-Origin这个与前面类似,Access-Control-Allow-Methods这里说明支持POST/GET/OPTIONS方法,Access-Control-Allow-Headers这里说明允许X-PINGOTHER自定义header,Access-Control-Max-Age用来指定本次预检请求的有效时间,86400是24小时也就是一天。
问题:1、若简单跨域请求校验失败,APIGW应如何回复?
2、若预检跨域请求校验失败,APIGW应如何回复?
3、目前浏览器并不支持预检请求的重定向,如果发生了预检请求的重定向,则浏览器会大概率报错
一旦服务器通过了"预检"请求,在Access-Control-Max-Age指定的时间内,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
Origin
Origin 字段表示了跨域请求的来源或者预检请求的来源。在任何跨域请求中,一定要携带Origin字段
Origin:
Access-Control-Request-Method(仅在预检请求中)
Access-Control-Request-Method 是用在预检请求中,告诉后端server实际请求用的HTTP方法
Access-Control-Request-Method:
Access-Control-Request-Headers(仅在预检请求中)
Access-Control-Request-Headers标识用于预检请求中,它会告诉后端server自己所携带的自定义header字段有哪些
Access-Control-Request-Headers: [, ]*
Access-Control-Allow-Origin
跨域响应会携带该字段,若服务器允许所有uri来访问自己的资源,那么则该字段为*;若要允许http://www.qq.com访问该资源,则为Access-Control-Allow-Origin: http://www.qq.com
Access-Control-Allow-Origin: | *
Access-Control-Expose-Headers
Access-Control-Expose-Headers表示服务器允许浏览器从响应中解析哪些header字段
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
表示服务器允许浏览器从响应中解析X-My-Custom-Header, X-Another-Custom-Header字段
Access-Control-Max-Age
Access-Control-Max-Age表示预检请求结果请求成功后,多长时间内非简单请求可以不需要再发预检请求,可以继续直接使用跨域请求请求资源
Access-Control-Max-Age:
Access-Control-Allow-Credentials
这里下次补充。
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods(仅在预检请求响应中)
Access-Control-Allow-Methods表示服务器访问操作该资源允许哪些方法
Access-Control-Allow-Methods: [, ]*
Access-Control-Allow-Headers(仅在预检请求响应)
Access-Control-Allow-Headers表示在访问这个域资源的时候,预检请求响应中哪些header字段可以在跨域请求中使用
Access-Control-Allow-Headers: [, ]*
https://www.w3.org/TR/cors/