概述
同源策略 是一個重要的安全策略,它用於限制一個 origin 的文檔或者它加載得腳本如何能與另一個源的資源進行交互。它能幫助阻擋惡意文檔,減少可能被攻擊的媒介 [ MDN ] .
同源策略 是為了保護用戶信息,用戶信息分為兩種:
- 存在用戶本地的信息,如cookie;
- 存在服務器數據庫的用戶信息,如個人資料;
所以 同源策略 限制 cookie 等信息的跨源網頁讀取,是為了保護本地用戶信息; 同源策略 限制跨域 ajax 請求,是為了保護被跨域請求的服務器中數據庫用戶信息。
簡單來說,限制讀寫跨域 cookie 是為了保護自己的信息;而限制跨域 ajax 請求,是為了保護別人的信息。跨源請求的本質是請求別人的信息,所以能否跨域請求,是由被請求的服務器決定的。
* 小知識:服務器間的訪問不存在同源策略,因此往往通過代碼做白名單或權限設置,如小程序的認證與上線一樣,載體 API 會對認證后的程序開放 權限 / 白名單。通過 proxyTable 、Nginx 等代理服務器進行跨域,本質是服務器請求另一台服務器,因此不存在瀏覽器的同源策略。
同源的定義
如果兩個 URL 的 protocol 、port (en-US) (有指定時)和 host 都相同的話,則這兩個 URL 是同源。這個方案也被稱為 “ 協議 / 主機 / 端口元組 ”,或者直接是 “ 元組 ” 。(“ 元組 ” 是指一組項目構成的整體,雙重 / 三重 / 四重 / 五重 / 等的通用形式)
下表給出了與 URL http://store.company.com/dir/page.html 的源進行對比的示例:
URL | 結果 | 原因 |
http://store.company.com/dir2/other.html |
同源 | 只有路徑不同 |
http://store.company.com/dir/inner/another.html |
同源 | 只有路徑不同 |
https://store.company.com/secure.html |
失敗 | 協議不同 |
http://store.company.com:81/dir/etc.html |
失敗 | 端口不同 ( http:// 默認端口是80) |
http://news.company.com/dir/other.html |
失敗 | 主機不同 |
另一組例子(同源):
http://example.com/app1/index.html http://example.com/app2/index.html |
same origin because same scheme (http ) and host (example.com ) |
http://Example.com:80 http://example.com |
same origin because a server delivers HTTP content through port 80 by default |
非同源:
http://example.com/app1 https://example.com/app2 |
different schemes |
http://example.com http://www.example.com http://myapp.example.com |
different hosts |
http://example.com http://example.com:8080 |
different ports |
非同源請求報錯:
這時后端會給你反饋:接口服務已經接收到請求並成功返回!!這時就要回歸文題了:瀏覽器的 同源策略。
服務器是不存在同源策略的,也就是我們平時通過代理服務器訪問非同源服務后再將響應數據返回給前端的理由了。
那為什么瀏覽器存在同源策略服務器仍然能接收到請求並作出響應?因為瀏覽器並沒有攔截我們的跨域請求,而是偷摸截胡了服務器返回給我們的響應,瀏覽器它拿到了,但是不給我們,就是玩兒。
為什么要進行跨域
當一個項目變大時,把所有的內容架構在一個網站或者后台服務器是不現實的。
一個體量很大的網站往往有許多獨立且復雜的業務,比如分為三個獨立服務模塊:
- 訂單管理api
- 用戶管理api
- 新聞管理api
此時我們把 web 項目和任何一個 API 服務集成在一起都不合適,它應該是一個專門的網站。
源的繼承
在頁面中通過 about:blank 或 javascript: URL 執行的腳本會繼承打開該 URL 的文檔的源,因為這些類型的 URLs 沒有包含源服務器的相關信息。
例如,about:blank
通常作為父腳本寫入內容的新的空白彈出窗口的 URL(例如,通過 Window.open() )。 如果此彈出窗口也包含 JavaScript,則該腳本將從創建它的腳本那里繼承對應的源。
注意:在Gecko 6.0之前,如果用戶在位置欄中輸入 data
URLs,data
URLs 將繼承當前瀏覽器窗口中網頁的安全上下文。
data
:URLs 獲得一個新的,空的安全上下文。
IE中的特例
Internet Explorer 的同源策略有兩個主要的差異點:
- 授信范圍 ( Trust Zones ) :兩個相互之間高度互信的域名,如公司域名 ( corporate domains ) ,則不受同源策略限制。
- 端口:IE 未將端口號納入到同源策略的檢查中,因此 https://company.com:81/index.html 和 https://company.com/index/html 屬於同源並且不受任何限制。
這些差異點是不規范的,其它瀏覽器也未做出支持。但會助於開發基於 window RT IE 的應用程序。
源的更改
滿足某些限制條件的情況下,頁面時可以修改它的源。腳本可以將 document.domain 的值設置為其當前域或其當前域的父域。如果將其設置為其當前域的父域,則這個較短的父域將用於后續源檢查。
例如:假設 http://store.company.com/dir/other.html 文檔中的一個腳本執行以下語句:
document.domain = "company.com";
這條語句執行之后,頁面將會成功地通過與 http://company.com/dir/page.html 的同源檢測(假設 http://company.com/dir/page.html 將其 document.domain 設置為 " company.com ",以表明它希望允許這樣做 - 更多有關信息,請參閱 document.domain )。然而, company.com 不能設置 document.domain 為 othercompany.com ,因為它不是 company.com 的父域。
端口號是由瀏覽器另行檢查的。任何對 document.domain 的賦值操作,包括 document.domain = document.domain 都會導致端口號被重寫為 null 。因此 company.com:8080 不能僅通過設置 document.domain = "company.com" 來與 company.com 通信。必須在他們雙方中都進行賦值,以確保端口號都 null 。
注意:使用 document.domain
來允許子域安全訪問其父域時,您需要在父域和子域中設置 document.domain 為相同的值。這是必要的,即使這樣做只是將父域設置回其原始值。不這樣做可能會導致權限錯誤。
跨源(域)網絡訪問
同源策略控制不同源之間的交互,例如在使用 XMLHttpRequest 或 <img> 標簽時會受到同源策略的約束。這些交互通常分為三類:
- 跨域寫操作 ( Cross-origin writes ) 一般是被允許的。例如鏈接(links),重定向以及表單提交。特定少數的HTTP請求需要添加 preflight 。
- 跨域資源嵌入 ( Cross-origin embedding ) 一般是被允許(下文會舉例說明)。
- 跨域讀操作 ( Cross-origin embedding ) 一般是不被允許的,但常可以通過內嵌資源來巧妙的進行讀取訪問。例如,你可以讀取嵌入圖片的高度和寬度,調用內嵌腳本的方法,或 availability of an embedded resource .
以下是可能嵌入跨源的資源的一些示例:
- <script src="..."></script> 標簽嵌入跨域腳本。語法錯誤信息只能被同源腳本中捕捉到。
- <link rel="stylesheet" href="..."> 標簽嵌入 CSS 。由於 CSS 的 松散的語法規則 , CSS 的跨域需要一個設置正確的 HTTP 頭部 Content-Type 。不同瀏覽器有不同的限制: IE 、Firefox 、Chrome 、Safari(跳至 CVE-2010-0051)部分 和 Opera 。
- 通過 <img> 展示的圖片。支持的圖片格式包括 PNG , JEPG , GIF , BMP , SVG , ...
- 通過 <video> 和 <audio> 播放的多媒體資源。
- 通過 <object> 、<embed> 和 <applet> 嵌入的插件。(注意,<embed> 是 HTML 5 中的新標簽,現在已經不建議使用了,可用 <img> 、<iframe> 、<video> 、<audio> 等標簽代替;HTML 5 不支持 <applet> 標簽,在 HTML 4.01 中該元素已廢棄,請使用 <object> 標簽代替它)
- 通過 @font-face 引入的字體。一些瀏覽器允許跨域字體 ( cross-origin fonts ),一些需要同源字體(same-origin fonts)。
- 通過 <iframe> 載入的任何資源。站點可以使用 X-Frame-Options 消息頭來阻止這種形式的跨域交互。
通俗來講,以上這些方式是不需要通過代理即可對他人的資源進行跨站讀取的。
如何允許跨源訪問
- CORS
可以使用 CORS 來允許跨源訪問。CORS 是 HTTP 的一部分,它允許服務端來指定哪些主機可以從這個服務端加載資源。
CORS 是 Cross-Origin-Resource-Sharing 的縮寫,它具體的工作流程是當瀏覽器檢測到我們發送的請求非同源時,會自動在 http 頭部添加一個 origin 字段。
我們拿不到數據時因為瀏覽器在中間做了一層劫持。
以此分析整個跨域過程:
- 這是一次跨域請求
- 請求成功發送到服務器了
- 服務器將數據返回給了瀏覽器
- 服務器返回的響應頭中,不曾告訴瀏覽器哪個域名可以訪問這些數據(沒有設置 Access-Control-Allow-Origin)
- 瀏覽器將這個數據丟棄了,拋出同源錯誤
這時只需要在服務的響應頭中加入 Access-Control-Allow-Origin: * 即可完成跨域數據獲取。
總結:
- 當瀏覽器發送一個跨域請求時會在 http 頭部自動加上 Origin
- 需要服務配合設置響應頭 Access-Control-Allow-Origin:*|Origin 即可完成跨域
- CORS 支持 GET、POST 常規請求
- CORS 支持 PUT、DELETE 等非 POST、GET 的請求,但會先發出一次預檢請求。
- proxy 代理模式
核心思想:通過前端請求我們自己的后台服務,這個服務即是代理服務器,再通過代理服務器去請求另一個非同源的后台服務,因為后台之間不需要跨域,也不存在 同源策略 機制,因此可以獲取到真實的數據,再將其返回給前台。
* 閱讀我的其他文章:VUE006. 前端跨域代理服務器ProxyTable概述與配置 和 Nginx:多項目開發配置跨域代理 以脫離服務獨自完成跨域,具體操作本文不再贅述。
- JSONP
也可以通過 JSONP (JSON with Padding) 進行跨源訪問。 JSONP 是 JSON 的一種 “使用模式”,上節提到的 資源嵌入 ,其中 HTML 的 <script> 元素是同源策略的例外。利用這個元素的開放策略,網頁可以得到從其他源動態產生的 JSON 資料,而這種使用模式就是所謂的 JSONP 。用 JSONP 抓到的資料並非 JSON ,而是任意的 JavaScript ,用 JavaScript 直譯器執行而不是用 JSON 解析器解析。如:
我們在 http://169.254.200.238:8020/jsonp/index.html 中向 http://169.254.200.238:8080/jsonp.do 發起請求。
$.get("http://169.254.200.238:8080/jsonp.do", function (data) {
console.log(data);
});
兩者端口號分別為 8080 、8020 ,非同源:
使用 JSONP 請求:
<script type="text/javascript" src="http://169.254.200.238:8080/jsonp.do"></script>
Status Code: 200,可以看出 JSONP 支持且僅支持 GET 請求,不受同源限制。但瀏覽器拋出了語句不合法的異常:
原因是我們通過此方法請求的數據會即時被瀏覽器當作 JavaScript 語句執行,這點也不難看出元素 <script> 的執行機制,也就是當我們通過 URL 引入包時頁面中能夠立即生效的原因,也是我們必須按順序引入包否則會報錯/失效的原因。
通過上述分析其實就不難理解, JSONP 跨域的本質就是期待一個函數。如我們請求到的數據為:
callback( {"result":"success"} )
其中 {"result":"success"} 是我們想要獲取的數據,瀏覽器會立即執行 callback 這個函數,因此我們需要:
function callback(data) {
// data為返回數據
// TODO 解析數據
}
因此 JSONP 跨域請求的關鍵在於:服務端要在返回的數據外包裹一層頁面已經定義好的函數。
換一句話說就是,由服務器提供數據和方法名,由前端來定義這個解析數據的方法, JSONP 請求成功響應返回 callback( { someData... } ),被瀏覽器自動執行,就對應頁面提前定義好的 function callback(data) { },其中 someData... 由 callback 中的 形參接收。
或是在 query 中告知后台前端提供的 fn 是什么:
<script type="text/javascript" src="http://localhost:12345/getJsonp?callback=show"></script>
* 使用 spring MVC 處理 jsonp 請求,可閱讀文章:jsonp跨域請求詳解——從繁至簡
如何阻止跨源訪問
- 阻止跨域寫操作,只要檢測請求中的一個不可推測的標記( CSRF token )即可,這個標記被稱為 Cross-Site Request Forgery (CSRF) 標記。我們必須使用這個標記來阻止頁面的跨站讀操作。
- 阻止資源的跨站讀取,需要保證該資源是不可嵌入的。阻止嵌入行為是必須的,因為嵌入資源通常向其暴露信息。
- 阻止跨站嵌入,需要確保你的資源不能通過以上列出的可嵌入資源格式使用。瀏覽器可能不會遵守 Content-Type 頭部定義的類型。例如,如果您在HTML文檔中指定 <script> 標記,則瀏覽器將嘗試將標簽內部的 HTML 解析為 JavaScript 。當您的資源不是您網站的入口點時,您還可以使用 CSRF 令牌來防止嵌入。
跨源腳本API訪問
JavaScript 的 API 中,如 iframe.contentWindow 、window.parent 、window.open 和 window.opener 允許文檔間直接相互引用。當兩個文檔的源不同時,這些引用方式將對 Window 和 Location 對象的訪問添加限制,如下兩節所述。
為了能讓不同源中文檔進行交流,可以使用 window.postMessage 。
規范:HTML Living Standard § Cross-origin objects 。
Window
允許以下對 Window 屬性的跨源訪問:
方法 |
---|
window.blur |
window.close |
window.focus |
window.postMessage |
屬性 | |
---|---|
window.closed |
只讀. |
window.frames |
只讀. |
window.length |
只讀. |
window.location |
讀/寫. |
window.opener |
只讀. |
window.parent |
只讀. |
window.self |
只讀. |
window.top |
只讀. |
window.window |
只讀. |
某些瀏覽器允許訪問除上述外更多的屬性。
Location
允許以下對 Location 屬性的跨源訪問:
方法 |
location.replace |
屬性 | |
URLUtils.href |
只寫. |
某些瀏覽器允許訪問除上述外更多的屬性。
跨源數據存儲訪問
訪問存儲在瀏覽器中的數據,如 localStorage 和 IndexedDB(事務型數據庫系統),是以源進行分割。每個源都擁有自己單獨的存儲控件,一個源中的 JavaScript 腳本不能對屬於其它源的數據進行讀寫操作。
Cookies 使用不同的源定義方式。一個頁面可以為本域和其父域設置 cookie ,只要是父域不是公共后綴( public suffix )即可。Firefox 和 Chrome 使用 Public Suffix List 檢測一個域是否時公共后綴( public suffix )。Internet Explorer 使用其內部的方法來檢測域是否是公共后綴。不管使用哪個協議( HTTP / HTTPS )或端口號,瀏覽器都允許給定的域一級其任何子域名( sub-domains )訪問 cookie 。當你設置 cookie 時,你可以使用 Domain 、 Path 、Secure 、和 HttpOnly 標記來限定其可訪問性。當你讀取 cookie 時,你無法知道它時在哪里被設置的。即使您只是用安全的 https 連接,您看到的任何 cookie 都有可能是使用不安全的連接進行設置的。
參見
原始文件資料
- Author(s): Jesse Runderman