跨域 是前端領域繞不開的一道題,今天就來好好聊一聊前端跨域。javascript
同源策略(same-origin policy) 最初是由 Netspace 公司在 1995 年引入瀏覽器的一種安全策略,如今全部的瀏覽器都遵照同源策略,它是瀏覽器安全的基石。css
同源策略規定跨域之間的腳本是相互隔離的,一個域的腳本不能訪問和操做另一個域的絕大部分屬性和方法。所謂的 同源 指的是 協議相同,域名相同,端口相同。html
同源策略最初只是用來防止不一樣域的腳本訪問 Cookie 的,可是隨着互聯網的發展,同源策略愈來愈嚴格,目前在不一樣域的場景下,Cookie、本地存儲(LocalStorage,SessionStorage,IndexDB),DOM 內容,AJAX(Asynchronous JavaScript and XML,非同步的 JavaScript 與 XML 技術) 都沒法正常使用。前端
下表給出以 http://www.a.com/page/index.html 爲例子進行同源檢測的示例:java
示例 | URL | 結果 | 緣由 |
---|---|---|---|
A | http://www.a.com/page/login.html | 成功 | 同源 |
B | http://www.a.com/page2/index.html | 成功 | 同源 |
C | https://www.a.com/page/secure.html | 失敗 | 不一樣協議 |
D | http://www.a.com:8080/page/index.html | 失敗 | 不一樣端口 |
E | http://static.a.com/page/index.html | 失敗 | 不一樣域名 |
F | http://www.b.com/page/index.html | 失敗 | 不一樣域名 |
解決方案按照解決方式能夠分爲四個大的方面:node
src
或者 herf
屬性的標籤src
或者 herf
屬性的標籤全部具備 src
屬性的標籤都是能夠跨域,好比:<script>
、<img>
、<iframe>
,以及 <link>
標籤,這些標籤給咱們了提供調用第三方資源的能力。git
這些標籤也有限制,如:只能用於 GET
方式獲取資源,須要建立一個 DOM 對象等。github
不一樣的標籤發送請求的機制不一樣,須要區別對待。如:<img>
標籤在更改 src
屬性時就會發起請求,而其餘的標籤須要添加到 DOM 樹以後纔會發起請求。web
const img = new Image() img.src = 'http://domain.com/picture' // 發起請求 const iframe = document.createElement('iframe') iframe.src = 'http://localhost:8082/window_name_data.html' document.body.appendChild(iframe) // 發起請求 複製代碼
原理:利用神奇的 window.name
屬性以及 iframe
標籤的跨域能力。 window.name 的值不是普通的全局變量,而是當前窗口的名字,iframe 標籤也有包裹的窗體,天然也就有 window.name 屬性。ajax
window.name 屬性神奇的地方在於 name 值在不一樣的頁面(甚至不一樣域)加載後依舊存在,且在沒有修改的狀況下不會變化。
// 打開一個空白頁,打開控制檯 window.name = JSON.stringify({ name: 'window', version: '1.0.0' }) window.location = 'http://baidu.com' //頁面跳轉且加載成功後, window.name 的值仍是咱們最初賦值的值 console.log(window.name) // {"name":"window","version":"1.0.0"} 複製代碼
window.name 屬性結合 iframe 的跨域能力就能夠實現不一樣域之間的數據通訊,具體步驟以下:
注意:當數據源頁面載入成功後(即 window.name 已經賦值),須要把 iframe 的 src 指向訪問頁面的同源頁面(或者空白頁 about:blank;
),不然在讀取 iframe.contentWindow.name
屬性時會由於同源策略而報錯。
window.name 還有一種實現思路,就是 數據頁在設置完 window.name 值以後,經過 js 跳轉到與父頁面同源的一個頁面地址,這樣的話,父頁面就能經過操做同源子頁面對象的方式獲取 window.name 的值,以達到通訊的目的。
原理:經過使用 js 對父子框架頁面設置相同的 document.domain
值來達到父子頁面通訊的目的。 限制:只能在主域相同的場景下使用。
iframe 標籤是一個強大的標籤,容許在頁面內部加載別的頁面,若是沒有同源策略那咱們的網站在 iframe 標籤面前基本沒有安全可言。
www.a.com
與 news.a.com
被認爲是不一樣的域,那麼它們下面的頁面可以經過 iframe 標籤嵌套顯示,可是沒法互相通訊(不能讀取和調用頁面內的數據與方法),這時候咱們可使用 js 設置 2 個頁面的 document.domain
的值爲 a.com
(即它們共同的主域),瀏覽器就會認爲它們處於同一個域下,能夠互相調用對方的方法來通訊。
// http://www.a.com/www.html document.domain = 'a.com' // 設置一個測試方法給 iframe 調用 window.openMessage = function () { alert('www page message !') } const iframe = document.createElement('iframe') iframe.src = 'http://news.a.com:8083/document_domain_news.html' iframe.style.display = 'none' iframe.addEventListener('load', function () { // 若是未設置相同的主域,那麼能夠獲取到 iframeWin 對象,可是沒法獲取 iframeWin 對象的屬性與方法 const iframeWin = iframe.contentWindow const iframeDoc = iframeWin.document const iframeWinName = iframeWin.name console.log('iframeWin', iframeWin) console.log('iframeDoc', iframeDoc) console.log('iframeWinName', iframeWinName) // 嘗試調用 getTestContext 方法 const iframeTestContext = iframeWin.getTestContext() document.querySelector('#text').innerText = iframeTestContext }) document.body.appendChild(iframe) // http://news.a.com/news.html document.domain = 'a.com' // 設置 windon.name window.name = JSON.stringify({ name: 'document.domain', version: '1.0.0' }) // 設置一些全局方法 window.getTestContext = function () { // 嘗試調用父頁面的方法 if (window.parent) { window.parent.openMessage() } return `${document.querySelector('#test').innerText} (${new Date()})` } 複製代碼
原理:利用修改 URL 中的錨點值來實現頁面通訊。URL 中有 #abc
這樣的錨點信息,此部分信息的改變不會產生新的請求(可是會產生瀏覽器歷史記錄),經過修改子頁的 hash 值傳遞數據,經過監聽自身 URL hash 值的變化來接收消息。
該方案要作到父子頁面的雙向通訊,須要用到 3 個頁面:主調用頁,數據頁,代理頁。這是由於主調用頁能夠修改數據頁的 hash 值,可是數據頁不能經過 parent.location.hash
的方式修改父頁面的 hash 值(僅 IE 與 Chrome 瀏覽器不容許),因此只能在數據頁中再加載一個代理頁(代理頁與主調用頁同域),經過同域的代理頁去操做主調用頁的方法與屬性。
// http://www.a.com/a.html const iframe = document.createElement('iframe') iframe.src = 'http://www.b.com/b.html' iframe.style.display = 'none' document.body.appendChild(iframe) setTimeout(function () { // 向數據頁傳遞信息 iframe.src = `${iframe.src}#user=admin` }, 1000) window.addEventListener('hashchange', function () { // 接收來自代理頁的消息(也可讓代理頁直接操做主調用頁的方法) console.log(`page: data from proxy.html ---> ${location.hash}`) }) // http://www.a.com/b.html const iframe = document.createElement('iframe') iframe.src = 'http://www.a.com/proxy.html' iframe.style.display = 'none' document.body.appendChild(iframe) window.addEventListener('hashchange', function () { // 收到主調用頁傳來的信息 console.log(`data: data from page.html ---> ${location.hash}`) // 一些其餘的操做 const data = location.hash.replace(/#/ig, '').split('=') if (data[1]) { data[1] = String(data[1]).toLocaleUpperCase() } setTimeout(function () { // 修改子頁 proxy.html iframe 的 hash 傳遞消息 iframe.src = `${iframe.src}#${data.join('=')}` }, 1000) }) // http://www.a.com/proxy.html window.addEventListener('hashchange', function () { console.log(`proxy: data from data.html ---> ${location.hash}`) if (window.parent.parent) { // 把數據代理給同域的主調用頁(也能夠直接調用主調用頁的方法傳遞消息) window.parent.parent.location.hash = location.hash } }) 複製代碼
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,能夠安全的實現跨域通訊,它可用於解決如下方面的問題:
postMessage 的具體使用方法能夠參考 window.postMessage ,其中有 2 點須要注意:
window.open
語句返回的窗口對象等。targetOrigin
參數能夠指定哪些窗口接收消息,包含 協議 + 主機 + 端口號,也能夠設置爲通配符 '*'。// http://www.a.com/a.html const iframe = document.createElement('iframe') iframe.src = 'http://www.b.com/b.html' iframe.style.display = 'none' iframe.addEventListener('load', function () { const data = { user: 'admin' } // 向 b.com 傳送跨域數據 // iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.b.com') iframe.contentWindow.postMessage(JSON.stringify(data), '*') }) document.body.appendChild(iframe) // 接受 b.com 返回的數據 window.addEventListener('message', function (e) { console.log(`a: data from b.com ---> ${e.data}`) }, false) // http://www.b.com/b.html window.addEventListener('message', function (e) { console.log(`b: data from a.com ---> ${e.data}`) const data = JSON.parse(e.data) if (data) { data.user = String(data.user).toLocaleUpperCase() setTimeout(function () { // 處理後再發回 a.com // window.parent.postMessage(JSON.stringify(data), 'http://www.a.com') window.parent.postMessage(JSON.stringify(data), '*') }, 1000) } }, false) 複製代碼
原理:藉助 CSS3 的 content
屬性獲取傳送內容的跨域傳輸文本的方式。
相比較 JSONP 來講更爲安全,不須要執行跨站腳本。
缺點就是沒有 JSONP 適配廣,且只能在支持 CSS3 的瀏覽器正常工做。
具體內容能夠經過查看 CSST 瞭解。
Flash 有本身的一套安全策略,服務器能夠經過 crossdomain.xml 文件來聲明能被哪些域的 SWF 文件訪問,經過 Flash 來作跨域請求代理,而且把響應結果傳遞給 javascript,實現跨域通訊。
同源策略針對的是瀏覽器,http/https 協議不受此影響,因此經過 Server Proxy 的方式就能解決跨域問題。
實現步驟也比較簡單,主要是服務端接收到客戶端請求後,經過判斷 URL 實現特定跨域請求就代理轉發(http,https),而且把代理結果返回給客戶端,從而實現跨域的目的。
// NodeJs const http = require('http') const server = http.createServer(async (req, res) => { if (req.url === '/api/proxy_server') { const data = 'user=admin&group=admin' const options = { protocol: 'http:', hostname: 'www.b.com', port: 8081, path: '/api/proxy_data', method: req.method, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(data), }, } const reqProxy = http.request(options, (resProxy) => { res.writeHead(resProxy.statusCode, { 'Content-Type': 'application/json' }) resProxy.pipe(res) // 將 resProxy 收到的數據轉發到 res }) reqProxy.write(data) reqProxy.end() } }) 複製代碼
NodeJs 中 Server Proxy 主要使用 http
模塊的 request
方法以及 stream
的 pipe
方法。
上面是一個最簡單的 NodeJs Server Proxy 實現,真實場景須要考慮更多複雜的狀況,更詳細的能夠介紹能夠點擊 如何編寫一個 HTTP 反向代理服務器 進行了解。
進一步瞭解:HTTP 代理原理及實現(一) HTTP 代理原理及實現(二)
CORS 的全稱是「跨域資源共享」(Cross-origin resource sharing),是 W3C 標準。經過 CORS 協議實現跨域通訊關鍵部分在於服務器以及瀏覽器支持狀況(IE不低於IE10),整個 CORS 通訊過程都是瀏覽器自動完成,對開發者來講 CORS 通訊與同源的 AJAX 請求沒有差異。
瀏覽器將 CORS 請求分爲兩類:簡單請求(simple request)和 非簡單請求(not-so-simple request)。更加詳細的信息能夠經過閱讀 阮一峯老師 的 跨域資源共享 CORS 詳解 文章進行深刻了解。
// server.js // http://www.b.com/api/cors const server = http.createServer(async (req, res) => { if (typeof req.headers.origin !== 'undefined') { // 若是是 CORS 請求,瀏覽器會在頭信息中增長 origin 字段,說明請求來自於哪一個源(協議 + 域名 + 端口) if (req.url === '/api/cors') { res.setHeader('Access-Control-Allow-Origin', req.headers.origin) res.setHeader('Access-Control-Allow-Credentials', true) res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, X-Access-Token') const resData = { error_code: 0, message: '', data: null, } if (req.method === 'OPTIONS') { // not-so-simple request 的 預請求 res.setHeader('status', 200) res.setHeader('Content-Type', 'text/plain') res.end() return } else if (req.method === 'GET') { // simple request Object.assign(resData, { data: { user: 'admin' } }) } else if (req.method === 'PUT') { // not-so-simple res.setHeader('Set-Cookie', ['foo=bar; HttpOnly', 'bar=baz; HttpOnly', 'y=88']) // 設置服務器域名 cookie Object.assign(resData, { data: { user: 'ADMIN', token: req.headers['x-access-token'] } }) } else { Object.assign(resData, { data: { user: 'woqu' } }) } res.setHeader('status', 200) res.setHeader('Content-Type', 'application/json') res.write(JSON.stringify(resData)) res.end() return } res.setHeader('status', 404) res.setHeader('Content-Type', 'text/plain') res.write(`This request URL '${req.url}' was not found on this server.`) res.end() return } }) // http://www.a.com/cors.html setTimeout(function () { console.log('CORS: simple request') ajax({ url: 'http://www.b.com:8082/api/cors', method: 'GET', success: function (data) { data = JSON.parse(data) console.log('http://www.b.com:8082/api/cors: GET data', data) document.querySelector('#test1').innerText = JSON.stringify(data) }, }) }, 2000) setTimeout(function () { // 設置 cookie document.cookie = 'test cookie value' console.log('CORS: not-so-simple request') ajax({ url: 'http://www.b.com:8082/api/cors', method: 'PUT', body: { user: 'admin' }, header: { 'X-Access-Token': 'abcdefg' }, success: function (data) { data = JSON.parse(data) console.log('http://www.b.com:8082/api/cors: PUT data', data) document.querySelector('#test2').innerText = JSON.stringify(data) }, }) }, 4000) 複製代碼
原理: <script>
標籤能夠跨域加載並執行腳本。
JSONP 是一種簡單高效的跨域方式,而且易於實現,可是由於有跨站腳本的執行,比較容易遭受 CSRF(Cross Site Request Forgery,跨站請求僞造) 攻擊,形成用戶敏感信息泄露,並且 由於 <script>
標籤跨域方式的限制,只能經過 GET 方式獲取數據。
// server.js // http://www.b.com/api/jsonp?callback=callback const server = http.createServer((req, res) => { const params = url.parse(req.url, true) if (params.pathname === '/api/jsonp') { if (params.query && params.query.callback) { res.writeHead(200, { 'Content-Type': 'text/plain' }) res.write(`${params.query.callback}(${JSON.stringify({ error_code: 0, data: 'jsonp data', message: '' })})`) res.end() } } // ... }) // http://www.a.com/jsonp.html const script = document.createElement('script') const callback = function (data) { console.log('jsonp data', typeof data, data) } window.callback = callback // 把回調函數掛載到全局對象 window 下 script.src = 'http://www.b.com:8081/api/jsonp?callback=callback' setTimeout(function () { document.body.appendChild(script) }, 1000) 複製代碼
WebSocket protocol 是 HTML5 一種新的協議。它實現了瀏覽器與服務器全雙工通訊,同時容許跨域通信,是 server push 技術的一種很好的實現。
// 服務端實現可使用 socket.io,詳見 https://github.com/socketio/socket.io // client const socket = new WebSocket('ws://www.b.com:8082') socket.addEventListener('open', function (e) { socket.send('Hello Server!') }) socket.addEventListener('message', function (e) { console.log('Message from server', e.data) }) 複製代碼
SSE 即 服務器推送事件,支持 CORS,能夠基於 CORS 作跨域通訊。
// server.js const server = http.createServer((req, res) => { const params = url.parse(req.url, true) if (params.pathname === '/api/sse') { // SSE 是基於 CORS 標準實現跨域的,因此須要設置對應的響應頭信息 res.setHeader('Access-Control-Allow-Origin', req.headers.origin) res.setHeader('Access-Control-Allow-Credentials', true) res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, X-Access-Token') res.setHeader('status', 200) res.setHeader('Content-Type', 'text/event-stream') res.setHeader('Cache-Control', 'no-cache') res.setHeader('Connection', 'keep-alive') res.write('retry: 10000\n') res.write('event: connecttime\n') res.write(`data: starting... \n\n`) const interval = setInterval(function () { res.write(`data: (${new Date()}) \n\n`) }, 1000) req.connection.addListener('close', function () { clearInterval(interval) }, false) return } }) // http://www.a.com:8081/sse.html const evtSource = new EventSource('http://www.b.com:8082/api/sse') evtSource.addEventListener('connecttime', function (e) { console.log('connecttime data', e.data) document.querySelector('#log').innerText = e.data }) evtSource.onmessage = function(e) { const p = document.createElement('p') p.innerText = e.data console.log('Message from server', e.data) document.querySelector('#log').append(p) } setTimeout(function () { evtSource.close() }, 5000) 複製代碼
No silver bullets:沒有一種方案可以適用全部的跨域場景,針對特定的場景使用合適的方式,纔是最佳實踐。
對於靜態資源,推薦藉助 <link>
<script>
<img>
<iframe>
標籤原生的能力實現跨域資源請求。
對於第三方接口,推薦基於 CORS 標準實現跨域,瀏覽器不支持 CORS 時推薦使用 Server Proxy 方式跨域。
頁面間的通訊首先推薦 HTML5 新 API postMessage 方式通訊,安全方便。
其次瀏覽器支持不佳時,當主域相同時推薦使用 document.domain
方式,主域不一樣推薦 location.hash
方式。
非雙工通訊場景建議使用輕量級的 SSE 方式。
雙工通訊場景推薦使用 WebSocket 方式。