## 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)