一日一練-JS 了解幾種跨域技術
子曰:了解幾種跨域機制
簡單介紹
首先簡單了解一下同源策略相關知識點:
1.同源策略 限制了從一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要機制。
2.源的定義:如果兩個頁面的協議、端口和域名都相同,則兩個頁面具有相同的 源
3.同源策略規定,是XHR 實現Ajax 通信的一個主要限制。默認情況下,XHR 對象只能訪問與包含它的頁面位於同一個域中的資源。這種安全策略可以預防某些惡意行為。但是,實現合理的跨域請求對開發某些瀏覽器應用程序也是至關重要的。
下面是幾種跨域技術。
CORS
0x00:定義
CORS(Cross-Origin Resource Sharing,跨域資源共享)是W3C 的一個工作草案,定義了在必須訪問跨域資源時,瀏覽器與服務器應該如何溝通。CORS 背後的基本思想,就是使用自定義的HTTP 頭部讓瀏覽器與服務器進行溝通,從而決定請求或響應是應該成功,還是應該失敗。
比如一個簡單的使用GET 或POST 發送的請求,它沒有自定義的頭部,而主體內容是text/plain
Origin
頭部,其中包含請求頁面的源信息(協議、域名和端口),以便服務器根據這個頭部信息來決定是否給與響應。下面是Origin
頭部的應該示例:
Origin: http://www.example.com
如果服務器任務這個請求可以接受,就在Access-Control-Allow-Origin
頭部中回發相同的源信息(如果是公共資源,可以回發*
)。例如:
Access-Control-Allow-Origin: http://www.example.com
如果沒有這個頭部,或者有這個頭部但源信息不匹配,瀏覽器就會駁回請求。正常情況下,瀏覽器會處理請求。註意,請求和響應都不包含cookie 信息。
0x01:現代瀏覽器對CORS 的實現
Webkit 內核的現代瀏覽器都通過XMLHttpRequest
對象實現了對CORS 的原生支持。在嘗試打開不同來源的資源時,無需額外編寫代碼就可以觸發這個行為。
var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ alert(xhr.responseText) } else { alert(‘Request was unsuccessful: ‘ + xhr.status) } } } xhr.open(‘get‘, ‘http://www.example.com/page/‘, true) xhr.send(null)
跨域XHR 對象有一些安全限制
1.不能使用setRequestHeader()
設置自定義頭部。
2.不能發送和接收cookie
3.調用getAllResponseHeader()
方法總會返回空字符串。
0x02:Preflighted Requests(預檢請求)
CORS 通過一種叫做Preflighted Requests 的透明服務器機制支持開發人員使用自定義的頭部、GET 或POST 之外的方法,以及不同類型的主體內容。在使用下列高級選項發送請求時,就會向服務器發送一個Preflight 請求。這種請求使用OPTIONS 方法,發送下列頭部。
1.Origin:與簡單的請求相同。
2.Access-Control-Request-Method:請求自身使用的方法。
3.Access-Control-Request-Headers:(可選)自定義的頭部信息,多個頭部以逗號分割。
下面是一個帶有自定義頭部NCZ 的使用POST 發送的請求
Origin: http://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
發送這個請求後,服務器可以決定是否允許這種類型的請求。服務器通過在響應中發送如下頭部與瀏覽器進行溝通。
1.Access-Control-Allow-Origin:與簡單的請求相同。
2.Access-Control-Allow-Methods:允許的方法,多個方法以逗號分隔。
3.Access-Control-Allow-Headers:允許的頭部,多個頭部以逗號分隔。
4.Access-Control-Max-Age:應該將這個Preflight 請求緩存多長時間(以秒表示)。
例如:
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Mag-Age:1728000
Preflight 請求結束後,結果將按照響應中指定的時間緩存起來。而為此付出的代價只是第一次發送這種請求時會多一次HTTP 請求。
0x03:帶憑據的請求
默認情況下,跨源請求不提供憑據(cookie,HTTP 認證及客戶端SSL 證明等)。通過將withCredentials
屬性設置為true
,可以指定某個請求應該發送憑據。如果服務器接受帶憑據的請求,會用下面的HTTP 頭部來響應。
Access-Contol-Allow-Credentials: true
如果發送的是帶憑據的請求,但服務器的響應中沒有包含這個頭部,那麽瀏覽器就不會把響應交給JavaScript(於是,responseText 中將是空字符串,status 的值為0,而且會調用onerror() 事件處理程序)。另外,服務器還可以在Preflight 響應中發送這個HTTP 頭部,表示允許源發送帶憑據的請求。
圖像Ping
0x00:定義
圖像Ping 是與服務器進行簡單、單向的跨域通信的一種方式。請求的數據是通過查詢字符串形式發送的,而響應可以是任意內容,但通常是像素圖或204 響應。通過圖像Ping,瀏覽器得不到任何具體的數據,但通過偵聽load 和error 事件,它能知道響應是什麽時候收到。
0x01:例子
var img = new Image()
img.onload = img.onerror = function () {
alert(‘Done!‘)
}
img.src = ‘http://www.example.com/test?name=Nicholas‘
這裏創建了一個Image 實例,然後將onload 和onerror 事件處理程序指定為同一個函數。這樣無論是什麽響應,只要請求完成,就能得到通知。請求從設置src 屬性那一刻開始,而這個例子在請求中發送了一個name 參數。
0x02: 作用
圖像Ping 最常用於跟蹤用戶點擊頁面或動態廣告曝光次數。圖像Ping 有兩個主要的缺點,一是只能發送GET 請求,而是無法訪問服務器的響應文本。因此,圖像Ping 只能用於瀏覽器與服務器間的簡單通信。
JSONP
0x00:定義
JSONP 是JSON with Padding(填充式JSON或參數式JSON)的簡寫,是應用JSON 的一種新方法。JSONP 看起來與JSON 差不多,只不過是被包含在函數調用中的JSON。
callback({"name": "Nicholas"})
JSONP 由兩部分組成:回調函數和數據。回調函數是當響應到來時應該在頁面中調用的函數。回調函數的名字一般是在請求中指定的。而數據就是傳入回調函數中的JSON 數據。
0x01:例子
這是應該典型的JSONP 請求
http://freegeoio.net/json/?callback=handleResponse
這個URL 是在請求應該JSONP 地理定位服務。通過查詢字符串來指定JSONP 服務的回調參數,這裏指定的回調函數的名字叫handleResponse()
JSONP 是通過動態創建<script>
元素來使用的,使用時可以為src
屬性指定應該跨域URL。<script>
元素和<img>
元素都有能力不受限制地從其他域加載資源。因為JSONP 是有效的JavaScript 代碼,所以在請求完成後,即在JSONP 響應加載到頁面中以後,就會立即執行。
function handleResponse (response) {
alert("You‘re at IP address " + reponse.ip + ", which is in " +
response.city + ", " + response.region_name)
}
var script = document.createElement(‘script‘)
script.src = ‘http://freegeoio.net/json/?callback=handleResponse‘
document.body.insertBefore(script, document.body.firstChild)
0x02: 作用
與圖像Ping 相比,優點在於能夠直接訪問響應文本,支持在瀏覽器與服務器之間雙向通信。
缺點:需要確保其他域的安全可靠,以及確定JSONP 請求是否失敗並不容易。
Comet
0x00:定義
Comet 指的一種更高級的Ajax 技術(也被稱為“服務器推送”)。Ajax 是一種從頁面向服務器請求數據的技術,而Comet 則是一種服務器向頁面推送數據的技術。Comet 能夠讓信息近乎實時地被推送到頁面上,非常適合處理體育比賽的分數和股票報價。
0x01:實現
有兩種實現Comet 的方式:長輪詢和流。
1.短輪詢:瀏覽器定時向服務器發送請求,看看有沒有更新的數據。下圖是短輪詢的時間線
2.長輪詢:頁面發生器一個服務器的請求,然後服務器一直保持連接打開知道有數據可發送。發送完數據之後,瀏覽器關閉連接,隨即由發起一個到服務器的新請求。這一過程在頁
面打開期間一直保持不斷。下面是長輪詢的時間線
3.HTTP流:流不同於輪詢,因為它在頁面的整個生命周期內只使用一個HTTP 連接。具體來說,就是瀏覽器向服務器發送一個請求,而服務器保持連接打開,然後周期性地向瀏覽器發送數據。
所有服務器端語言都支持打印到輸出緩存然後刷新(將輸出緩存中的內容一次性全部發送到客戶端)的功能。而這正是實現HTTP 流的關鍵所在。
在現代瀏覽器中,通過偵聽readstatechange
事件以及檢測readyState
的值是否為3,就可以利用XHR 對象實現HTTP 流。隨著不斷從服務器接收數據,readState
的值就會周期性地變為3。當readyState
值變為3 時,responseText
屬性中會保存接收到的所有數據。
服務器發送事件
0x00:定義
SSE(Server-Sent Events,服務器發送事件)是圍繞只讀Comet 交互推出的API 或者模式。SSE API 用於創建到服務器的單向連接,服務器通過這個連接可以發送任意數量的數據。服務器響應的MIME 類型必須是text/event-stream
,而且是瀏覽器中的JavaScript API能解析格式輸出。
0x01: SSE API
SSE 的JavaScript API 與其他傳遞消息的JavaScript API 很相似。要預定新的事件流,首先要創建一個新的EventSource
對象,並傳進一個入口點:
var source = new EventSource(‘myevents.php‘)
註意,傳入的URL 必須與創建對象的頁面同源(相同的URL 模式、域及端口)。EventSource
的實例有一個readyState
屬性,值為0 表示正連接到服務器,值為1 表示打開連接,值為2 表示關閉連接。
另外,還有以下三個事件。
1.open
:在建立連接時觸發。
2.message
:在從服務器接收到新事件時觸發。
3.error
:無法建立連接時觸發。
onmessage
事件處理程序的使用
source.onmessage = function (event) {
var data = event.data
// 處理數據
}
服務器發回的數據以字符串形式保存在event.data
中。
默認情況下,EventSource
對象會保持與服務器的活動連接。如果連接斷開,還會重新連接。這就意味著SSE 適合長輪詢和HTTP 流。如果想強制立即斷開連接並且不再重新連接,可以調用close()
方法。
source.close()
0x02:事件流
所謂的服務器事件會通過一個持久的HTTP 響應發送,這個響應的MIME 類型為text/event-stream
。響應的格式是純文本,最簡單的情況是每個數據項都帶有前綴data:
,例如:
data: foo
data: bar
data: foo
data: bar
對以上響應而言,事件流中的第一個message
事件返回的event.data
值為foo
,第二個message
事件返回的event.data
值為bar
,第三個message
事件返回的event.data
值為foo\nbar
(註意中間的換行符)。對於多個連續的以data:
開頭的數據行,將作為多段數據解析,每個值之間以一個換行符分割。只能在包含data:
的數據行後面有空行時,才會觸發message
事件,因此在服務器上生成事件流時不能忘了多添加這一行。
通過id:
前綴可以給特定的事件指定一個關聯的ID,這個ID 行位於data:
行前面或皆可:
data: foo
id: 1
設置了ID 後,EventSource
對象會跟蹤上一次觸發的事件。如果連接斷開,會向服務器發送一個包含名為Last-Event-ID
的特殊HTTP 頭部的請求,以便服務器知道下一次該觸發哪個事件。在多次連接的事件流中,這種機制可以確保瀏覽器以正確的順序收到連接的數據段。
Web Sockets
0x00:定義
Web Sockets 的目標是在一個單獨的持久連接上提供全雙工、雙向通信。在JavaScript 中創建了Web Socket 之後,會有一個HTTP 請求發送到瀏覽器以發起連接。在取得服務器響應後,建立的連接會使用HTTP 升級為HTTP 協議交換為Web Socket 協議。也就是說,使用標準的HTTP 服務器無法實現Web Sockets,只有支持這種協議的專門服務器才能正常工作。
由於Web Sockets 使用了自定義的協議,所以URL 模式也略有不同。未加密的連接不再是http://
而是ws://
;加密的連接也不是https://
而是wss://
。
使用自定義協議而非HTTP 協議的好處是,能夠在客戶端和服務器之間發送非常少量的數據,由於傳遞的數據包很小,因此Web Sockets 非常適合移動應用。
0x01:Web Sockets API
要創建Web Socket,先實例一個WebSocket 對象並傳入要連接的URL:
var socket = new WebSocket(‘ws://www.example.com/server.php‘)
註意,必須給WebSocket 構造函數傳入絕對URL。同源策略對Web Sockets 不適用,因此可以通過它打開到任何站點的連接。
實例化了WebSocket 對象後,瀏覽器就會馬上嘗試連接。WebSocket 也有一個表示當前狀態的readyState
屬性。
1.WebSocket.OPENING(0):正在建立連接。
2.WebSocket.OPEN(1):已經建立連接。
3.WebSocket.CLOSING(2):正在關閉連接。
4.WebSocket.CLOSE(3):已經關閉連接。
readyState
的值永遠從0 開始。
要關閉Web Socket 連接,可以在任何時候調用close()
方法。
socket.close()
調用了close()
之後,readyState
的值立即變為2(正在關閉),而在關閉連接後就會變成3。
0x02:發送和接收數據
Web Socket 打開之後,就可以通過連接發送和接收數據。要向服務器發送數據,使用send()
方法並傳入任意字符串,例如:
var socket = new WebSocket(‘ws://www.example.com/server.php‘)
socket.send(‘Hello world!‘)
因為Web Socket 只能通過連接發送純文本數據,所以對於復雜的數據結構,在通過連接發送之前,必須進行序列化。下面的例子展示了先將數據序列化為一個JSON 字符串,然後再發送到服務器:
var message = {
time: new Date(),
text: ‘Hello world!‘,
clientId: ‘asdfp‘
}
socket.send(JSON.stringify(message))
接下來,服務器要讀取其中的數據,就要解析接收到的JSON 字符串。
當服務器向客戶端發來消息時,WebSocket 對象就會觸發message
事件。這個message
事件與其他傳遞消息的協議類似,也是把返回的數據保存在event.data
屬性中。
socket.onmessage = function (event) {
var data = event.data
// 處理數據
}
與通過send()
發送到服務器的數據一樣,event.data
中返回的數據也是字符串。如果你想要得到其他格式的數據,必須手動解析這些數據。
0x03:其他事件
WebSocket 對象還有其他三個事件,在建立生命周期的不同階段觸發。
1.open: 在成功建立連接時觸發。
2.error: 在發生錯誤時觸發,連接不能持續。
3.close: 在連接關閉時觸發。
WebSocket 對象不支持DOM 2 級事件偵聽器,因此必須使用DOM 0 級語法分別定義每個事件處理程序。
var socket = new WebSocket(‘ws://www.example.com/server.php‘)
socket.onopen = function () {
alert(‘Connection established.‘)
}
socket.onerror = function () {
alert(‘Connection error.‘)
}
socket.onclose = function () {
alert(‘Connection closed.‘)
}
在這三個事件中,只有close
事件的event
對象有額外的信息。這個事件的事件對象有三個額外的屬性:wasClean
、code
和reason
。其中,wasClean
是一個布爾值,表示連接是否已經明確地關閉;code
是服務器返回的數值狀態碼;而reason
是一個字符串,包含服務器發回的消息。可以把這些信息顯示給用戶,也可以記錄到日誌中以便將來分析。
socket.onclose = function (event) {
console.log(‘Was clean? ‘ + event.wasClean + " Code=" + event.code + " Reason=" + event.reason)
}
一日一練-JS 了解幾種跨域技術