預請求
參考:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#預請求簡而言之,在跨域并且嘗試添加一些特殊頭及自定義頭的情況下,由于浏覽器的安全機制,會加多一次OPTIONS預請求(詢問請求),與跨域伺服器協商可以設定的頭部資訊,可以允許的HTTP協定等等資訊。
以如下圖一次跨域請求為例。

圖中代碼如下
1 var settings = {
2 type: "POST",
3 url: 'http://www.movesun.com/cors.php?allow_method=PUT',
4 contentType: "application/json",
5 dataType:"json",
6 data : {
7 "name" : "lvyahui"
8 },
9 xhrFields : {
10 // withCredentials : true
11 },
12 success: function(resp) {
13 console.log(resp);
14 }
15 ,
16 headers: {
17 appkey:"87a8ea08-dbaa-11e6-b3f9-7056818a4db5",
18 "X_forwarded-for":"10.104.239.XXX"
19 }
20 };
21 $.ajax(settings);
可以看到,這段代碼在movesun.com網站下,嘗試向www.movesun.com發送跨域POST 請求,并且有自定義頭(Content-Type設定了application/json類型也是原因之一),是以浏覽器在發送真實post請求之前,發起了一個OPTIONS請求詢問。
請求之是以可以成功,是因為後端伺服器正常處理了OPTIONS請求,并且響應了正确的跨域響應頭,後端代碼cors.php如下
1 <?php
2
3 if('OPTIONS' === $_SERVER['REQUEST_METHOD']){
4 header('Access-Control-Allow-Origin:*');
5 header('Access-Control-Allow-Headers:appkey,X_forwarded-for,Content-Type');
6 header('Access-Control-Allow-Methods:' . (isset($_GET['allow_method']) ? $_GET['allow_method'] : 'OPTIONS'));
7 //header('Access-Control-Allow-Credentials:true');
8 exit(0);
9 }
10
11 header('Access-Control-Allow-Origin:*');
12
13 echo json_encode(array(
14 'code' => 0,
15 'msg' => '',
16 'data' => array()
17 ));exit(0);
可以看到伺服器判斷請求類型為OPTIONS時,指定了如下幾個Http響應頭
1、Access-Control-Allow-Origin : 跨域伺服器允許的來源位址(跟請求的Origin進行比對),可以是*或者某個确切的位址,不允許多個位址。當然背景代碼可以動态判斷來源位址進行動态設定,這主要是因為有時需要允許任意來源通路,并且要攜帶Cookie,此時需要明确指定位址(原因在文後常見問題中說明),下面這段PHP代碼和Java代碼(注意Java代碼中Cookie沒有取端口,因為Cookie端口不同也算同域,可以通路到)就是取來源位址并響應
1 if (isset($_SERVER['HTTP_REFERER'])) {
2 $urls = parse_url($_SERVER['HTTP_REFERER']);
3 $url = $urls['scheme'] . '://' . $urls['host'];
4 if (isset($urls['port'])) {
5 $url .= ':' . $urls['port'];
6 }
7 } else {
8 $url = '*';
9 }
10
11 header("Access-Control-Allow-Origin: " . $url);
1 public void filter(ContainerRequestContext requestContext) throws IOException {
2 String origin = requestContext.getHeaderString("Origin");
3 if(origin != null && !origin.trim().equals("")
4 // postMan 請求的protocol 是 chrome-extension://
5 && origin.startsWith("http://")){
6 URL url = new URL(origin);
7 String strUrl = url.getProtocol() + "://" + url.getHost();
8 if(url.getPort() > 0){
9 strUrl += ":" + url.getPort();
10 }
11 originUrl = strUrl;
12 if(!cookieDomainAuto
13 && (sysConfig.getCookieDomain() == null || sysConfig.getCookieDomain().equals(""))){
14 cookieDomainAuto = true;
15 }
16 if(cookieDomainAuto){
17 // 動态判斷 cookie domain
18 if(url.getHost().matches(PATTERN_IP)){
19 // IP
20 sysConfig.setCookieDomain(url.getHost());
21 } else {
22 int start = url.getHost().lastIndexOf('.',url.getHost().lastIndexOf('.') - 1);
23 String domain;
24 if(start > 0){
25 domain = url.getHost().substring(start + 1);
26 }else{
27 domain = url.getHost();
28 }
29 // domain
30 sysConfig.setCookieDomain(domain);
31 }
32 }
33 }
34 }
Java動态設定Allow-Origin與Cookie Domain
2、Access-Control-Allow-Methods:跨域伺服器允許的請求方法。經測試發現,不論Access-Control-Allow-Methods設定為簡單請求還是複雜請求類型,所有的簡單的請求(GET,HEAD,POST)也是可以正常請求的。
3、Access-Control-Allow-Headers:跨域伺服器允許用戶端添加或自定義哪些http 頭。
下面是這兩次請求的封包
OPTIONS請求封包
1 OPTIONS http://www.movesun.com/cors.php HTTP/1.1
2 Host: www.movesun.com
3 Proxy-Connection: keep-alive
4 Pragma: no-cache
5 Cache-Control: no-cache
6 Access-Control-Request-Method: POST
7 Origin: http://movesun.com
8 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36
9 Access-Control-Request-Headers: appkey, content-type, x_forwarded-for
10 Accept: */*
11 Referer: http://movesun.com/
12 Accept-Encoding: gzip, deflate, sdch
13 Accept-Language: zh-CN,zh;q=0.8
14
15
16 HTTP/1.1 200 OK
17 Date: Fri, 10 Mar 2017 05:48:07 GMT
18 Server: Apache
19 Access-Control-Allow-Origin: *
20 Access-Control-Allow-Headers: appkey,X_forwarded-for,Content-Type
21 Access-Control-Allow-Methods: POST
22 Vary: User-Agent,Accept-Encoding
23 Content-Encoding: gzip
24 Content-Length: 20
25 Content-Type: text/html
26 X-Cache: MISS from SK-SQUIDDEV-11
27 X-Cache-Lookup: MISS from SK-SQUIDDEV-11:8080
POST請求封包
1 POST http://www.movesun.com/cors.php HTTP/1.1
2 Host: www.movesun.com
3 Proxy-Connection: keep-alive
4 Content-Length: 12
5 Pragma: no-cache
6 Cache-Control: no-cache
7 Origin: http://movesun.com
8 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36
9 Content-Type: application/json
10 Accept: application/json, text/javascript, */*; q=0.01
11 X_forwarded-for: 10.104.239.194
12 appkey: 87a8ea08-dbaa-11e6-b3f9-7056818a4db5
13 Referer: http://movesun.com/
14 Accept-Encoding: gzip, deflate
15 Accept-Language: zh-CN,zh;q=0.8
16 name=lvyahui
從封包中可以看出,OPTIONS請求背景可以拿到URL中的GET參數,也就是說,如果真實請求是GET請求,則後端在處理來詢問的OPTIONS請求時,就可以擷取到所有查詢參數了。如mozilla官網所寫,筆者調試發現,一些跨域請求,即便抛出了錯誤的情況下,請求也真的到了背景伺服器,隻是響應被浏覽器攔截了。
另外,有時不想在背景代碼中處理OPTIONS請求,則可以在nginx server節點下做如下配置,表示攔截處理所有OPTIONS請求。
1 location ^~ / {
2 if ($request_method = OPTIONS ) {
3 add_header Content-Length 0;
4 add_header Content-Type text/plain;
5 add_header 'Access-Control-Allow-Origin' '*';
6 add_header 'Access-Control-Allow-Methods' '*';
7 add_header 'Access-Control-Allow-Headers' 'appkey,X_forwarded-for,Content-Type';
8 return 200;
9 }
10 }
常見跨域問題
下面是一些跨域下常見的一些問題
- 添加了跨域伺服器不允許的自定義頭會抛出 XMLHttpRequest cannot load http://www.movesun.com/cors.php. Request header field custom_heaer is not allowed by Access-Control-Allow-Headers in preflight response.
- 當未設定允許某種複雜請求時,使用複雜請求就會抛出如下錯誤,表示真實請求使用了伺服器不允許的方法。在隻允許POST的情況下,GET請求是可以被發送的,HEAD也可以成功,僅僅允許GET的情況下,POST也是可以發送成功的,HEAD也可以成功 。簡單請求都可以成功,等等,其實經測試發現,不論Access-Control-Allow-Methods設定為簡單請求還是複雜請求類型,所有的簡單的請求(GET,HEAD,POST)也是可以正常請求的。XMLHttpRequest cannot load http://www.movesun.com/cors.php. Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.
- 預處理請求沒有沒正常處理,這種是詢問請求響應了非200狀态碼,會抛出 XMLHttpRequest cannot load http://movesun.qq.com/cors.php?allow_method=PUT. Response for preflight has invalid HTTP status code 405 。如
-
1 if('OPTIONS' === $_SERVER['REQUEST_METHOD']){ 2 header("HTTP/1.1 405 Method Not Allowed"); 3 exit(-1); 4 }
- 錯誤是來源位址不是伺服器所允許的來源位址。如下,此時伺服器響應 Access-Control-Allow-Origin:http://www.movesun.com,表示跨域伺服器允許在Origin:http://www.movesun.com 的機器上通路,而使用者試圖在http://movesun.com跨域請求目的伺服器http://movesun.qq.com/cors.php?allow_method=PUT:XMLHttpRequest cannot load http://movesun.qq.com/cors.php?allow_method=PUT. Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'http://www.movesun.com' that is not equal to the supplied origin. Origin 'http://movesun.com' is therefore not allowed access.
- 前端設定了攜帶簽名标志,但是跨域伺服器不允許攜帶,沒有設定 Access-Control-Allow-Credentials:true 。如:XMLHttpRequest cannot load http://movesun.qq.com/cors.php?allow_method=PUT. Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is ''. It must be 'true' to allow credentials. Origin 'http://movesun.com' is therefore not allowed access.
- 前端嘗試在真實請求中攜帶簽名Cookie,跨域伺服器允許攜帶Cookie,但是伺服器允許所有來源位址,會報這個錯誤,在跨域攜帶cookie時,必須明确指定來源位址,比如 Access-Control-Allow-Origin:http://movesun.com。例如:XMLHttpRequest cannot load http://movesun.qq.com/cors.php?allow_method=PUT. A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin' header when the credentials flag is true. Origin 'http://movesun.com' is therefore not allowed access. The credentials mode of an XMLHttpRequest is controlled by the withCredentials attribute.
并且 跨域攜帶Cookie時,跨域伺服器處理詢問請求(OPTIONS)和真實請求,都必須響應明确的來源位址和允許攜帶cookie的标志。否則會報上面兩種錯誤。當然很顯然的兩次響應的Allow-Origin都是一緻的。如下
1 if('OPTIONS' === $_SERVER['REQUEST_METHOD']){
2 header('Access-Control-Allow-Origin:http://movesun.com');
3 header('Access-Control-Allow-Headers:appkey,X_forwarded-for,Content-Type');
4 header('Access-Control-Allow-Methods:' . (isset($_GET['allow_method']) ? $_GET['allow_method'] : 'OPTIONS'));
5 header('Access-Control-Allow-Credentials:true');
6 exit(0);
7 }
8 header('Access-Control-Allow-Origin:http://movesun.com');
9 header('Access-Control-Allow-Credentials:true');
10 echo json_encode(array(
11 'code' => 0,
12 'msg' => '',
13 'data' => array()
14 ));exit(0);
注意:文中的測試接口 在
http://movesun.com/cors.php或者
http://www.movesun.com/cors.php,感興趣的讀者可以用這個接口測試。