快速了解JavaScript的跨域

跨域.png

瀏覽器有一個重要的安全策略,就是 同源策略,它用于限制不同源之間資源的交互。能夠幫助阻擋一些惡意的訪問,減少可能被攻擊的媒介。

下面看一下與該 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)建圖片,然后通過它們的 onloadonerror 事件處理程序得知何時收到響應。

這種動態(tài)創(chuàng)建圖片的技術(shù)經(jīng)常用于圖片探測(image pings)。圖片探測是與服務器之間簡單、跨域、單向的通信。數(shù)據(jù)通過查詢字符串發(fā)送,響應可以隨時設置,不過一般是位圖圖片或值為 204 的狀態(tài)碼。瀏覽器通過圖片探測拿不到任何數(shù)據(jù),但可以通過監(jiān)聽 onloadonerror 事件知道什么時候能接收到響應。下面看一個例子:

let img = new Image();
img.onload = img.onerror = function() {
    console.log("已完成!");
};
img.src = "http://www.example.com/test?name=Manoa";

這個例子創(chuàng)建了一個新的 Image 實例,然后給 onloadonerror 事件添加同一個函數(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 請求。例如,XMLHttpRequestFetch API 遵循同源策略。這意味著使用這些 API 的 Web 應用程序只能從加載應用程序的同一個域請求 HTTP 資源,而 CORS 是 HTTP 的一部分,它允許服務端來指定哪些主機可以從這個服務端加載資源,因此要響應報文中要包含 CORS 響應頭。

簡單請求

不會觸發(fā) CORS 預檢請求的請求稱為 “簡單請求”。“簡單請求” 的請求方式是 GET、HEADPOST三種之一,首部字段主要是以下字段集合:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type:只限 text/plainmultipart/form-dataapplication/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 對象允許訪問 statusstatusText 屬性,也允許同步請求。出于安全考慮,跨域 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-HeaderX-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)注公眾號「海人為記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容