跨域資源共享——CORS
跨域資源共享(Cross-Origin Resource Sharing)是一種機制,它使用額外的 HTTP 頭部告訴瀏覽器可以讓一個web應用進行跨域資源請求。
請求型別
簡單請求
若一個請求同時滿足下述所有條件,則該請求可視為“簡單請求”(注:灰色字型內容瞭解即可):
- 使用的方法為
GET
HEAD
POST
- 手動設定的頭部欄位只能是(注意:也可以設定 Forbidden header name 中的頭部欄位,如
Connection
、Accept-Encoding
等,但是設定無效)Accept
Accept-Language
Content-Language
Content-Type
(值的範圍還要符合下面的要求)- `DPR`
- `Downlink`
- `Save-Data`
- `Viewpoer-Width`
- `Width`
Content-Type
的值只能為application/x-www-form-urlencoded
multipart/form-data
text/plain
- No event listeners are registered on any `XMLHttpRequestUpload` object used in the request; these are accessed using the XMLHttpRequest.upload property.
- No ReadableStream object is used in the request.
預檢請求
CORS 預檢請求發生在實際請求之前,用於檢查伺服器是否支援 CORS,以判斷實際請求傳送是否安全。預檢請求使用的方式是 OPTIONS
當一個請求不是“簡單請求”時,即應該先發送預檢請求,比如:
- 這個請求的請求方式不是
GET
、HEAD
、POST
- 或者,這個請求設定了自定義的頭部欄位,比如
X-xxx
- 或者這個請求的
Content-Type
值不是application/x-www-form-urlencoded
、multipart/form-data
、text/plain
,等等
跨域請求過程
跨域請求,CORS要求服務端設定一些頭部欄位,最重要的一個就是 Access-Control-Allow-Origin
。下面以案例進行說明,前端使用 axios 進行 http 傳輸,後端以 koa 作為服務端框架,並使用CORS中介軟體 koa2-cors。
簡單跨域請求
// Client http://localhost:8080
simpleRequest() {
axios({
method: 'GET',
url: 'http://localhost:3000/api/simple'
}).then(data => {
console.log(data);
});
}
複製程式碼
// Server http://localhost:3000
app.use(cors());
router.get('/api/simple', ctx => {
ctx.body = { result: 'simple request success' };
});
複製程式碼
HTTP 報文:
HTTP 請求頭部有個 Origin
欄位,表示請求來自哪裡。HTTP 響應頭部中的 Access-Control-Allow-Origin
表示哪個域可以訪問該資源。使用 Origin
和 Access-Control-Allow-Origin
就完成了最簡單的訪問控制。
預檢請求&正式請求
// Client http://localhost:8080
mainRequest() {
axios({
method: 'POST',
url: 'http://localhost:3000/api/mainRequest',
headers: { 'X-test': 'CORS' } // 增加一個自定義的頭部欄位,觸發預檢請求
}).then(data => {
console.log(data);
});
}
複製程式碼
// Server http://localhost:3000
app.use(cors());
router.post('/api/mainRequest', ctx => {
ctx.body = { result: 'main request success' };
});
複製程式碼
預檢請求的報文:
請求首部欄位 Access-Control-Request-Method
告知伺服器,實際請求將使用 POST
方法。 請求首部欄位 Access-Control-Request-Headers
告知伺服器,實際請求將攜帶一個自定義請求首部欄位:x-test。伺服器據此決定,該實際請求是否被允許。
響應首部欄位 Access-Control-Allow-Methods
表明伺服器允許客戶端使用哪些方法發起請求。 響應首部欄位 Access-Control-Allow-Headers
表明伺服器允許請求中攜帶欄位 x-test。
實際請求的報文:
實際請求中傳送了 X-test
頭部欄位,響應狀態碼 200 OK。
可以看到,預檢請求中 Client 和 Server 使用了更多的頭部欄位來完成訪問控制。那麼,CORS 相關的請求頭部欄位和響應頭部欄位共有哪些呢?
頭部欄位
HTTP 請求頭部欄位
- Origin
Origin
頭部欄位表示預檢請求或實際請求的源站。 - Access-Control-Request-Method
Access-Control-Request-Method
頭部欄位用於預檢請求。其作用是,將實際請求所使用的 HTTP 方法告訴伺服器。 - Access-Control-Request-Headers
Access-Control-Request-Headers
頭部欄位用於預檢請求。其作用是,將實際請求所攜帶的首部欄位告訴伺服器。
注意,以上請求頭部欄位無須手動設定,當使用 XMLHttpRequest
物件發起跨域請求時,它們已經被設定就緒。
HTTP 響應頭部欄位
-
Access-Control-Allow-Origin
其語法如下:Access-Control-Allow-Origin: <origin> | * 複製程式碼
origin 引數的值指定了允許訪問該資源的外域 URI。如果該欄位的值為萬用字元
*
,則表示允許來自所有域的請求。
注意,如果服務端指定了具體的域名而非*
,那麼響應頭部中的Vary
欄位的值必須包含Origin
。這將告訴客戶端:伺服器對不同的源站返回不同的內容。 -
Access-Control-Allow-Methods
Access-Control-Allow-Methods
頭部欄位用於預檢請求的響應。其指明瞭實際請求所允許使用的 HTTP 方法。 -
Access-Control-Allow-Headers
Access-Control-Allow-Headers
頭部欄位用於預檢請求的響應。其指明瞭實際請求中允許攜帶的首部欄位。 -
Access-Control-Expose-Headers
跨域請求中,瀏覽器預設情況下通過API只能獲取到以下響應頭部欄位:Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
如果想要訪問其他響應頭部資訊,則需要在伺服器端設定
Access-Control-Allow-Headers
。Access-Control-Expose-Headers
讓伺服器把允許瀏覽器訪問的頭部欄位放入白名單,比如: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: 5 複製程式碼
表示在第一次預檢請求發出後,5s 內再訪問該介面時會直接傳送實際請求,而不需要先發預檢請求。過了 5s 後,會再要求先發送預檢請求,以此類推。
app.use( cors({ maxAge: 5 }) ); 複製程式碼
服務端設定了 5s 快取,實際請求如下:
注意,如果設定快取後,發現每次還是會發送 OPTIONS 請求,請檢查你是不是勾選了“禁止快取”。
-
Access-Control-Allow-Credentials
XMLHttpRequest.withCredentials
(或者Request.credentials
)表示跨域請求中,user agent 是否應該傳送 cookies、authorization headers 或者 TLS client certificates 等憑據。Access-Control-Allow-Credentials
的作用就是:當 credentials 為 “真” 時(XHR和Fetch設定方式不一樣),Access-Control-Allow-Credentials
告訴瀏覽器是否把響應內容暴露給前端 JS 程式碼。比如:// Client http://localhost:8080 simpleRequest() { axios({ method: 'GET', url: 'http://localhost:3000/api/simple', withCredentials: true // 增加了withCredentials 選項 }).then(data => { console.log(data); }); } // Server http://localhost:3000 app.use( cors({ maxAge: 5, // credentials: true }) ); 複製程式碼
此時,服務端未設定
credentials: true
,發起請求能看到客戶端報錯:如果服務端設定了
credentials: true
則客戶端就不會報錯了。預檢請求的時候,
Access-Control-Allow-Credentials
響應頭部欄位表示實際請求中是否可以使用 credentials。
關於 CORS 響應頭部欄位的運用,建議看一下 koa2-cors 中介軟體的原始碼。程式碼只有幾十行,特別清晰易懂。
CORS 相關內容如上,瞭解之後能更好地幫助我們解決日常聯調中出現的問題,比如:出現跨域了服務端怎麼設定,axios.post
方法傳送一個物件時為什麼會出現 OPTIONS 請求,代理伺服器怎麼才能轉發cookies等等。