相信每一個前端對於跨域這兩個字都不會陌生,在實際項目中應用也不少,但跨域方法的多種多樣讓人應接不暇,前段時間公司同事出現了跨域問題,又一時找不到問題所在,因此在此總結下跨域知識,一篇由淺入深的萬字Web基操文javascript
其實很早就開始寫了,只不過剛開始寫的時候理解不夠深入,後來慢慢就寫其餘以爲較高大尚較內涵的了,而後就又是以爲不夠完美不夠深入又寫一半,就此陷入強迫症患者明知不可爲而爲的死循環,SO,產出少,週期長(不過你們能看到的文章都是準備良久又反覆斟酌後自認爲還不錯的)。。。css
總之又是一篇因爲各類緣由半途而廢的積壓文,這裏終於收尾了,長出一口氣,哎,仍是太年輕,吐槽結束,進入正文html
文章收錄地址: isboyjc/blog 傳送門前端
簡單來講跨域是指一個域下的文檔或腳本想要去去請求另外一個域下的資源vue
其實一些像A連接、重定向、表單提交的資源跳轉,像 <link>、<script>、<img>、<frame>
等dom標籤,還有樣式中 background:url()、@font-face()
等嵌入的文件外鏈,又好比一些像 js 發起的ajax請求、dom 和 js 對象的跨域操做等等都是跨域java
咱們一般所說的跨域,大可能是由瀏覽器同源策略限制引發的一類請求場景,這裏你可能注意到了同源策略,那麼瀏覽器的同源策略是什麼呢?node
同源策略/SOP(Same origin policy)是一種約定,由 Netscape
公司1995年引入瀏覽器,它是瀏覽器最核心也最基本的安全功能,若是缺乏了同源策略,瀏覽器很容易受到 XSS、CSFR 等攻擊webpack
同源同源,什麼是源呢?源指的是 協議、域名、端口
,那麼同源即三者相同,即使是不一樣的域名指向同一個ip地址,也不一樣源nginx
咱們來看一個域名組成,咱們以 http://www.hahaha.com/abc/a.js
爲例git
http://
默認端口是80)那麼咱們以這個域名的源爲例,來與下面這些作下對比
URL | 結果 | 緣由 |
---|---|---|
http://www.hahaha.com/abc/b.js |
同源 | 只有路徑不一樣 |
http://www.hahaha.com/def/b.js |
同源 | 只有路徑不一樣 |
https://www.hahaha.com/abc/a.js |
不一樣源 | 協議不一樣 |
http://www.hahaha.com:8081/abc/a.js |
不一樣源 | 端口不一樣 |
http://aaa.hahaha.com/abc/a.js |
不一樣源 | 主機不一樣 |
而在不一樣源的狀況下,同源策略限制了咱們
到了這裏,相信你對跨域已經有所瞭解了,那麼咱們如何有效的規避跨域呢,應該說如何解決跨域問題,由於咱們在開發過程當中免不了要跨域,針對不一樣的類型,解決跨域的方式也有不少
document.domain
的方式實現跨域,適用場景僅在 主域名相同,子級域名不一樣 的狀況下
例如,下面這兩個頁面
http://aaa.hahaha.com/a.html
http://bbb.hahaha.com/b.html
複製代碼
那麼它能夠作到什麼呢
document.domain
,共享Cookiedocument.domain
,經過 iframe
實現兩個頁面的數據互通首先,兩個頁面都設置相同的 document.domain
document.domain = 'hahaha.com'; 複製代碼
頁面 a 經過腳本設置一個 Cookie
document.cookie = "test=a"; 複製代碼
網頁 b 讀這個 Cookie
let cookieA = document.cookie; console.log(cookieA) 複製代碼
服務器也能夠在設置Cookie的時候,指定Cookie的所屬域名爲一級域名,好比.hahaha.com
Set-Cookie: key=value; domain=.hahaha.com; path=/
複製代碼
這樣的話,二級域名和三級域名不用作任何設置,均可以讀取這個Cookie
<!--a頁面--> <iframe src="http://bbb.hahaha.com/b.html" onload="load()" id="frame"></iframe> <script> document.domain = 'hahaha.com'; let a = "this is a"; // 獲取b頁面數據 function load(){ let frame = document.getElementById("frame") console.log(frame.contentWindow.b) // this is b } </script> 複製代碼
<!--b頁面--> <script> document.domain = 'hahaha.com'; let b = "this is b" // 獲取a頁面數據 console.log(window.parent.a); // this is a </script> 複製代碼
兩個頁面不一樣源,是沒法拿到對方DOM的,典型的例子就是 iframe
窗口和 window.open
方法打開的窗口,它們與父窗口是沒法通訊的
好比,不一樣源的頁面a和頁面b,若是咱們直接獲取對方數據
頁面a:http://www.hahaha0.com/a.html
<iframe src="http://www.hahaha1.com/b.html" onload="load()" id="frame"></iframe> <script> let a = "this is a" // 獲取b頁面數據 function load(){ console.log(document.getElementById("frame").contentWindow.b) // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame. } </script> 複製代碼
頁面b:http://www.hahaha1.com/b.html
<!--b--> <script> let b = "this is b" // 獲取a頁面數據 console.log(window.parent.a); // 報錯 </script> 複製代碼
顯而易見,都是獲取不到的,由於都跨域了,上面咱們講到的 document.domain
,只能在同主域名的狀況下使用才能規避同源政策,而在主域名不相同的狀況下是沒有辦法作到的
咱們來了解另外一種辦法 window.location.hash
,它拿到的是 URL 的#
號後面的部分,它叫片斷標識符(fragment identifier)
好比 http://hahaha.com/a.html#fragment
的 #fragment
,若是隻是改變片斷標識符,頁面是不會從新刷新的,就像大名鼎鼎的Vue中的hash路由就是用的這種方式
經過 location.hash
+ iframe
咱們能夠作到在不一樣主域下也能夠拿到對方的數據
首先,咱們要實現頁面a和頁面b的跨域相互通訊,由於不一樣域因此利用 iframe
加上 location.hash
傳值,可是這個傳值是單向的,只能由一方向另外一方傳值,不一樣域時子頁面並不能獲取到父頁面,也就不能相互通訊,因此咱們須要一箇中間人頁面c來幫忙
不一樣域之間利用 iframe
的location.hash
傳值,相同域之間直接 JS 訪問來通訊
那麼咱們的邏輯就變成了下面這樣
a 與 b 不一樣域只能經過hash值單向通訊,b 與 c 也不一樣域也只能單向通訊,但 c 與 a 同域,因此 c 可經過parent.parent 訪問 a 頁面全部對象
頁面a:http://www.hahaha0.com/a.html
<!--a中經過iframe引入了b--> <iframe id="frame" src="http://www.hahaha1.com/b.html"></iframe> <script> let frame = document.getElementById('frame'); // 向b傳hash值 frame.src = frame.src + '#a=我是a'; // 給同域c使用的回調方法 function cb(data) { console.log(data) // 打印 我是a+b } </script> 複製代碼
頁面b:http://www.hahaha1.com/b.html
<!--b中經過iframe引入了中間人c--> <iframe id="frame" src="http://www.hahaha0.com/c.html"></iframe> <script> let frame = document.getElementById('frame'); // 監聽a傳來的hash值,傳給c.html window.onhashchange = function () { frame.src = frame.src + location.hash + '+b'; }; </script> 複製代碼
頁面c:http://www.hahaha0.com/c.html
<script> // 監聽 b 的hash值變化 window.onhashchange = function () { // c調用父親的父親,來操做同域a的js回調,將結果傳回 window.parent.parent.cb(location.hash.replace('#a=', '')); }; </script> 複製代碼
window
對象有一個 name
屬性,該屬性有一個特徵,即在一個窗口的生命週期內,窗口載入全部的頁面都是共享一個 window.name
的,每個頁面對 window.name
都有讀寫的權限
window.name
是持久的存在於一個窗口載入的全部頁面中的,並不會由於新的頁面的載入而被重置,好比下例
頁面a
<script> window.name = '我是a'; setInterval(function(){ window.location = 'b.html'; // 兩秒後把一個新頁面b.html載入到當前的window中 },2000) </script> 複製代碼
頁面b
<script> console.log(window.name); // 我是a </script> 複製代碼
經過上面這個例子,咱們能夠很直觀的看到,a 頁面載入2s後,跳轉到 b 頁面,b 會在控制檯輸出 我是a
不過 window.name
的值只能是字符串的形式,最大容許2M左右,具體取決於不一樣的瀏覽器,可是通常是夠用了
那麼咱們就能夠利用它這一特性來實現跨域,看標題就知道是使用 window.name
和 iframe
,那麼你能想到要如何投機取巧,哦不,是巧妙的規避跨域而不留痕跡嗎?
經歷過上文的摧殘咱們知道,不一樣域狀況下的 a 頁面和 b 頁面,使用 iframe
嵌入一個頁面,數據也是互通不了的,由於會跨域,這裏咱們要使用 window.name
+ iframe
來實現跨域數據互通,顯然咱們不能直接在 a 頁面中經過改變 window.location
來載入b 頁面,由於咱們如今須要實現的是 a 頁面不跳轉,可是也可以獲取到 b 中的數據
究竟要怎麼實現呢?其實仍是要靠一箇中間人頁面 c
首先中間人 c 要和 a 是同域
a 頁面中經過
iframe
加載了 b ,在 b 頁面中把數據留在了當前iframe
窗口的window.name
屬性裏這個時候 a 是讀取不了
iframe
的,由於不一樣域,可是咱們能夠在 a 中動態的把iframe
的src
改成 c中間人 c 什麼都不用寫,由於它直接繼承了 b 留下的
window.name
由於c 和 a由於是同域,因此 a 能夠正常拿到子頁面 c 中的
window.name
屬性值不得不說,這種作法還真挺讓人歎爲觀止的,致敬前輩們
頁面a:http://www.hahaha1.com/abc/a.html
<iframe src="http://www.hahaha2.com/abc/b.html" id="frame" onload="load()"></iframe> <script> let flag = true // onload事件會觸發2次 // 第1次onload跨域頁b成功後,留下數據window.name,後切換到同域代理頁面 // 第2次onload同域頁c成功後,讀取同域window.name中數據 function load() { if(flag){ // 第1次 let frame = document.getElementById('frame') frame.src = 'http://www.hahaha1.com/abc/c.html' flag = false }else{ // 第二次 console.log(frame.contentWindow.name) // 我是b } } </script> 複製代碼
頁面b:http://www.hahaha2.com/abc/b.html
<script> window.name = '我是b' </script> 複製代碼
咱們上面說的幾種窗口跨域作法是能夠適用相應場景且安全可靠的,可是它們都是屬於投機取巧,不對,是另闢捷徑,可是HTML5 XMLHttpRequest Level 2
中爲了解決這個問題,引入了一個全新的API:跨文檔通訊 API(Cross-document messaging)
這個API爲 window
對象新增了一個 window.postMessage
方法,能夠容許來自不一樣源的腳本採用異步方式進行有限的通訊,能夠實現跨文本檔、多窗口、跨域消息傳遞
主流瀏覽器的兼容狀況也很是可觀
咱們來看下它的使用,先來看看它怎麼發送數據
otherWindow.postMessage(message, targetOrigin, [transfer]);
複製代碼
iframe
的 contentWindow
屬性,執行 window.open
返回的窗口對象,或者是命名過的或數值索引的 window.frames
origin
屬性來指定哪些窗口能接收到消息事件,指定後只有對應 origin
下的窗口才能夠接收到消息,設置爲通配符 *
表示能夠發送到任何窗口,但一般處於安全性考慮不建議這麼作,若是想要發送到與當前窗口同源的窗口,可設置爲 /
message
同時傳遞的 Transferable 對象,這些對象的全部權將被轉移給消息的接收方,而發送一方將再也不保有全部權它也能夠監聽 message
事件的發生來接收數據
window.addEventListener("message", receiveMessage, false) function receiveMessage(event) { let origin= event.origin console.log(event) } 複製代碼
接下來咱們實戰下跨域狀況下,經過 window.postMessage
來互通數據
仍是以不一樣域的頁面 a 和 b 爲例子
頁面a:http://www.hahaha1.com/abc/a.html
,建立跨域 iframe
併發送信息
<iframe src="http://www.hahaha2.com/abc/b.html" id="frame" onload="load()"></iframe> <script> function load() { let frame = document.getElementById('frame') // 發送 frame.contentWindow.postMessage('哈嘍,我是a', 'http://www.hahaha2.com/abc/b.html') // 接收 window.onmessage = function(e) { console.log(e.data) // 你好,我是b } } </script> 複製代碼
頁面b:http://www.hahaha2.com/abc/b.html
,接收數據並返回信息
<script> // 接收 window.onmessage = function(e) { console.log(e.data) // 哈嘍,我是a // 返回數據 e.source.postMessage('你好,我是b', e.origin) } </script> 複製代碼
對於 JSONP
這塊,雖然不經常使用,咱們好好的提一下,由於遇到過一些初學者,把 AJAX
和 JSONP
混爲一談了,提起 JSONP
,會說很 easy,就是在 AJAX
請求裏設置一下字段就好了,可能你用過 JQuery
封裝後的 JSONP
跨域方式,確實只是在請求里加個字段,可是,那是 JQ 封裝好的一種使用方式而已,可不能被表象迷惑,你真的懂它的原理嗎(JQ:我可不背鍋!!!)
Ajax
的原理簡單來講經過瀏覽器的 javascript
對象 XMLHttpRequest
(Ajax引擎)對象向服務器發送異步請求並接收服務器的響應數據,而後用 javascript
來操做 DOM 而更新頁面
這其中最關鍵的一步就是從服務器得到請求數據,即用戶的請求間接經過 Ajax
引擎發出而不是經過瀏覽器直接發出,同時 Ajax
引擎也接收服務器返回響應的數據,因此不會致使瀏覽器上的頁面所有刷新
使用方式也很簡單
一:建立XMLHttpRequest對象,也就是建立一個異步調用對象
二:建立一個新的HTTP請求,並指定該HTTP請求的方法、URL及驗證信息
三:設置響應HTTP請求狀態變化的函數
四:發送HTTP請求
五:獲取異步調用返回的數據
複製代碼
JSON(JavaScript Object Notation)
你們應該是很瞭解,就是一種輕量級的數據交換格式,不瞭解的同窗能夠去json.org 上了解下,分分鐘搞定
而 JSONP(JSON with Padding)
,它是一個 非官方 的協議,它容許在服務器端集成 Script tags
返回至客戶端,經過 javascript callback
的形式實現跨域訪問,這就是簡單的JSONP實現形式,這麼說可能不太明白,那咱們來看下它究竟是怎麼個原理
先來看個小例子,仍是不一樣域的 a 和 b 兩頁面
頁面a:http://www.hahaha1.com/abc/a.html
<html> <head> <title>test</title> <script type="text/javascript" src="http://www.hahaha2.com/abc/b.html"></script> </head> <body> <script> console.log(b) // 我是b </script> </body> </html> 複製代碼
頁面b:http://www.hahaha2.com/abc/b.js
var b = "我是b" 複製代碼
能夠看到,雖然不一樣域,可是 a 頁面中仍是能夠訪問到並打印出了 b 頁面中的變量
這個小例子咱們能夠很直觀的看到 <script>
標籤的 src 屬性並不被同源策略所約束,因此能夠獲取任何服務器上腳本並執行它,這就是 JSONP
最核心的原理了,至於它如何傳遞數據,咱們來簡單實現一個
剛纔的例子說了跨域的原理,並且咱們以前有講到 javascript callback
的形式實現跨域訪問,那咱們就來修改下代碼,如何實現 JSONP
的 javascript callback
的形式
頁面a:http://www.hahaha1.com/abc/a.html
<script type="text/javascript"> //回調函數 function cb(res) { console.log(res.data.b) // 我是b } </script> <script type="text/javascript" src="http://www.hahaha2.com/abc/b.js"></script> 複製代碼
頁面b:http://www.hahaha2.com/abc/b.js
var b = "我是b" // 調用cb函數,並以json數據形式做爲參數傳遞 cb({ code:200, msg:"success", data:{ b: b } }) 複製代碼
建立一個回調函數,而後在遠程服務上調用這個函數而且將JSON 數據形式做爲參數傳遞,完成回調,就是 JSONP
的簡單實現模式,或者說是 JSONP
的原型,是否是很簡單呢
將 JSON
數據填充進回調函數,如今懂爲何 JSONP
叫 JSON with Padding
了吧
上面這種實現很簡單,一般狀況下,咱們但願這個 script
標籤可以動態的調用,而不是像上面由於固定在 HTML
裏面加載時直接執行了,很不靈活,咱們能夠經過 javascript
動態的建立 script
標籤,這樣咱們就能夠靈活調用遠程服務了,那麼咱們簡單改造下頁面 a 以下
<script type="text/javascript"> function cb(res) { console.log(res.data.b) // 我是b } // 動態添加 <script> 標籤方法 function addScriptTag(src){ let script = document.createElement('script') script.setAttribute("type","text/javascript") script.src = src document.body.appendChild(script) } window.onload = function(){ addScriptTag("http://www.hahaha2.com/abc/b.js") } </script> 複製代碼
如上所示,只是些基礎操做,就不解釋了,如今咱們就能夠優雅的控制執行了,再想調用一個遠程服務的話,只要添加 addScriptTag
方法,傳入遠程服務的 src 值就能夠
接下來咱們就能夠愉快的進行一次真正意義上的 JSONP
服務調取了
咱們使用 jsonplaceholder
的 todos
接口做爲示例,接口地址以下
https://jsonplaceholder.typicode.com/todos?callback=?
複製代碼
callback=?
這個拼在接口後面表示回調函數的名稱,也就是將你本身在客戶端定義的回調函數的函數名傳送給服務端,服務端則會返回以你定義的回調函數名的方法,將獲取的 JSON
數據傳入這個方法完成回調,咱們的回調函數名字叫 cb
,那麼完整的接口地址就以下
https://jsonplaceholder.typicode.com/todos?callback=cb
複製代碼
那麼話很少說,咱們來試下
<script type="text/javascript"> function cb(res) { console.log(res) } function addScriptTag(src){ let script = document.createElement('script') script.setAttribute("type","text/javascript") script.src = src document.body.appendChild(script) } window.onload = function(){ addScriptTag("https://jsonplaceholder.typicode.com/todos?callback=cb") } </script> 複製代碼
能夠看到,頁面在加載完成後,輸出了接口返回的數據,這個時候咱們再來看 JQ 中的 JSONP 實現
仍是用上面的接口,咱們來看 JQ 怎麼拿數據
$.ajax({ url:"https://jsonplaceholder.typicode.com/todos?callback=?", dataType:"jsonp", jsonpCallback:"cb", success: function(res){ console.log(res) } }); 複製代碼
能夠看到,爲了讓 JQ 按照 JSONP
的方式訪問,dataType
字段設置爲 jsonp
, jsonpCallback
屬性的做用就是自定義咱們的回調方法名,其實內部和咱們上面寫的差很少
調用方式上
AJAX
和 JSONP
很像,都是請求url,而後把服務器返回的數據進行處理JQuery
的庫只是把 JSONP
做爲 AJAX
請求的一種形式進行封裝,不要搞混核心原理上
AJAX
的核心是經過 xmlHttpRequest
獲取非本頁內容JSONP
的核心是動態添加 script
標籤調用服務器提供的 JS 腳本,後綴 .json
二者區別上,
AJAX
不一樣域會報跨域錯誤,不過也能夠經過服務端代理、CORS
等方式跨域,而 JSONP
沒有這個限制,同域不一樣域均可以JSONP
是一種方式或者說非強制性的協議,AJAX
也不必定非要用 json
格式來傳遞數據 JSONP
只支持 GET
請求,AJAX
支持 GET
和 POST
最後,JSONP是很老的一種跨域方式了,如今基本沒什麼人用,因此,咱們瞭解懂它便可
通常狀況下,咱們但願這個script標籤可以動態的調用,而不是像上面由於固定在html裏面因此沒等頁面顯示就執行了,很不靈活。咱們能夠經過javascript動態的建立script標籤,這樣咱們就能夠靈活調用遠程服務了
在出現 CORS
以前,咱們都是使用 JSONP
的方式實現跨域,可是這種方式僅限於 GET
請求,而 CORS
的出現,爲咱們很好的解決了這個問題,這也是它成爲一個趨勢的緣由
CORS
是一個W3C標準,全稱是 跨域資源共享(Cross-origin resource sharing)
它容許瀏覽器向跨源服務器,發出 XMLHttpRequest
請求,從而克服了 AJAX
只能同源使用的限制
CORS
須要瀏覽器和服務器同時支持,目前基本全部瀏覽器都支持該功能,IE瀏覽器不低於 IE10 便可
整個 CORS
通訊過程,都是瀏覽器自動完成,是不須要用戶參與的,對於咱們開發者來講,CORS
通訊與同源的 AJAX
通訊沒有差異,代碼徹底同樣,瀏覽器一旦發現 AJAX
請求跨源,就會自動添加一些附加的頭信息,有的時候還會多出一次附加的請求,但這個過程當中用戶是無感的
所以,實現 CORS
通訊的關鍵是服務器,只要服務器設置了容許的 CORS
接口,就能夠進行跨源通訊,要了解怎麼實現 CORS
跨域通訊,咱們還要先了解瀏覽器對每一個請求都作了什麼
瀏覽器會將 CORS
請求分紅兩類,簡單請求(simple request)和非簡單請求(not-so-simple request),瀏覽器對這兩種請求的處理,是不同的
什麼是簡單請求,其實很好理解記住兩條就行了
HEAD、GET、POST
三種方法之一application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)只要同時知足這兩個條件,那麼這個請求就是一個簡單請求
對於簡單請求來講,瀏覽器會直接發出CORS請求,就是在這個請求的頭信息中,自動添加一個 Origin
字段來講明本次請求的來源(協議 + 域名 + 端口),然後服務器會根據這個值,決定是否贊成此次請求
知道了簡單請求的定義,非簡單請求就比較簡單了,由於只要不是簡單請求,它就是非簡單請求
瀏覽器應對非簡單請求,會在正式通訊以前,作一次查詢請求,叫預檢請求(preflight),也叫 OPTIONS
請求,由於它使用的請求方式是 OPTIONS
,這個請求是用來詢問的
瀏覽器會先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些HTTP動詞和頭信息字段,只有獲得確定答覆,瀏覽器纔會發出正式的 XMLHttpRequest
請求,不然就會報跨域錯誤
在這個預檢請求裏,頭信息除了有代表來源的 Origin
字段外,還會有一個 Access-Control-Request-Method
字段和 Access-Control-Request-Headers
字段,它們分別代表了該瀏覽器 CORS
請求用到的 HTTP
請求方法和指定瀏覽器 CORS
請求會額外發送的頭信息字段,若是你看的雲裏霧裏,不要着急,咱們看個例子
以下爲一個 AJAX 請求示例
let url = 'http://www.hahaha.com/abc' let xhr = new XMLHttpRequest() xhr.open('POST', url, true) xhr.setRequestHeader('X-Token', 'YGJHJHGJAHSGJDHGSJGJHDGSJHS') xhr.setRequestHeader('X-Test', 'YGJHJHGJAHSGJDHGSJGJHDGSJHS') xhr.send() 複製代碼
這個例子中,咱們發送了一個POST請求,並在它的請求頭中添加了一個自定義的 X-Token
和 X-Test
字段,由於添加了自定義請求頭字段,因此它是一個非簡單請求
那麼這個非簡單請求在預檢請求頭信息中就會攜帶如下信息
// 來源
Origin: http://www.hahaha.com
// 該CORS請求的請求方法
Access-Control-Request-Method: POST
// 額外發出的頭信息字段
Access-Control-Request-Headers: X-Token, X-Test
複製代碼
CORS 請求默認不發送 Cookie 和 HTTP 認證信息
若是要把 Cookie 發到服務端,首先要服務端贊成,指定Access-Control-Allow-Credentials
字段
Access-Control-Allow-Credentials: true
複製代碼
其次,客戶端必須在發起的請求中打開 withCredentials
屬性
xhr = new XMLHttpRequest() xhr.withCredentials = true 複製代碼
否則的話,服務端和客戶端有一個沒設置,就不會發送或處理Cookie
雖然說瀏覽器默認不發送 Cookie 和 HTTP 認證信息,可是有的瀏覽器,仍是會一塊兒發送Cookie,這時你也能夠顯式關閉 withCredentials
xhr.withCredentials = false 複製代碼
注意,如要發送 Cookie
,Access-Control-Allow-Origin
字段就不能設爲星號,必須指定明確的、與請求網頁一致的域名,同時,Cookie
依然遵循同源政策,只有用服務器域名設置的 Cookie
纔會上傳,其餘域名的 Cookie
並不會上傳,且(跨源)原網頁代碼中的 document.cookie
也沒法讀取服務器域名下的 Cookie
,下面還會提到
上面的東西只是爲了讓咱們理解CORS,可是要解決它仍是須要服務端配置的,不一樣語言的配置項語法上可能有差別,可是內容確定都是同樣的
配置容許跨域的來源
Access-Control-Allow-Origin: *
複製代碼
CORS
跨域請求中,最關鍵的就是 Access-Control-Allow-Origin
字段,是必需項,它表示服務端容許跨域訪問的地址來源,你能夠寫入須要跨域的域名,也能夠設爲星號,表示贊成任意跨源請求
注意,將此字段設置爲 *
是很不安全的,建議指定來源,而且設置爲 *
號後,遊覽器將不會發送 Cookie
,即便你的 XHR
設置了 withCredentials
,也不會發送 Cookie
配置容許跨域請求的方法
Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT...
複製代碼
該字段也是必需項,它的值是逗號分隔的一個字符串,代表服務器支持的全部跨域請求的方法
配置容許的請求頭字段
Access-Control-Allow-Headers: x-requested-with,content-type...
複製代碼
若是你的請求中有自定義的請求頭字段,那麼此項也是必須的,它也是一個逗號分隔的字符串,代表服務器支持的全部頭信息字段,不限於瀏覽器在預檢中請求的字段
配置是否容許發送Cookie
Access-Control-Allow-Credentials: true
複製代碼
該字段可選,它的值是一個布爾值,表示是否容許發送Cookie,默認狀況下,Cookie不包括在CORS請求之中
設爲true
,即表示服務器明確許可,Cookie能夠包含在請求中,一塊兒發給服務器
該字段只能設爲true
,若是服務器不要瀏覽器發送Cookie,刪除該字段便可
配置本次預檢請求的有效期
Access-Control-Max-Age: 1728000
複製代碼
該字段可選,用來指定本次預檢請求的有效期,單位爲秒,上面結果中,有效期是20天(1728000秒),即容許緩存該條迴應20天,在此期間若是你再次發出了這個接口請求,就不用發預檢請求了,節省服務端資源
對於咱們開發時,在跨域中最容易碰釘子的地方就是預檢請求,因此列舉幾個預檢請求錯誤的緣由,知道哪錯了能夠直接找後端同窗理論,關於預檢請求,最終目的只有一個,客戶端發送預檢,服務端容許並返回200便可
OPTIONS 404
No 'Access-Control-Allow-Origin' header is present on the requested resource
且 The response had HTTP status code 404
複製代碼
服務端沒有設置容許 OPTIONS
請求,那麼在發起該預檢請求時響應狀態碼會是404,由於沒法找到對應接口地址
那麼你可能須要找到後端,優雅的告訴他,請容許下 OPTIONS
請求
OPTIONS 405
No 'Access-Control-Allow-Origin' header is present on the requested resource
且 The response had HTTP status code 405
複製代碼
服務端已經容許了 OPTIONS
請求,可是一些配置文件中(如安全配置)阻止了 OPTIONS
請求
那麼你可能須要找到後端,優雅的告訴他,請關閉對應的安全配置
OPTIONS 200
No 'Access-Control-Allow-Origin' header is present on the requested resource
且 OPTIONS 請求 status 爲 200
複製代碼
服務器端容許了 OPTIONS
請求,配置文件中也沒有阻止,可是頭部匹配時出現不匹配現象
所謂頭部匹配,就好比 Origin
頭部檢查不匹配,或者少了一些頭部的支持(如 X-Requested-With
等),而後服務端就會將 Response
返回給前端,前端檢測到這個後就觸發 XHR.onerror
,從而致使報錯
那麼你可能須要找到後端,優雅的告訴他,請增長對應的頭部支持
OPTIONS 500
這個就更簡單了,服務端針對 OPTIONS
請求的代碼出了問題,或者沒有響應
那麼你可能須要找到後端,將 Network
中的錯誤信息截一圖發給他,優雅的告訴他,檢測到預檢請求時,請把它搞成200
瀏覽器跨域訪問 js/css/img
等常規靜態資源時被同源策略許可的,但 iconfont
字體文件好比 eot|otf|ttf|woff|svg
例外,此時可在 Nginx
的靜態資源服務器中加入如下配置來解決
location / {
add_header Access-Control-Allow-Origin *;
}
複製代碼
咱們知道同源策略只是 瀏覽器 的安全策略,不是 HTTP
協議的一部分, 服務器端調用 HTTP
接口只是使用 HTTP
協議,不會執行 JS 腳本,不須要同源策略,也就不存在跨越問題
通俗點說就是客戶端瀏覽器發起一個請求會存在跨域問題,可是服務端向另外一個服務端發起請求並沒有跨域,由於跨域問題歸根結底源於同源策略,而同源策略只存在於瀏覽器
那麼咱們是否是能夠經過 Nginx
配置一個代理服務器,反向代理訪問跨域的接口,而且咱們還能夠修改 Cookie
中 domain
信息,方便當前域 Cookie
寫入
Nginx
其實就是各類配置,簡單易學,就算沒接觸過,也很好理解,咱們來看示例
首先假如咱們的頁面 a 在 http://www.hahaha.com
域下,可是咱們的接口卻在 http://www.hahaha1.com:9999
域下
接着咱們在頁面 a 發起一個 AJAX
請求時,就會跨域,那麼咱們就能夠經過 Nginx
配置一個代理服務器,域名和頁面 a 相同,都是 http://www.hahaha.com
,用它來充當一個跳板的角色,反向代理訪問 http://www.hahaha1.com
接口
# Nginx代理服務器 server { listen 80; server_name www.hahaha.com; location / { # 反向代理地址 proxy_pass http://www.hahaha1.com:9999; # 修改Cookie中域名 proxy_cookie_domain www.hahaha1.com www.hahaha.com; index index.html index.htm; # 前端跨域攜帶了Cookie,因此Allow-Origin配置不可爲* add_header Access-Control-Allow-Origin http://www.hahaha.com; add_header Access-Control-Allow-Credentials true; } } 複製代碼
沒錯,這個代理配置相信沒接觸過 Nginx
也能看明白,大部分都是咱們上文提到過的,是否是很簡單呢
Node
實現跨域代理,與 Nginx
道理相同,都是啓一個代理服務器,就像咱們經常使用的 Vue-CLI
配置跨域,其實也是 Node
啓了一個代理服務,接下來咱們來看看是如何作的
Vue-CLI 是基於 webpack
的,經過 webpack-dev-server
在本地啓動腳手架,也就是在本地啓動了一個 Node
服務,來實時監聽和打包編譯靜態資源,因爲都是封裝好的,只須要配置便可,咱們在 vue.config.js
中配置代理以下,寫法不少,列幾個常見的自行選擇
使用一
module.exports = { //... devServer: { proxy: { '/api': 'http://www.hahaha.com' } } } 複製代碼
如上所示時,當你請求 /api/abc
接口時就會被代理到 http://www.hahaha.com/api/abc
使用二
固然,你可能想將多個路徑代理到同一個 target
下,那你可使用下面這種方式
module.exports = { //... devServer: { proxy: [{ context: ['/api1', '/api2', '/api3'], target: 'http://www.hahaha.com', }] } } 複製代碼
使用三
正如咱們第一種使用方式代理時,代理了 /api
,最終的代理結果是 http://www.hahaha.com/api/abc
,可是有時咱們並不想代理時傳遞 /api
,那麼就可使用下面這種方式,經過 pathRewrite
屬性來進行路徑重寫
module.exports = { //... devServer: { proxy: { '/api': { target: 'http://www.hahaha.com', pathRewrite: {'^/api' : ''} } } } } 複製代碼
這個時候,/api/abc
接口就會被代理到 http://www.hahaha.com/abc
使用四
默認狀況下,咱們代理是不接受運行在 HTTPS
上,且使用了無效證書的後端服務器的
若是你想要接受,須要設置 secure: false
,以下
module.exports = { //... devServer: { proxy: { '/api': { target: 'https://www.hahaha.com', secure: false } } } } 複製代碼
使用五
配置一個字段 changeOrigin
,當它爲 true
時,本地就會虛擬一個服務器接收你的請求而且代你發送該請求,因此若是你要代理跨域,這個字段是必選項
module.exports = { // ... devServer: { proxy: { "/api": { target: 'http://www.hahaha.com', changeOrigin: true, } } } } 複製代碼
使用六
若是你想配置多個不一樣的代理,也簡單,以下所示,能夠在任意代理中設置對應的代理規則
module.exports = { // ... devServer: { proxy: { "/api1": { target: 'http://www.hahaha1.com', changeOrigin: true }, "/api2": { target: 'http://www.hahaha2.com', pathRewrite: {'^/api2' : ''} }, "/api3": { target: 'http://www.hahaha3.com', changeOrigin: true, pathRewrite: {'^/api3' : ''} } // ... } } } 複製代碼
注意,在本地配置代理跨域,只是解決開發時的跨域問題,當你的項目上線時,前端靜態文件和後端在一個域下沒有問題,若是並不在一個域下,依然會報跨域錯誤,這個時候還得須要後端配置跨域
這裏咱們使用 express + http-proxy-middleware
來搭建一個代理服務器,使用 http-proxy-middleware
這個中間件沒有別的意思,只是由於 webpack-dev-server
裏就是使用的它
let express = require('express') let proxy = require('http-proxy-middleware') let app = express() app.use('/', proxy({ // 代理跨域目標接口 target: 'http://www.hahaha1.com:9999', changeOrigin: true, // 修改響應頭信息,實現跨域並容許帶cookie onProxyRes: function(proxyRes, req, res) { res.header('Access-Control-Allow-Origin', 'http://www.hahaha.com') res.header('Access-Control-Allow-Credentials', 'true') }, // 修改響應信息中的cookie域名,爲false時,表示不修改 cookieDomainRewrite: 'www.hahaha.com' })) app.listen(3000) 複製代碼
WebSocket
是一種在單個 TCP 鏈接上進行全雙工通訊的協議,2008年誕生,2011年被 IETF 定爲標準 RFC 6455
,並由 RFC7936
補充規範,WebSocket API
也被 W3C 定爲標準
WebSocket
使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據, 在 WebSocket API
中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸,同時,它也是跨域的一種解決方案
創建在 TCP 協議之上,服務器端的實現比較容易
與 HTTP 協議有着良好的兼容性,默認端口也是 80 和 443,而且握手階段採用 HTTP 協議,所以握手時不容易屏蔽,能經過各類 HTTP 代理服務器
數據格式比較輕量,性能開銷小,通訊高效
能夠發送文本,也能夠發送二進制數據
沒有同源限制,客戶端能夠與任意服務器通訊
協議標識符是 ws
(若是加密,則爲 wss
),服務器網址就是 URL
以下
ws://www.hahaha.com:80/abc/def
複製代碼
每一個服務端語言對 websocket
有相應的支持,寫法不一樣罷了,這裏咱們使用 Node
作示例
在客戶端咱們能夠直接使用 HTML5 的 websocket API
,服務端也可使用 nodejs-websocket
實現 websocket server
,可是不建議這樣作,由於原生 WebSocket API
使用起有些複雜,在瀏覽器的兼容性上還不夠理想,因此咱們使用 Socket.io
,它很好地封裝了 webSocket
接口,提供了更簡單、靈活的接口,也對不支持 webSocket
的瀏覽器提供了向下兼容,使用 Socket.io
庫實現 websocket
,在發送數據時能夠直接發送可序列化的對象,也能夠自定義消息,利用事件字符串來區分不一樣消息,整個開發過程會舒服不少
想要了解更多看官網便可 Socket.io - 傳送門 ,咱們來看示例
客戶端:http://www.hahaha.com/a.html
<script src="/socket.io/socket.io.js"></script> <script> let socket = io.connect('http://www.hahaha1.com:3000') socket.on('my event', (data) => { console.log(data) // { hello: 'world' } socket.emit('my other event', { my: 'data' }) }) </script> 複製代碼
服務端:http://www.hahaha1.com:3000
const app = require('express').createServer() const io = require('socket.io')(app) app.listen(3000) io.on('connection', (socket) => { socket.emit('my event', { hello: 'world' }) socket.on('my other event', (data) => { console.log(data) // { my: 'data' } }) }) 複製代碼
如上所示,使用了 Socket.io
以後的 websocket
鏈接是否是超級簡單呢,跟着文檔本身動手試試吧
歡迎你們關注公衆號「不正經的前端」,時不時發一篇文章,也沒有花裏胡哨的推廣和廣告,但願可讓你們隨意點開一篇文章,均可以看到滿滿的乾貨,也能夠直接加機器人好友備註「前端 | 後端 | 全棧」自動經過,加交流羣,閒聊、吐槽、解決問題、交朋友均可以,固然技術爲主
按照時間線貼下了總結的比較全的幾個帖子,還有其餘的瑣碎的文章,比較多就不貼了,這些文章都寫的差很少,可能之間有互相抄襲,有互相借鑑,這些都是避免不了的,此文寫的時候也借鑑了這些文章,只不過我手敲了一遍例子,又用我本身的理解碼下來了,爲此花了1周的業餘時間,內容上與下面做者寫的有些許雷同,那實屬無奈,能夠說是知識點就那麼多,你們的總結稍有不一樣的地方就剩表達的語法,我也難受,還特地找了工具鑑別了下類似度,以避免被誤會,畢竟我也特別反感搬運工,嗯,又是一個深夜,終於收工了,睡覺嘍,哦對了,碼字不易,望點贊,若有錯誤,望指出,謝謝支持
參考文章
瀏覽器同源政策及其規避方法 - 阮一峯 - 2016.04
跨域資源共享 CORS 詳解 - 阮一峯 - 2016.04
前端跨域整理 - 思否 damonare - 2016.10
前端常見跨域解決方案(全)- 思否 安靜de沉澱 - 2017.07
正確面對跨域,別慌 - 掘金 Neal_yang - 2017.12
九種跨域方式實現原理(完整版)- 掘金 浪裏行舟 - 2019.01
9種常見的前端跨域解決方案(詳解)- 掘金 小銘子 - 2019.07