同源策略和跨域

在前端開發的過程當中,咱們常常遇到"跨域"的問題,如下的文章將列舉一下我在工做中碰到的跨域問題。
以及稍稍的探討一下爲何會有"跨域"問題的出現,和所謂的"同源策略"javascript

同源策略

1. 歷史

1995 年由 Netscape 公司提出,以後被其餘瀏覽器廠商採納。html

同源策略只是一個規範,並無指定其具體的使用範圍和實現方式,各個瀏覽器廠商都針對同源策略作了本身的實現。前端

一些 web 技術都默認採起了同源策略,這些技術範圍包括但不限於Silverlight, Adobe Flash, Adobe Acrobat, Dom, XMLHttpRequestjava

2. 定義

Under the policy, a web browser permits scripts contained in a first web page to access data in a second web page, but only if both web pages have the same origin. An origin is defined as a combination of URI scheme, hostname, and port number.

判斷同源的三個要素:web

  • 相同的協議
  • 相同的域名
  • 相同的端口號

3. 存在的意義

爲了保證使用者信息的安全,防止惡意網站篡改用戶數據

舉個例子:ajax

假設沒有同源策略,那麼我在A網站下的cookie就能夠被任何一個網站拿到;那麼這個網站的全部者,就可使用個人cookie(也就是個人身份)在A網站下進行操做。json

同源策略能夠算是 web 前端安全的基石,若是缺乏同源策略,瀏覽器也就沒有了安全性可言。canvas

4. 限制範圍

非同源的網站之間跨域

  • 沒法共享 cookie, localStorage, indexDB
  • 沒法操做彼此的 dom 元素
  • 沒法發送 ajax 請求
  • 沒法經過 flash 發送 http 請求
  • 其餘

跨域

同源策略作了很嚴格的限制,可是在實際的場景中,又確實有不少地方須要突破同源策略的限制,也就是咱們常說的跨域瀏覽器

1. cookie

同源策略最先被提出的時候,爲的就是防止不一樣域名的網頁之間共享 cookie,可是若是兩個網頁的一級域名是相同的,能夠經過設置 document.domain來共享 cookie。

舉個例子,
https://market.douban.comhttps://book.douban.com,這兩個網頁的一級域名都是 douban.com,若是我在 market.douban.com中執行了

document.domain = 'douban.com'
    document.cookie = 'cross=yes'
    或
    document.cookie = 'cross=yes;path=/;domain=douban.com'

這樣設置了 cookie 以後,在 book.douban.com 中是能夠取到這個 cookie 的。

除了在前端設置以外,也能夠直接在 response 裏將 cookie 的 domain 設置成 .douban.com

2. Ajax

在使用 ajax 的過程當中,咱們碰到的同源限制的問題是最多的。

針對 ajax ,咱們有三種方式能夠繞過同源策略的限制:

2.1 設置 CORS

設置 cross-domain 是目前在 ajax 中最經常使用的一種跨域的方式,相比jsonpwebsoket也是最安全的一種方式。

惟一美中不足的是低版本的瀏覽器支持的不是很好

IE ✘ 5.5+ ◒ 8+² ◒ 10+¹ ✔ 11

Edge ✔

Firefox ✘ 2+ ✔ 3.5+

Chrome ◒ 4+¹ ✔ 13+

Safari ✘ 3.1+ ◒ 4+¹ ✔ 6+³

Opera ✘ 9+ ✔ 12+

¹Does not support CORS for images in <canvas>

²Supported somewhat in IE8 and IE9 using the XDomainRequest object (but has limitations)

³Does not support CORS for <video> in <canvas>: https://bugs.webkit.org/show_...

2.1.1 CORS 的運做

CROS 的設置,大部分是須要在服務端進行設置,在服務端設置以前,先來看一下 CROS 在瀏覽器中是怎麼運做的:

首先,在瀏覽器中,http 請求將被分爲兩種 簡單請求(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]

不能同時知足以上兩個條件的,就都視做非簡單請求

2.1.2 簡單請求(simple request)

瀏覽器端

瀏覽器在處理簡單請求時,會在 Header 中加上一個 origin(protocal + host + path + port) 字段,來標明這個請求是來自哪裏。

在 CROS 請求中,默認是不會攜帶 cookie之類的用戶信息的,可是不攜帶用戶信息的話,是沒辦法判斷用戶身份的,因此,能夠在請求時將withCredentials設置爲 true, 例如:

var xhr = new XMLHttpRequest()
    xhr.withCredentials = true

設置了這個值以後,在服務端會將 response 中的 Access-Control-Allow-Credentials 也設置爲 true,這樣瀏覽器纔會相應 cookie

服務端

在服務端拿到這個請求以後,會對 origin 進行判斷,若是是在容許範圍內的請求,將會在 respones 返回的 Header 中加上:

Access-Control-Allow-Origin: origin
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: something

下面來講說這幾個字段都表明什麼:

  • Access-Control-Allow-Origin

    看名字大概就能猜出來,這個就是告訴瀏覽器,服務端接受那些域名的訪問。值能夠是 request 中的 origin,也能夠是 *,也能夠是originA | originB 這樣的形式,可是目前看來,在瀏覽器中只支持單一值和*兩種方式。具體能夠參考這裏:access-control-allow-origin-response-header

  • Access-Control-Allow-Credentials

    從名字上來看,這個字段標明瞭是否擁有用戶相關的權限。

    在瀏覽器中,具體表現爲是否能夠發送 cookie。這個值能夠選擇性返回,若是不返回的話,默認就 是不容許發送 cookie,若是返回,則只能返回 true。

    另外,若是這個值被設爲了true,那麼Access-Control-Allow-Origin就不能被設置爲 *,必需要顯示指定爲origin的值;而且返回的cookie由於是在被跨域訪問的域名下,由於遵照同 源策略,因此在origin網頁中是不能被讀取到的。

  • Access-Control-Expose-Headers

    從字面意義上來看,這個字段返回的就是其餘可被返回的數據。

    之因此會有這個字段,是由於在簡單請求中,response返回的頭信息中,瀏覽器只能拿到如下幾個基本字段:Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma

    若是想要拿到更多的額外信息,只能在Access-Control-Expose-Headers裏設置,例如:

    Access-Control-Expose-Headers: "Foo=foo"

    這樣的話,在瀏覽中,就能夠獲取 Foo 這個字段所攜帶的信息了

2.1.3 非簡單請求(not-so-simple request)

簡單請求最大的不一樣在於,非簡單請求其實是發送了兩個請求。

預請求

首先,在正式請求以前,會先發送一個預請求(preflight-request),這個請求的做用是儘量少的攜帶信息,供服務端判斷是否響應該請求。

瀏覽器

瀏覽器發送預請求,請求的 Request Method 會設置爲 options

另外,還會帶上這幾個字段:

  • Origin: 同簡單請求origin
  • Access-Control-Request-Method: 請求將要使用的方法
  • Access-Control-Request-Headers: 瀏覽器會額外發送哪些頭信息
服務端

服務端收到預請求以後會根據request中的origin,Access-Control-Request-MethodAccess-Control-Request-Headers判斷是否響應該請求。

若是判斷響應這個請求,返回的response中將會攜帶:

  • Access-Control-Allow-Origin: origin
  • Access-Control-Allow-Methods: like request
  • Access-Control-Allow-Headers: like request

若是否認這個請求,直接返回不帶這三個字段的response就能夠,瀏覽器將會把這種返回判斷爲失敗的返回,觸發onerror方法

正式響應

若是預請求被正確響應,接下來就會發送正式請求,正式請求的request和正常的 ajax 請求基本沒有區別,只是會攜帶 origin 字段;response簡單請求同樣,會攜帶上Access-Control-*這些字段

2.2 websocket

websocket 不遵循同源策略。

可是在 websocket 請求頭中會帶上 origin 這個字段,服務端能夠經過這個字段來判斷是否須要響應,在瀏覽器端並無作任何限制。

2.3 jsonp

jsonp 其實算是一種 hack 形式的請求。

jsonp 的本質實際上是請求一段 js 代碼,是對靜態文件資源的請求,因此並不遵循同源策略。可是由於是對靜態文件資源的請求,因此只能支持 GET 請求,對於其餘方法沒有辦法支持。

3. iframe

3.1 iframe 中的同源策略

根據同源策略的規定,若是兩個頁面不一樣源,那麼相互之間實際上是隔離的。

在使用 iframe 的頁面中,雖然咱們能夠經過iframe.contentWindow,window.parent,window.top等方法拿到window對象,可是根據同源策略,瀏覽器將對非同源的頁面之間的windowlocation對象添加限制

不一樣源的兩個網頁將不能:

  • 操做彼此的 dom
  • 獲取/調用彼此 window 對象中的屬性/方法

不一樣源的兩個網頁能夠:

  • 改變父/子級的 url

具體的規則能夠參考這裏:integration-with-idl

可是在現實世界中,有不少場景下,實際上是須要兩個非同源的 iframe 之間進行「跨域」操做的。爲了實現這種「跨域」,咱們借用瞭如下幾種方法:

  • 片斷標識符(fragment identifier)
  • 使用 window.name
  • 跨文檔通訊

3.2 使用片斷標識符(fragment identifier)

片斷標識符指的就是 url 中 # 以後的部分,也就是咱們常說的 location.hash
使用片斷標識符依託於如下幾個關鍵點:

  1. 改變 url 裏的這個部分,是不會觸發頁面的刷新的
  2. 父級頁面雖然不能操做 iframe 中的 windowdom,可是能夠改變 iframe 的 url
  3. window 對象能夠監聽 hashchange 事件

經過這幾個關鍵點,能夠實現基於 hashchange 來操做頁面

3.3 使用 window.name

window.name這個屬性最厲害的地方在於,window對象沒有改變的話,這個 window 跳轉的網頁,都讀取 window.name 這個值。

例如,A 網頁設置了 window.name,而後跳轉到了 B 網頁,可是 B 網頁中,仍然能夠讀取到 A 設置的 window.name

經過這個特性,在 iframe 中,子頁面能夠先設置 window.name

而後跳轉到一個跟父頁面同級的地址,這個 window.name 依然存在,由於已經調到了跟父級頁面同源的地址中,因此父頁面能夠獲取到 iframe.contentWindow中屬性,也就是能夠讀取到 window.name

這種方法最大的優勢就是window.name能夠傳一個很長的字符串,可是缺點也比較明顯,就是須要在父級頁面不停的去檢查子頁面的window.name是否被改變

3.4 跨文檔通訊API(Cross-document messaging)

雖然上面的兩種方法均可以實現不一樣源頁面之間的通訊,可是總歸是屬於hack的方法,眼看着你們對非同源頁面的通訊都有需求,因此在 HTML5 規範中,添加了一個window.postMessage的方法。

經過這個方法,能夠方便的實現不一樣源的頁面之間的通訊。

看一個簡單的例子:

// Page Foo
iframe.contentWindow.postMessage('Hello from foo', '/path/to/bar')


// Page Bar
window.parent.addEventListener('message', function (e) {
    console.log(e.source)    // 發送消息的窗口
    console.log(e.origin)  // 消息發向的網址
    console.log(e.data)    // 消息內容
})

2.6 canvas

canvas 的使用過程當中,也會碰到同源策略的限制。

如下的幾種操做,都會受到同源策略的限制:

  • canvas.toDataURL
  • canvas.toBlob
  • canvas.getContent('2d').getImageData(x,y,w,h)

例如:

// 這段 JS 運行在 a.com 這個域名下
var canvas = document.createElement('canvas')
var ctx = canvas.getContent('2d')
var src = 'http://b.com/path/to/a/image'
var img = new Image()
img.onload = function () {
    canvas.with = img.style.width
    canvas.height = img.style.height
    ctx.drawImage(img)
    // 如下的這這三種操做都會報錯
    canvas.toDataURL('image/jpg')
    canvas.toBlob(function () {})
    ctx.getImageData(0, 0, 10, 10)
}
img.src = src

運行時會報錯

Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

能夠看到是toDataURL的時候,由於 a.comb.com是不一樣源的兩個網頁,觸發了同源策略的限制。換成toBlobgetImageData會報一樣的錯誤。

咱們來探究如下報這個錯誤的緣由:

首先,全部bitmaps類型的對象,在被canvasImageBitmap使用時,都會先檢查當前這對象,是否是處在origin clean的狀態。

而後,全部bitmaps類型的對象,默認狀況下,這個origin clean都是true,可是若是這個bitmaps被跨域調用,那麼,這個origin clean將會被設置成 false

再而後,在使用toDataURL,toBlobgetImageData時,都會先檢查origin clean,若是爲 false 的話,就會拋出SecurityError這樣的異常。

那麼,這個origin clean的狀態,是如何設置的呢?

能夠經過crossOrigin來設置,看代碼:

var canvas = document.createElement('canvas')
var ctx = canvas.getContent('2d')
var src = 'http://b.com/path/to/a/image'
var img = new Image()
img.onload = function () {
    canvas.with = img.style.width
    canvas.height = img.style.height
    ctx.drawImage(img)
    canvas.toDataURL('image/jpg')
}
img.crossOrigin = '*'
img.src = src

加上了crossOrigin這個屬性,而後執行,發現還會報個錯:

Image from origin 'http://b.com' has been blocked from loading by Cross-Origin Resource Sharing policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access

看報錯信息大概能夠知道,是Access-Control-Allow-Origin這裏出了問題,只須要把Access-Control-Allow-Origin設置成對應的值就能夠了。

更具體的緣由能夠參考這裏:Security with canvas elements

2.7 flash

flash在進行 HTTP 請求時,也遵循同源策略。

可是相比較以上的各類場景和繞過同源策略的方法,flash 的跨域請求設置很容易,只須要在目標服務的根目錄下設置一個crossdomain.xml文件便可。

這個文件中會規定哪些域能夠訪問當前服務,看一個真實世界裏的例子:

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy
 SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <site-control permitted-cross-domain-policies="master-only"/>
  <allow-access-from domain="t.simple.com"/>
  <allow-access-from domain="img1.simple.com"/>
  <allow-access-from domain="img2.simple.com"/>
  <allow-access-from domain="img3.simple.com"/>
  <allow-access-from domain="img4.simple.com"/>
  <allow-access-from domain="img5.simple.com"/>
  <allow-access-from domain="*.simple.com.cn"/>
  <allow-access-from domain="all.vic.sina.com.cn"/>
</cross-domain-policy>

參考文章:

相關文章
相關標籤/搜索