天天看點

CORS詳解

## 0、關于CORS

說到CORS,就不得不先了解跨站HTTP請求(Cross-site HTTP request)。

跨域HTTP請求是指發起請求的資源所在域不同于該請求所指向資源所在的域的HTTP請求。

正如大家所知,出于安全考慮,浏覽器會限制腳本中發起的跨站請求。使用XMLHttpRequest發起HTTP請求必須遵守同源政策。 具體而言,Web 應用程式能且隻能使用 XMLHttpRequest 對象向其加載的源域名發起 HTTP 請求,而不能向任何其它域名發起請求。

由于Web應用技術越來越豐富,我們非常渴望在不丢失安全的前提下,能夠實作跨站請求。特别是現在的Web程式結構,一般是HTML+REST API。在之前的實作中,我們一般采用jsonp來發起跨站請求,這其實是利用了html标簽的特點。

W3C的Web應用工作組推薦了一種新的機制,即跨域資源共享(Cross-Origin Resource Sharing),也就是目前我們提到的CORS。 

CORS的核心,就是讓伺服器來确定是否允許跨域通路。

## 1、典型場景

### 1.1、簡單請求

什麼是簡單請求?全部滿足以下條件的請求可以稱之為簡單請求:

1. 隻使用GET、HEAD或者POST請求方法。如果是POST,則資料類型(Content-Type)隻能是``application/x-www-form-urlencodeed``、``multipart/form-data``、``text/plain``中的一種。
2. 沒有使用自定義的請求頭(如x-token)

按照這個規則,那我們的能實作跨域請求的情況如下:

Server代碼:

```javascript
'use strict';

var http = require('http');
var server = http.createServer((req, res) => {
  //之後設定了Access-Control-Allow-Origin,才會允許跨域
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.write('abc');
  res.end();
});

server.listen(10000, () => {
  console.log('started.');
});
```
Client代碼:

```javascript
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
  if(xhr.readyState === XMLHttpRequest.DONE){
    console.log('Result:', xhr.responseText);
  }
}

//場景一:GET請求,不需要Header,允許跨域
xhr.open('GET', 'http://localhost:10000/', true);
xhr.send();

//場景二: POST請求,需要設定為指定Header(不設定content-type也可),允許跨域
xhr.open('POST', 'http://localhost:10000/', true);
//此處value必須是text/plain或者application/x-www-form-urlencoded或者multipart/form-data。
//此處也可以不設定
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.send();

//場景三:DELETE請求(不允許跨域)
xhr.open('DELETE', 'http://localhost:10000/', true);
xhr.send();

//場景四:POST請求,有自定義Header(不允許跨域)
xhr.open('POST', 'http://localhost:10000/', true);
xhr.setRequestHeader('x-token', 'a');
xhr.send();
```

### 1.2、預請求

不同于簡單請求,預請求要求必須先發送一個OPTIONS請求給站點,來查明該站點是否允許跨域請求,這樣做的原因是為了避免跨站請求可能對目的站點的資料造成的損壞。

如果請求滿足以下任一條件,則會産生預請求:

1. 請求以GET、HEAD、POST之外的方法發起。或者,使用POST,但資料類型為``application/x-www-form-urlencoded``, ``multipart/form-data`` 或者 ``text/plain`` 以外的資料類型。(注:之前的版本隻有text/plain可以不用發起預請求)。
2. 使用了自定義請求頭。

按照如上規則,我們來列舉幾個應用場景:

Server端代碼:

```javascript
'use strict';

var http = require('http');
var server = http.createServer((req, res) => {
  //之後設定了Access-Control-Allow-Origin,才會允許跨域
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'POST, DELETE, GET');
  res.setHeader('Access-Control-Allow-Headers', 'x-token');
  //設定預請求緩存1天,1天内再次請求,可以跳過預請求
  //此功能需要用戶端緩存支援,如果用戶端禁用緩存,那麼每次都會預請求
  res.setHeader('Access-Control-Max-Age', 60 * 60 * 24); 
  res.write('abc');
  res.end();
});

server.listen(10000, () => {
  console.log('started.');
});
```

Client端代碼:

```javascript
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
  if(xhr.readyState === XMLHttpRequest.DONE){
    console.log('Result:', xhr.responseText);
  }
}

//場景一:DELETE請求,發送OPTIONS,比對,允許跨域
xhr.open('DELETE', 'http://localhost:10000/', true);
xhr.send();

//場景二:PUT請求,發送OPTIONS,不比對,不允許跨域
xhr.open('PUT', 'http://localhost:10000/', true);
xhr.send();

//場景三:DELETE請求比對,使用自定義Header不比對,不允許跨域
xhr.open('DELETE', 'http://localhost:10000/', true);
xhr.setRequestHeader('x-token1', 'aa');
xhr.send();

//場景四:POST請求,比對的自定義Header,允許跨域
xhr.open('POST', 'http://localhost:10000/', true);
xhr.setRequestHeader('x-token', 'a');
xhr.send();
```

### 1.3、帶憑證的請求

一般來說,對于跨站請求,浏覽器是不會發送憑證(HTTP Cookies和驗證資訊)的。如果要發送帶憑證的資訊,隻需要給XMLHttpRequest設定一個特殊的屬性``withCredentials = true``,通過這種方式,浏覽器就允許發送憑證資訊。

帶憑證的請求可能是簡單請求,也可以是會有預請求。是否允許跨域,會先判斷簡單請求和預請求的規則,然後還會帶上帶憑證的請求自己的規則。

在帶憑證的請求中,後端的響應必須包含Header``Access-Control-Allow-Credentials=true``,同時Header ``Access-Control-Allow-Origin``,不能再使用*号這種比對符。

具體示例如下:

服務端代碼:

```javascript 
'use strict';

var http = require('http');
var server = http.createServer((req, res) => {
  //要處理帶憑證的請求,此Header不能使用*。
  res.setHeader('Access-Control-Allow-Origin', 'http://10.16.85.170:8000');
  res.setHeader('Access-Control-Allow-Methods', 'POST, DELETE, GET');
  res.setHeader('Access-Control-Allow-Headers', 'x-token');
  res.setHeader('Access-Control-Max-Age', 60 * 60 * 24); 
  //隻有設定了該Header,才允許帶憑證的請求。
  res.setHeader('Access-Control-Allow-Credentials', true);
  res.write('abc');
  res.end();
});

server.listen(10000, () => {
  console.log('started.');
});
```

用戶端代碼:

```javascript
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
  if(xhr.readyState === XMLHttpRequest.DONE){
    console.log('Result:', xhr.responseText);
  }
}
//優先滿足預請求,然後滿足憑證請求,允許跨域。
xhr.open('POST', 'http://localhost:10000/', true);
xhr.withCredentials = true;
xhr.setRequestHeader('x-token', 'a');
xhr.send();
```
## 2、HTTP響應頭

### 2.1、 後端HTTP響應頭

此處列舉後端有關CORS的響應頭:

1. Access-Control-Allow-Origin: <origin> | *  允許的域名
2. Access-Control-Expose-Headers: <headers> 允許的白名單Header,多個用逗号隔開
3. Access-Control-Max-Age: <delta-seconds>  預請求緩存時間,機關秒
4. Access-Control-Allow-Credentials: true | false  是否允許帶憑證的請求
5. Access-Control-Allow-Methods: <methods> 允許的請求類型,多個用逗号隔開
6. Access-Control-Allow-Headers: <headers> 在實際請求中,允許的自定義header,多個用逗号隔開

### 2.2、 浏覽器發出跨域請求的響應頭

此處列舉出浏覽器在發送跨域請求時,會帶上的響應頭:

1. Origin: <origin> 告訴伺服器,請求來自哪裡,僅僅是伺服器名,不包含路徑。
2. Access-Control-Request-Method: <method> 預請求時,告訴伺服器實際的請求方式
3. Access-Control-Request-Headers: <headers> 預請求時,告訴伺服器,實際請求所攜帶的自定義Header


## 3、參考資料

1. [MDN HTTP access control (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests)
2. [MDN HTTP通路控制(CORS)](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS)      

繼續閱讀