「同源政策」(same-origin policy)javascript
瀏覽器安全的基石
html
1995年,同源政策由 Netscape 公司引入瀏覽器。目前,全部瀏覽器都實行這個政策前端
目前,若是非同源,共有如下三項受到限制java
經過 JavaScript 腳本能夠拿到其餘窗口的 window 對象。若是是非同源的網頁,目前容許一個窗口能夠接觸其餘網頁的window對象的九個屬性和四個方法web
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()json
目的: 保證用戶信息的安全,防止惡意的網站竊取數據
api
Cookie 是服務器寫入瀏覽器的一小段信息,只有同源的網頁才能共享。跨域
只適用於 Cookie 和 iframe 窗口瀏覽器
若是兩個網頁一級域名相同,只是次級域名不一樣,瀏覽器容許經過設置 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 的時候,指定 Cookie 的所屬域名爲一級域名,好比.example.com
Set-Cookie: key=value; domain=.example.com; path=/
這樣的話,二級域名 和 三級域名 不用作任何設置,均可以讀取這個 Cookie
對於徹底不一樣源的網站,目前有兩種方法,能夠解決跨域窗口的通訊問題
1. 片斷識別符(fragment identifier)
片斷標識符(fragment identifier)指的是,URL 的 # 號後面的部分
好比 http://example.com/x.html#fragment 的 #fragment。若是隻是改變片斷標識符,頁面不會從新刷新
hashchange
事件獲得通知父窗口
var src = originURL + '#' + data; document.getElementById('myIFrame').src = src;
子窗口 iframe
window.onhashchange = checkMessage; function checkMessage() { var message = window.location.hash; // ... }
一樣的,子窗口也能夠改變父窗口的片斷標識符
parent.location.href = target + '#' + hash;
上面的這種方法屬於破解,HTML5 爲了解決這個問題,引入了一個全新的API:跨文檔通訊 API(Cross-document messaging)
2. 跨文檔通訊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');
子窗口向父窗口發送消息的寫法相似
// 子窗口向父窗口發消息 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 屬性引用父窗口,而後發送消息
window.addEventListener('message', receiveMessage); function receiveMessage(event) { event.source.postMessage('Nice to see you!', '*'); }
注意:
一般來講,這兩種作法是不推薦的,由於不夠安全,可能會被惡意利用
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); } }
第一個參數 ---- 具體的信息內容
第二個參數 ---- 接收消息的窗口的源(origin),即「協議 + 域名 + 端口」。也能夠設爲 * ,表示不限制域名,向全部窗口發送
經過 window.postMessage,讀寫其餘窗口的 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); localStorage.setItem(payload.key, JSON.stringify(payload.data)); };
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); };
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; } };
解決 AJAX 請求(服務器與客戶端 交互) 跨域的 三種方法
JSONP 只能發 GET 請求
簡單適用,老式瀏覽器所有支持,服務端改造很是小
網頁經過動態插入一個 <script>
元素,向服務器請求 JSON 數據 ---- 這種 通常請求 的作法不受同源政策限制
服務器收到請求後,將數據放在一個指定名字的回調函數裏傳回來 ---- foo({"ip": "192.168.3.31"})
因爲 <script>
元素請求的腳本,直接做爲代碼運行
只要瀏覽器定義了 foo
函數,foo 函數就會當即調用。做爲參數的 JSON 數據 被視爲 JavaScript 對象
使用:
前端頁面
function foo(data) { console.log('Your public IP address is: ' + data.ip); }; 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'); }
後臺服務器
WebSocket 通訊協議
WebSocket 是一種通訊協議,使用 ws://(非加密)和 wss://(加密)做爲協議前綴
該協議不實行同源政策,幾乎全部的瀏覽器都支持,因此只要服務器支持,就能夠經過它進行跨源通訊
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 這個字段,判斷是否許可本次通訊。
若是該域名在白名單內,服務器就會作出以下回應
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
CORS 跨源資源分享(Cross-Origin Resource Sharing) 運行任何類型的請求
是一個W3C 標準,也是 跨源 AJAX 請求 的根本解決方法
整個 CORS 通訊過程,都是瀏覽器自動完成
CORS 須要瀏覽器和服務器同時支持。目前,全部瀏覽器都支持該功能
實現 CORS 通訊的關鍵是服務器。因此,只要服務器實現了 CORS 接口,就能夠跨域通訊
XMLHttpRequest
請求,從而克服了 AJAX 只能同源使用的限制沒有差異,代碼徹底同樣
瀏覽器一旦發現 AJAX 請求跨域,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感知
CORS 請求分紅兩類
同時知足如下兩大條件
請求方法是 GET、POST、HEAD 其中之一
HTTP 的頭信息不超出如下幾種字段
xAccept
Accept-Language
Content-Language
Last-Event-ID
Content-Type 只限於三個值 application/x-www-form-urlencoded、multipart/form-data、text/plain
這樣劃分的緣由是,表單在歷史上一直能夠跨域發出請求。
簡單請求就是表單請求,瀏覽器沿襲了傳統的處理方式,不把行爲複雜化
對於非簡單請求,瀏覽器會採用新的處理方式
瀏覽器直接發出 CORS 請求
具體來講,就是在頭信息之中,增長一個 Origin
字段
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 指定的源,不在許可範圍內服務器會返回一個正常的 HTTP 迴應
瀏覽器發現,這個迴應的頭信息沒有包含 Access-Control-Allow-Origin 字段(詳見下文)就知道出錯了
從而拋出一個錯誤,被 XMLHttpRequest 的 onerror 回調函數捕獲
注意,這種錯誤沒法經過狀態碼識別,由於 HTTP 迴應的狀態碼有多是200
若是Origin指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段
該字段是必須的
值要麼是請求時 Origin 字段的值;要麼是一個*,表示接受任意域名的請求
該字段可選
值是一個布爾值,表示是否容許發送 Cookie
默認狀況下 false,Cookie 不包括在 CORS 請求之中
默認不包含 Cookie 信息(以及 HTTP 認證信息等),這是爲了下降 CSRF 攻擊的風險
設爲 true,即表示服務器明確許可,瀏覽器能夠把 Cookie 包含在請求中,一塊兒發給服務器
某些場合,服務器可能須要拿到 Cookie
1. 這時須要服務器顯式指定 Access-Control-Allow-Credentials
字段,告訴瀏覽器能夠發送 Cookie
Access-Control-Allow-Credentials: true
2. 同時,開發者必須在 AJAX 請求中打開 withCredentials
屬性
不然,即便服務器要求發送 Cookie,瀏覽器也不會發送
或者說,服務器要求設置 Cookie,瀏覽器也不會處理
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
注意:
若是服務器要求瀏覽器發送 Cookie,Access-Control-Allow-Origin 就不能設爲星號,必須指定明確的、與請求網頁一致的域名
同時,Cookie 依然遵循同源政策,只有用服務器域名設置的 Cookie 纔會上傳
其餘域名的 Cookie 並不會上傳,且(跨域)原網頁代碼中的 document.cookie 也沒法讀取服務器域名下的 Cookie
這個值也只能設爲 true,若是服務器不要瀏覽器發送 Cookie,不發送該字段便可
該字段可選
CORS 請求時,XMLHttpRequest 對象的 getResponseHeader() 方法只能拿到6個服務器返回的基本字段
Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。
若是想拿到其餘字段,就必須在 Access-Control-Expose-Headers 裏面指定
下面的例子指定,getResponseHeader('FooBar')能夠返回FooBar字段的值
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
是那種對服務器提出特殊要求的請求
好比 請求方法是 PUT
或 DELETE
,或者 Content-Type
字段的類型是 application/json
會在正式通訊以前,增長一次 HTTP 查詢請求,稱爲「預檢」請求 (preflight)
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些 HTTP 動詞和頭信息字段
只有獲得確定答覆,瀏覽器纔會發出正式的 XMLHttpRequest 請求,不然就報錯
這是爲了防止這些新增的請求,對傳統的沒有 CORS 支持的服務器造成壓力,給服務器一個提早拒絕的機會
這樣能夠防止服務器收到大量 DELETE 和 PUT 請求,這些傳統的表單不可能跨域發出的請求
舉個例子:
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
OPTIONS
,表示這個請求是用來詢問的Origin
,表示請求來自哪一個源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...
預檢請求的迴應
服務器收到 「預檢」 請求之後
檢查了 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能夠請求數據
該字段也能夠設爲星號,表示贊成任意跨源請求
若是服務器否認了「預檢」請求
會返回一個正常的 HTTP 迴應
可是沒有任何 CORS 相關的頭信息字段,或者明確表示請求不符合條件
OPTIONS http://api.bob.com HTTP/1.1 Status: 200 Access-Control-Allow-Origin: https://notyourdomain.com Access-Control-Allow-Method: POST
這時,瀏覽器就會認定,服務器不一樣意預檢請求,所以觸發一個錯誤,被 XMLHttpRequest 對象的 onerror 回調函數捕獲
控制檯會打印出以下的報錯信息
Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000 XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
Access-Control-Allow-Methods
該字段必需,它的值是逗號分隔的一個字符串,代表服務器支持的全部跨域請求的方法。
注意,返回的是全部支持的方法,而不單是瀏覽器請求的那個方法。這是爲了不屢次「預檢」請求
Access-Control-Allow-Headers
若是瀏覽器請求包括 Access-Control-Request-Headers 字段,則 Access-Control-Allow-Headers 字段是必需的
它也是一個逗號分隔的字符串,代表服務器支持的全部頭信息字段,不限於瀏覽器在「預檢」中請求的字段
Access-Control-Allow-Credentials
含義與同簡單請求相同
Access-Control-Max-Age
該字段可選,用來指定本次預檢請求的有效期,單位爲秒
上面結果中,有效期是20天(1728000秒)即容許緩存該條迴應1728000秒(即20天),在此期間,不用發出另外一條預檢請求
一旦服務器經過了「預檢」請求,之後每次瀏覽器正常的 CORS 請求,就都跟簡單請求同樣
會有一個 Origin 頭信息字段
服務器的迴應,也都會有一個 Access-Control-Allow-Origin 頭信息字段
JSONP 只支持GET請求,
CORS 支持全部類型的 HTTP 請求。
JSONP 的優點在於支持老式瀏覽器,以及能夠向不支持 CORS 的網站請求數據