在前端開發的過程當中,咱們常常遇到"跨域"的問題,如下的文章將列舉一下我在工做中碰到的跨域問題。
以及稍稍的探討一下爲何會有"跨域"問題的出現,和所謂的"同源策略"javascript
1995 年由 Netscape
公司提出,以後被其餘瀏覽器廠商採納。html
同源策略只是一個規範,並無指定其具體的使用範圍和實現方式,各個瀏覽器廠商都針對同源策略作了本身的實現。前端
一些 web 技術都默認採起了同源策略,這些技術範圍包括但不限於Silverlight
, Adobe Flash
, Adobe Acrobat
, Dom
, XMLHttpRequest
。java
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
爲了保證使用者信息的安全,防止惡意網站篡改用戶數據
舉個例子:ajax
假設沒有同源策略,那麼我在A網站下的cookie
就能夠被任何一個網站拿到;那麼這個網站的全部者,就可使用個人cookie
(也就是個人身份)在A網站下進行操做。json
同源策略能夠算是 web 前端安全的基石,若是缺乏同源策略,瀏覽器也就沒有了安全性可言。canvas
非同源的網站之間跨域
同源策略作了很嚴格的限制,可是在實際的場景中,又確實有不少地方須要突破同源策略的限制,也就是咱們常說的跨域
瀏覽器
同源策略最先被提出的時候,爲的就是防止不一樣域名的網頁之間共享 cookie,可是若是兩個網頁的一級域名是相同的,能夠經過設置 document.domain
來共享 cookie。
舉個例子,https://market.douban.com
和https://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
。
在使用 ajax 的過程當中,咱們碰到的同源限制的問題是最多的。
針對 ajax ,咱們有三種方式能夠繞過同源策略的限制:
設置 cross-domain 是目前在 ajax 中最經常使用的一種跨域的方式,相比jsonp
和websoket
也是最安全的一種方式。
惟一美中不足的是低版本的瀏覽器支持的不是很好
IE ✘ 5.5+ ◒ 8+² ◒ 10+¹ ✔ 11Edge ✔
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_...
CROS 的設置,大部分是須要在服務端進行設置,在服務端設置以前,先來看一下 CROS 在瀏覽器中是怎麼運做的:
首先,在瀏覽器中,http 請求將被分爲兩種 簡單請求(simple request)
和 非簡單請求(not-so-simple request)
。
簡單請求的判斷包括兩個條件:
請求方法必須是一下幾種:
HTTP 頭只能包括如下信息:
不能同時知足以上兩個條件的,就都視做非簡單請求
瀏覽器在處理簡單請求時,會在 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
下面來講說這幾個字段都表明什麼:
看名字大概就能猜出來,這個就是告訴瀏覽器,服務端接受那些域名的訪問。值能夠是 request
中的 origin
,也能夠是 *
,也能夠是originA | originB
這樣的形式,可是目前看來,在瀏覽器中只支持單一值和*
兩種方式。具體能夠參考這裏:access-control-allow-origin-response-header
從名字上來看,這個字段標明瞭是否擁有用戶相關的權限。
在瀏覽器中,具體表現爲是否能夠發送 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
這個字段所攜帶的信息了
與簡單請求
最大的不一樣在於,非簡單請求
其實是發送了兩個請求。
首先,在正式請求以前,會先發送一個預請求(preflight-request)
,這個請求的做用是儘量少的攜帶信息,供服務端判斷是否響應該請求。
瀏覽器發送預請求
,請求的 Request Method
會設置爲 options
。
另外,還會帶上這幾個字段:
簡單請求
的origin
服務端收到預請求
以後會根據request
中的origin
,Access-Control-Request-Method
和Access-Control-Request-Headers
判斷是否響應該請求。
若是判斷響應這個請求,返回的response
中將會攜帶:
若是否認這個請求,直接返回不帶這三個字段的response
就能夠,瀏覽器將會把這種返回判斷爲失敗的返回,觸發onerror
方法
若是預請求
被正確響應,接下來就會發送正式請求,正式請求的request
和正常的 ajax 請求基本沒有區別,只是會攜帶 origin
字段;response
和簡單請求
同樣,會攜帶上Access-Control-*
這些字段
websocket 不遵循同源策略。
可是在 websocket 請求頭中會帶上 origin
這個字段,服務端能夠經過這個字段來判斷是否須要響應,在瀏覽器端並無作任何限制。
jsonp 其實算是一種 hack 形式的請求。
jsonp 的本質實際上是請求一段 js 代碼,是對靜態文件資源的請求,因此並不遵循同源策略。可是由於是對靜態文件資源的請求,因此只能支持 GET
請求,對於其餘方法沒有辦法支持。
根據同源策略的規定,若是兩個頁面不一樣源,那麼相互之間實際上是隔離的。
在使用 iframe 的頁面中,雖然咱們能夠經過iframe.contentWindow
,window.parent
,window.top
等方法拿到window
對象,可是根據同源策略,瀏覽器將對非同源的頁面之間的window
和location
對象添加限制
不一樣源的兩個網頁將不能:
window
對象中的屬性/方法不一樣源的兩個網頁能夠:
具體的規則能夠參考這裏:integration-with-idl
可是在現實世界中,有不少場景下,實際上是須要兩個非同源的 iframe 之間進行「跨域」操做的。爲了實現這種「跨域」,咱們借用瞭如下幾種方法:
片斷標識符
指的就是 url 中 #
以後的部分,也就是咱們常說的 location.hash
。
使用片斷標識符依託於如下幾個關鍵點:
window
和 dom
,可是能夠改變 iframe 的 url
hashchange
事件經過這幾個關鍵點,能夠實現基於 hashchange
來操做頁面
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
是否被改變
雖然上面的兩種方法均可以實現不一樣源頁面之間的通訊,可是總歸是屬於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) // 消息內容 })
在 canvas
的使用過程當中,也會碰到同源策略的限制。
如下的幾種操做,都會受到同源策略的限制:
例如:
// 這段 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.com
和b.com
是不一樣源的兩個網頁,觸發了同源策略的限制。換成toBlob
或getImageData
會報一樣的錯誤。
咱們來探究如下報這個錯誤的緣由:
首先,全部bitmaps
類型的對象,在被canvas
或ImageBitmap
使用時,都會先檢查當前這對象,是否是處在origin clean
的狀態。
而後,全部bitmaps
類型的對象,默認狀況下,這個origin clean
都是true
,可是若是這個bitmaps
被跨域調用,那麼,這個origin clean
將會被設置成 false
。
再而後,在使用toDataURL
,toBlob
和getImageData
時,都會先檢查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
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>