
瀏覽器有一個重要的安全策略,就是 同源策略,它用于限制不同源之間資源的交互。能夠幫助阻擋一些惡意的訪問,減少可能被攻擊的媒介。
下面看一下與該 URL http://chat.example.com/u1/arts1/101011.html 的源進行對比的示例。
| URL | 結(jié)果 | 原因 |
|---|---|---|
http://chat.example.com/u2/arts1/101012.html |
同源 | 路徑不同 |
http://chat.example.com/u1/arts2/101013.html |
同源 | 路徑不同 |
https://chat.example.com/u2/arts2/101014.html |
失敗 | 協(xié)議不同 |
http://chat.example.com:88/u1/arts1/101015.html |
失敗 | 端口不同(http:// 默認80端口) |
http://news.example.com/u3/arts4/101017.html |
失敗 | 主機不同 |
因此,默認情況下使用 XMLHttpRequest 進行 Ajax 通信,不能訪問除它自己以外的域、協(xié)議和端口。但瀏覽器也需要有合法跨源訪問的能力。
圖片探測
圖片探測是利用 <img> 標簽實現(xiàn)跨域通信的最早的一種技術(shù)。任何頁面都可以跨域加載圖片而不必擔心限制,因此這也是在線廣告跟蹤的主要方式??梢詣討B(tài)創(chuàng)建圖片,然后通過它們的 onload 和 onerror 事件處理程序得知何時收到響應。
這種動態(tài)創(chuàng)建圖片的技術(shù)經(jīng)常用于圖片探測(image pings)。圖片探測是與服務器之間簡單、跨域、單向的通信。數(shù)據(jù)通過查詢字符串發(fā)送,響應可以隨時設置,不過一般是位圖圖片或值為 204 的狀態(tài)碼。瀏覽器通過圖片探測拿不到任何數(shù)據(jù),但可以通過監(jiān)聽 onload 和 onerror 事件知道什么時候能接收到響應。下面看一個例子:
let img = new Image();
img.onload = img.onerror = function() {
console.log("已完成!");
};
img.src = "http://www.example.com/test?name=Manoa";
這個例子創(chuàng)建了一個新的 Image 實例,然后給 onload 和 onerror 事件添加同一個函數(shù)。這樣可確保請求完成時,無論成否都會收到通知。設置完 src 屬性之后請求就會開始,這個例子向服務器發(fā)送了一個 name 值。
圖片探測頻繁用于跟蹤用戶在頁面上的點擊操作或動態(tài)顯示廣告。當然,圖片探測的缺點是只能發(fā)送 GET 請求和無法獲取服務器響應的內(nèi)容。這也是只能利用探測實現(xiàn)瀏覽器與服務器單向通信的原因。
JSONP
JSONP(JSON with padding)是 JSON 的一種變體。用于解決瀏覽器的跨域問題。需要動態(tài)創(chuàng)建 <script> 元素并為 src 屬性指定跨域 URL,并將一個回調(diào)函數(shù)指定在 URL 后面。這樣能夠不受限制地從其它域加載資源。下面是一個 JSONP 的 URL:
http://example.com/json/?callback=handleResponse
而想要發(fā)送這樣的 HTTP 請求,我們就需要實現(xiàn)如下腳本:
function handleResponse(response) {
console.log(`
You're at IP address ${response.ip}, which is in
${response.city}, ${response.region_name}`);
}
let script = document.createElement("script");
script.src = "http://example.com/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
JSONP 由于其簡單易用,在開發(fā)者中非常流行。相比圖片探測,使用 JSONP 可以直接訪問響應,實現(xiàn)瀏覽器與服務器的雙向通信。不過 JSONP 也有一些缺點。
首先,JSONP 是從不同的域拉取可執(zhí)行代碼。如果這個域并不可信,則可能在響應中加入惡意內(nèi)容。此時除了完全刪除 JSONP 沒有其它辦法。在使用不受控的 Web 服務時,一定要保證是可以信任的。
第二個缺點是不好確定 JSONP 請求是否失敗。雖然 HTML5 規(guī)定了 <script> 元素的 onerror 事件處理程序,但還沒有被任何瀏覽器實現(xiàn)。為此,開發(fā)者經(jīng)常使用計數(shù)器來決定是否放棄等待響應。這種方式并不準確,畢竟不同用戶的網(wǎng)絡連接速度和帶寬是不一樣的。
CORS 出現(xiàn)之前,實現(xiàn)跨源 Ajax 通信是有點麻煩的。開發(fā)者需要依賴能夠執(zhí)行跨源請求的 DOM 特性,在不使用 XMLHttpRequest 對象情況下發(fā)送某種類型的請求。雖然 CORS 目前已經(jīng)得到廣泛支持,但這些技術(shù)仍然沒有過時,因為它們不需要修改服務器。
跨源資源共享
在 CORS 出現(xiàn)之前,實現(xiàn)跨源 Ajax 通信有點麻煩。開發(fā)者需要依賴能夠執(zhí)行跨源請求的 DOM 特性,在不使用 XMLHttpRequest 的情況下發(fā)送某種類型的請求。而 CORS 跨域出現(xiàn)之后,就比較方便了,但也不是說上面介紹的技術(shù)已經(jīng)過時,因為它們不需要修改服務的緣故,還是有使用的場景。下面介紹下 CORS。
跨源資源共享(CORS,Cross-Origin Resource Sharing)是一種基于 HTTP 頭的機制,該機制通過允許服務器自定義 HTTP 頭部允許瀏覽器與服務器實現(xiàn)跨源通信。
出于安全性,瀏覽器限制腳本內(nèi)發(fā)起的跨源 HTTP 請求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。這意味著使用這些 API 的 Web 應用程序只能從加載應用程序的同一個域請求 HTTP 資源,而 CORS 是 HTTP 的一部分,它允許服務端來指定哪些主機可以從這個服務端加載資源,因此要響應報文中要包含 CORS 響應頭。
簡單請求
不會觸發(fā) CORS 預檢請求的請求稱為 “簡單請求”。“簡單請求” 的請求方式是 GET、HEAD 或 POST三種之一,首部字段主要是以下字段集合:
AcceptAccept-LanguageContent-Language-
Content-Type:只限text/plain、multipart/form-data和application/x-www-form-urlencoded三者之一。
沒有自定義頭部,請求在發(fā)送時會有一個額外的 Origin 頭部,其包含發(fā)送請求的頁面的源,以便服務器確實是否為其提供響應。下面是 Origin 頭部的一個示例:
Origin: http://www.example.com
如果服務器決定響應請求,那么應該發(fā)送 Access-Control-Allow-Origin 頭部,包含相同的源;或者如果資源是公開的,那么就包含 "*"。比如:
Access-Control-Allow-Origin: http://www.example.com
如果沒有這個頭部,或者有但源不匹配,則表明不會響應瀏覽器請求。否則,服務器就會處理這個請求。注意,無論請求還是響應都不會包含 cookie 信息。
現(xiàn)代瀏覽器通過 XMLHttpRequest 對象原生支持 CORS。在嘗試訪問不同源的資源時,這個行為會被自動觸發(fā)。要向不同域的源發(fā)送請求,可以使用標準 XMLHttpRequest 對象并給 open() 方法傳入一個絕對 URL,比如:
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.log("請求失敗: " + xhr.status);
}
}
};
xhr.open("get", "http://www.examplecom/page/", true);
xhr.send(null);
跨域 XMLHttpRequest 對象允許訪問 status 和 statusText 屬性,也允許同步請求。出于安全考慮,跨域 XMLHttpRequest 對象也施加了一些額外限制。
- 不能使用
setRequestHeader()設置自定義頭部。 - 不能發(fā)送和接收
cookie。 -
getAllResponseHeaders()方法始終返回空字符串。
因為無論同域還是跨域請求都使用同一個接口,所以最好在訪問本地資源時使用相對 URL,在訪問遠程資源時使用絕對 URL。這樣可以更明確地區(qū)分使用場景,同時避免出現(xiàn)訪問本地資源時出現(xiàn)頭部或 cookie 信息訪問受限的問題。
預檢請求
CORS 通過一種預檢請求(preflighted request)的服務器驗證機制,允許使用自定義頭部、除 GET 和 POST 之外的方法,以及不同請求體內(nèi)容類型。在要發(fā)送涉及上述某種高級選項的請求時,會先向服務器發(fā)送一個 “預檢” 請求。這個請求使用 OPTIONS 方法發(fā)送并包含以下頭部。
-
Origin:與簡單請求相同。 -
Access-Control-Request-Method:請求希望使用的方法。 -
Acces-Control-Request-Headers:(可選)要使用的逗號分隔的自定義頭部列表。
下面是一個假設的 POST 請求,包含自定義的 NCZ 頭部:
Origin: http://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
在這個請求發(fā)送后,服務器可以確定是否允許這種類型的請求。服務器會通過在響應中發(fā)送如下頭部與瀏覽器溝通這些信息。
-
Access-Control-Allow-Origin:與簡單請求相同。 -
Access-Control-Allow-Methods:允許的方法(逗號分隔的列表)。 -
Access-Control-Allow-Headers:服務器允許的頭部(逗號分隔的列表)。 -
Access-Control-Max-Age:緩存預檢請求的秒數(shù)。
例如:
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
預檢請求返回后,結(jié)果會按響應指定的時間緩存一段時間。換句話說,只有第一次發(fā)送這種類型的請求時才會多發(fā)送一次額外的 HTTP 請求。“預檢請求” 的使用,可以避免跨域請求對服務器的用戶數(shù)據(jù)產(chǎn)生未預期的影響。
憑據(jù)請求
默認情況下,跨源請求不提供憑據(jù)(cookie、HTTP 認證和客戶端 SSL 證書)??梢酝ㄟ^將 withCredentials 屬性設置為 true 來表明請求會發(fā)送憑據(jù)。如果服務器允許帶憑據(jù)的請求,那么可以在響應中包含如下 HTTP 頭部:
Access-Control-Allow-Credentials: true
如果發(fā)送了憑據(jù)請求而服務器返回的響應中沒有這個頭部,則瀏覽器不會把響應交給 JavaScript(responseText 是空字符串,status 是 0,onerror() 被調(diào)用)。注意,服務器也可以在預檢請求的響應中發(fā)送這個 HTTP 頭部,以表明這個源允許發(fā)送憑據(jù)請求。
HTTP 響應首部字段
下面列出規(guī)范所定義的響應首部字段。
Access-Control-Expose-Headers
允許訪問該資源的外域 URI。<origin> 參數(shù)的值是允許的外域。而設置成通配符*則是服務器允許來自所有域的請求。
Access-Control-Allow-Origin: <origin> | *
Access-Control-Expose-Headers
服務器把允許瀏覽器訪問的頭放入白名單,瀏覽器中的XMLHttpRequest就可以通過getResponseHeader訪問響應頭的 X-My-Custom-Header 和 X-Another-Custom-Header。
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
Access-Control-Max-Age
指定了預檢請求的結(jié)果能夠被緩存的秒數(shù)。
Access-Control-Max-Age: <delta-seconds>
Access-Control-Allow-Credentials
指定了當瀏覽器的 credentials 設置為 true 時是否允許瀏覽器讀取 response 的內(nèi)容。當用在對預檢請求的響應中時,它指定了實際的請求是否可以使用 credentials。
Access-Control-Allow-Credentials: true
注意:簡單 GET 請求不會被預檢;如果對此類請求的響應中不包含該字段,響應將被忽略,并且瀏覽器也不會將相應內(nèi)容返回給網(wǎng)頁。
Access-Control-Allow-Methods
首部字段用于預檢請求的響應。指明了實際請求所允許使用的 HTTP 方法。
Access-Control-Allow-Methods: <method>[, <method>]*
Access-Control-Allow-Headers
首部字段用于預檢請求的響應。其指明了實際請求中允許攜帶的首部字段。
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
HTTP 請求首部字段
下面列出規(guī)范所定義的可用于發(fā)起跨源請求的首部字段。注意:這些首部字段無須手動設置。使用 XMLHttpRequest 時會自動設置。
Origin
表明預檢請求或?qū)嶋H請求的源(協(xié)議、域名和端口)。
Origin: <origin>
origin 參數(shù)的值為源站 URI。它不包含任何路徑信息,只是服務器名稱。注意:在所有訪問控制請求(Access control request)中,Origin 首部字段總是被發(fā)送。
Access-Control-Request-Method
用于預檢請求。其作用是,將實際請求所使用的 HTTP 方法告訴服務器。
Access-Control-Request-Method: <method>
Access-Control-Request-Headers
用于預檢請求。其作用是,將實際請求所攜帶的首部字段告訴服務器。
Access-Control-Request-Headers: <field-name>[, <field-name>]*
總結(jié)
XMLHttpRequest 不允許訪問其它域名、協(xié)議和端口的資源,訪問的話會導致安全錯誤,這是因為瀏覽器的同源策略所造成的。而想要跨源訪問,就需要使用跨源資源共享方案,XMLHttpRequest原生支持CORS。圖片探測和 JSONP 是另兩種跨域通信技術(shù),但與 CORS 相比,不是很可靠。
更多內(nèi)容請關(guān)注公眾號「海人為記」