背景
不知道大家有沒有發現,有時候我們在調用后臺接口的時候,會請求兩次,如下圖的
其實第一次發送的就是preflight request(預檢請求),那么這篇文章將講一下,為什么要發預檢請求,什么時候會發預檢請求,預檢請求都做了什么
一. 為什么要發預檢請求
我們都知道瀏覽器的同源策略,就是出于安全考慮,瀏覽器會限制從腳本發起的跨域HTTP請求,像XMLHttpRequest和Fetch都遵循同源策略。
瀏覽器限制跨域請求一般有兩種方式:
- 瀏覽器限制發起跨域請求
- 跨域請求可以正常發起,但是返回的結果被瀏覽器攔截了
一般瀏覽器都是第二種方式限制跨域請求,那就是說請求已到達服務器,并有可能對數據庫里的數據進行了操作,但是返回的結果被瀏覽器攔截了,那么我們就獲取不到返回結果,這是一次失敗的請求,但是可能對數據庫里的數據產生了影響。
為了防止這種情況的發生,規范要求,對這種可能對服務器數據產生副作用的HTTP請求方法,瀏覽器必須先使用OPTIONS
方法發起一個預檢請求,從而獲知服務器是否允許該跨域請求:如果允許,就發送帶數據的真實請求;如果不允許,則阻止發送帶數據的真實請求。
二. 什么時候發預檢請求
HTTP請求包括: 簡單請求 和 需預檢的請求
1. 簡單請求
簡單請求不會觸發CORS預檢請求,“簡屬于
單請求”術語并不屬于Fetch(其中定義了CORS)規范。
若滿足所有下述條件,則該請求可視為“簡單請求”:
- 使用下列方法之一:
GET
HEAD
-
POST
-
Content-Type
: (僅當POST方法的Content-Type值等于下列之一才算做簡單需求)text/plain
multipart/form-data
application/x-www-form-urlencoded
-
注意: WebKit Nightly 和 Safari Technology Preview 為Accept
, Accept-Language
, 和 Content-Language
首部字段的值添加了額外的限制。如果這些首部字段的值是“非標準”的,WebKit/Safari 就不會將這些請求視為“簡單請求”。WebKit/Safari 并沒有在文檔中列出哪些值是“非標準”的,不過我們可以在這里找到相關討論:Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language, Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它瀏覽器并不支持這些額外的限制,因為它們不屬于規范的一部分。
2.需預檢的請求
“需預檢的請求”要求必須首先使用OPTIONS
方法發起一個預檢請求到服務區,以獲知服務器是否允許該實際請求?!邦A檢請求”的使用,可以避免跨域請求對服務器的用戶數據產生未預期的影響。
當請求滿足下述任一條件時,即應首先發送預檢請求:
- 使用了下面任一 HTTP 方法:
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
- 人為設置了對 CORS 安全的首部字段集合之外的其他首部字段。該集合為:
Accept
Accept-Language
Content-Language
Content-Type
DPR
Downlink
Save-Data
Viewport-Width
Width
-
Content-Type
的值不屬于下列之一:application/x-www-form-urlencoded
multipart/form-data
text/plain
如下是一個需要執行預檢請求的HTTP請求:
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-PRODUCT', 'H5');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
......
上面的代碼使用POST請求發送一個XML文檔,該請求包含了一個自定義的首部字段(X-PRODUCT:H5)。另外,該請求的Content-Type
為application/xml
。因此,該請求需要首先發起“預檢請求”。
1. OPTIONS /resources/post-here/
2. HTTP/1.1
3. Host: bar.other
4. User-Agent: Mozilla/5.0 (Macintosh; U; 5.Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
6. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
7. Accept-Language: en-us,en;q=0.5
8. Accept-Encoding: gzip,deflate
9. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
10. Connection: keep-alive
11. Origin: http://foo.example
12. Access-Control-Request-Method: POST
13. Access-Control-Request-Headers: X-PINGOTHER, Content-Type
14. HTTP/1.1 200 OK
15. Date: Mon, 01 Dec 2008 01:15:39 GMT
16. Server: Apache/2.0.61 (Unix)
17. Access-Control-Allow-Origin: http://foo.example
18. Access-Control-Allow-Methods: POST, GET, OPTIONS
19. Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
20. Access-Control-Max-Age: 86400
21. Vary: Accept-Encoding, Origin
22. Content-Encoding: gzip
23. Content-Length: 0
24. Keep-Alive: timeout=2, max=100
25. Connection: Keep-Alive
26. Content-Type: text/plain
從上面的報文中可以看到,第1~12行發送了一個使用OPTIONS
方法的預檢請求。 OPTIONS
是HTTP/1.1協議中定義的方法,用以從服務器獲取更多信息。該方法不會對服務器資源產生影響。遇見請求中同時攜帶了下面兩個首部字段:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PRODUCT
首部字段 Access-Control-Request-Method 告知服務器,實際請求將使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服務器,實際請求將攜帶兩個自定義請求首部字段:X-PINGOTHER 與 Content-Type。服務器據此決定,該實際請求是否被允許。
第14~26行 為預檢請求的響應,表明服務器將堅守后續的實際請求。重點看第17~20行:
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
首部字段 Access-Control-Allow-Methods
表明服務器允許客戶端使用 POST,GET 和 OPTIONS 方法發起請求。
首部字段Access-Control-Allow-Headers
表明服務器允許請求中攜帶字段X-PINGOTHER
與Content-Type
。與 Access-Control-Allow-Methods
一樣,Access-Control-Allow-Headers
的值為逗號分割的列表。
最后,首部字段
Access-Control-Max-Age
表明該響應的有效時間為 86400 秒,也就是 24 小時。在有效時間內,瀏覽器無須為同一請求再次發起預檢請求。請注意,瀏覽器自身維護了一個最大有效時間,如果該首部字段的值超過了最大有效時間,將不會生效。
預檢請求完成之后,發送實際請求:
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
<?xml version="1.0"?><person><name>Arun</name></person>
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
[Some GZIP'd payload]
四.參考資料
https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS