跨域資源共享(CORS)安全性
背景
提起瀏覽器的同源策略,大家都很熟悉。不同域的客戶端腳本不能讀寫對方的資源。但是實踐中有一些場景需要跨域的讀寫,所以出現了一些hack的方式來跨域。比如在同域內做一個代理,JSON-P等。但這些方式都存在缺陷,無法完美的實現跨域讀寫。所以在XMLHttpRequest v2標準下,提出了CORS(Cross Origin Resourse-Sharing)的模型,試圖提供安全方便的跨域讀寫資源。目前主流瀏覽器均支持CORS。
技術原理
CORS定義了兩種跨域請求,簡單跨域請求和非簡單跨域請求。當一個跨域請求發送簡單跨域請求包括:請求方法為HEAD,GET,POST;請求頭只有4個字段,Accept,Accept-Language,Content-Language,Last-Event-ID;如果設置了Content-Type,則其值只能是application/x-www-form-urlencoded,multipart/form-data,text/plain。說起來比較別扭,簡單的意思就是設置了一個白名單,符合這個條件的才是簡單請求。其他不符合的都是非簡單請求。
之所以有這個分類是因為瀏覽器對簡單請求和非簡單請求的處理機制是不一樣的。當我們需要發送一個跨域請求的時候,瀏覽器會首先檢查這個請求,如果它符合上面所述的簡單跨域請求,瀏覽器就會立刻發送這個請求。如果瀏覽器檢查之后發現這是一個非簡單請求,比如請求頭含有X-Forwarded-For字段。這時候瀏覽器不會馬上發送這個請求,而是有一個preflight,跟服務器驗證的過程。瀏覽器先發送一個options方法的預檢請求。如果預檢通過,則發送這個請求,否則就不拒絕發送這個跨域請求。
下面詳細分析一下實現安全跨域請求的控制方式。先看一下非簡單請求的預檢過程。
非簡單請求
瀏覽器先發送一個options方法的請求。帶有如下字段:
- Origin:?普通的HTTP請求也會帶有,在CORS中專門作為Origin信息供后端比對,表明來源域。
-?Access-Control-Request-Method:?接下來請求的方法,例如PUT,?DELETE等等
-?Access-Control-Request-Headers:?自定義的頭部,所有用setRequestHeader方法設置的頭部都將會以逗號隔開的形式包含在這個頭中
服務器收到"預檢"請求以后,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,確認允許跨源請求,就可以做出回應。
會返回對應對的字段
- Access-Control-Allow-Origin:
- Access-Control-Allow-Methods:該字段必需,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次“預檢”請求。
- Access-Control-Allow-Headers:
如果服務器否定了"預檢"請求,會返回一個正常的HTTP回應,但是沒有任何CORS相關的頭信息字段。這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發一個錯誤,被XMLHttpRequest對象的onerror回調函數捕獲
一旦服務器通過了"預檢"請求,以后每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭信息字段。服務器的回應,也都會有一個Access-Control-Allow-Origin頭信息字段。
為什么要預檢?
這是為了防止這些新增的請求,對傳統的沒有 CORS 支持的服務器形成壓力,給服務器一個提前拒絕的機會,這樣可以防止服務器大量收到DELETE和PUT請求,這些傳統的表單不可能跨域發出的請求。
簡單請求
簡單請求前面講過是直接發送,只是多加一個origin字段表明跨域請求的來源。
如果Origin指定的源,不在許可范圍內,服務器會返回一個正常的HTTP回應。瀏覽器發現,這個回應的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。注意,這種錯誤無法通過狀態碼識別,因為HTTP回應的狀態碼有可能是200。
如果Origin指定的域名在許可范圍內,服務器返回的響應,會多出幾個頭信息字段。
- Access-Control-Allow-Origin: 允許跨域訪問的域,可以是一個域的列表,也可以是通配符"*"。這里要注意Origin規則只對域名有效,并不會對子目錄有效。即http://foo.example/subdir/ 是無效的。但是不同子域名需要分開設置,這里的規則可以參照同源策略
- Access-Control-Allow-Credentials: 是否允許請求帶有驗證信息,這部分將會在下面詳細解釋
- Access-Control-Expose-Headers: 允許腳本訪問的返回頭,請求成功后,腳本可以在XMLHttpRequest中訪問這些頭的信息(貌似webkit沒有實現這個)
- Access-Control-Max-Age: 緩存此次請求的秒數。在這個時間范圍內,所有同類型的請求都將不再發送預檢請求而是直接使用此次返回的頭作為判斷依據,非常有用,大幅優化請求次數
- Access-Control-Allow-Methods: 允許使用的請求方法,以逗號隔開
- Access-Control-Allow-Headers: 允許自定義的頭部,以逗號隔開,大小寫不敏感
然后瀏覽器通過返回結果的這些控制字段來決定是將結果開放給客戶端腳本讀取還是屏蔽掉。如果服務器沒有配置cors,返回結果沒有控制字段,瀏覽器會屏蔽腳本對返回信息的讀取。
withCredentials
withCredentials是什么?
withCredentials是XMLHttpRequest的一個屬性,表示跨域請求是否提供憑據信息(cookie、HTTP認證及客戶端SSL證明等)
實際中用途就是跨域請求是要不要攜帶cookie
在需要跨域攜帶cookie時,要把withCredentials設置為true,比如
var?xhr?=?new?XMLHttpRequest()
xhr.withCredentials?=?true
xhr.open('GET',?'http://localhost:8888/',?true)
xhr.send(null)
服務端的設置
只有客戶端設置當然不夠了,服務端還需要設置兩點
比如你頁面所在的域名為http://www.abc.com,服務端的Access-Control-Allow-Origin,必須是http://www.abc.com
- Access-Control-Allow-Credentials
在響應頭中,Access-Control-Allow-Credentials這個值也要設置為true,根據mdn上的說法,只有設置為true的時候,瀏覽器才會把響應結果暴露給你的js代碼
- Access-Control-Allow-Origin
既然是跨域請求,服務端要設置Access-Control-Allow-Origin,告訴瀏覽器允許跨域,而且這個值必須指定域名,不能設置為*
盡管瀏覽器可以支持通配符,但是不能同時將憑證標志設置成true。
就像下面這種頭部配置:
Access-Control-Allow-Origin:?*
Access-Control-Allow-Credentials:?true
這樣配置瀏覽器將會報錯,因為在響應具有憑據的請求時,服務器必須指定單個域,所不能使用通配符。簡單的使用通配符將有效的禁用“Access-Control-Allow-Credentials”這個字段。這些限制和行為的結果就是許多CORS的實現方式是根據“Origin”這個頭部字段的值來生成“AccessControl-Allow-Origin”的值
為什么不能兩者共存?
默認情況下,如果沒有設置“Access-Control-Allow-Credentials”這個頭的話,瀏覽器發送的請求就不會帶有用戶的身份數據(cookie或者HTTP身份數據),所以就不會泄露用戶隱私信息。下面這個圖展示一個簡單的CORS請求流:

其實圖片所展示的就是經典的CSRF攻擊。而Origin的限制,是為了明確是哪個站點發送的請求,根據Origin就可以發現是釣魚網站發起的請求。從而避免cookie的泄露。
參考文獻:
CORS通信
跨域資源共享(CORS)安全性淺析
【web前端】withCredentials有什么作用
cors安全完全指南