<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          你真的了解跨域嗎

          共 28854字,需瀏覽 58分鐘

           ·

          2021-03-30 17:31

          點擊上方“前端簡報”,選擇“設(shè)為星標

          第一時間關(guān)注技術(shù)干貨!

          前言

          相信每個前端對于跨域這兩個字都不會陌生,在實際項目中應(yīng)用也很多,但跨域方法的多種多樣讓人目不暇接,前段時間公司同事出現(xiàn)了跨域問題,又一時找不到問題所在,所以在此總結(jié)下跨域知識,一篇由淺入深的萬字Web基操文

          其實很早就開始寫了,只不過剛開始寫的時候理解不夠深刻,后來慢慢就寫其他覺得較高大尚較內(nèi)涵的了,然后就又是覺得不夠完美不夠深刻又寫一半,就此陷入強迫癥患者明知不可為而為的死循環(huán),SO,產(chǎn)出少,周期長(不過大家能看到的文章都是準備良久又反復(fù)斟酌后自認為還不錯的)。。。

          總之又是一篇由于各種原因半途而廢的積壓文,這里終于收尾了,長出一口氣,哎,還是太年輕,吐槽結(jié)束,進入正文

          文章收錄地址:isboyjc/blog 傳送門[1]

          什么是跨域

          簡單來說跨域是指一個域下的文檔或腳本想要去去請求另一個域下的資源

          其實一些像A鏈接、重定向、表單提交的資源跳轉(zhuǎn),像 <link>、<script>、<img>、<frame> 等dom標簽,還有樣式中 background:url()、@font-face() 等嵌入的文件外鏈,又比如一些像  js 發(fā)起的ajax請求、dom 和 js 對象的跨域操作等等都是跨域

          我們通常所說的跨域,大多是由瀏覽器同源策略限制引起的一類請求場景,這里你可能注意到了同源策略,那么瀏覽器的同源策略是什么呢?

          瀏覽器同源策略

          同源策略/SOP(Same origin policy)是一種約定,由 Netscape 公司1995年引入瀏覽器,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,瀏覽器很容易受到 XSS、CSFR[2] 等攻擊

          同源同源,什么是源呢?源指的是 協(xié)議、域名、端口 ,那么同源即三者相同,即便是不同的域名指向同一個ip地址,也不同源

          我們來看一個域名組成,我們以 http://www.hahaha.com/abc/a.js 為例

          • http://            -->    協(xié)議
          • www              -->    子域名
          • hahaha.com -->    主域名
          • 80                   -->     端口(http:// 默認端口是80)
          • abc/a.js         -->     請求資源路徑

          那么我們以這個域名的源為例,來與下面這些做下對比

          URL結(jié)果原因
          http://www.hahaha.com/abc/b.js同源只有路徑不同
          http://www.hahaha.com/def/b.js同源只有路徑不同
          https://www.hahaha.com/abc/a.js不同源協(xié)議不同
          http://www.hahaha.com:8081/abc/a.js不同源端口不同
          http://aaa.hahaha.com/abc/a.js不同源主機不同

          而在不同源的情況下,同源策略限制了我們

          • Cookie、LocalStorage、IndexedDB 等存儲性內(nèi)容無法讀取
          • DOM 節(jié)點和 Js對象無法獲得
          • AJAX 請求發(fā)送后,結(jié)果被瀏覽器攔截(注意是 「請求發(fā)送出去了,也拿到結(jié)果了,只是被瀏覽器截胡了」

          到了這里,相信你對跨域已經(jīng)有所了解了,那么我們?nèi)绾斡行У囊?guī)避跨域呢,應(yīng)該說如何解決跨域問題,因為我們在開發(fā)過程中免不了要跨域,針對不同的類型,解決跨域的方式也有很多

          不同類型的跨域解決方案

          No.1 document.domain+iframe跨域

          簡介

          document.domain 的方式實現(xiàn)跨域,適用場景僅在 「主域名相同,子級域名不同」 的情況下

          例如,下面這兩個頁面

          http://aaa.hahaha.com/a.html
          http://bbb.hahaha.com/b.html

          那么它可以做到什么呢

          • 兩個頁面設(shè)置相同的 document.domain ,共享Cookie
          • 兩個頁面設(shè)置相同的 document.domain ,通過 iframe 實現(xiàn)兩個頁面的數(shù)據(jù)互通

          示例

          共享Cookie

          首先,兩個頁面都設(shè)置相同的 document.domain

          document.domain = 'hahaha.com';

          頁面 a 通過腳本設(shè)置一個 Cookie

          document.cookie = "test=a";

          網(wǎng)頁 b 讀這個 Cookie

          let cookieA = document.cookie;
          console.log(cookieA)

          服務(wù)器也可以在設(shè)置Cookie的時候,指定Cookie的所屬域名為一級域名,比如.hahaha.com

          Set-Cookie: key=value; domain=.hahaha.com; path=/

          這樣的話,二級域名和三級域名不用做任何設(shè)置,都可以讀取這個Cookie

          共享數(shù)據(jù)
          <!--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頁面數(shù)據(jù)
            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頁面數(shù)據(jù)
            console.log(window.parent.a); // this is a
          </script>

          局限

          • 首先,僅在主域名相同,子級域名不同的情況下
          • 只適用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 數(shù)據(jù)無法通過這種方法共享

          No.2 location.hash + iframe跨域

          簡介

          兩個頁面不同源,是無法拿到對方DOM的,典型的例子就是 iframe 窗口和 window.open 方法打開的窗口,它們與父窗口是無法通信的

          比如,不同源的頁面a和頁面b,如果我們直接獲取對方數(shù)據(jù)

          頁面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頁面數(shù)據(jù)
            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頁面數(shù)據(jù)
            console.log(window.parent.a); // 報錯
          </script>

          顯而易見,都是獲取不到的,因為都跨域了,上面我們講到的 document.domain,只能在同主域名的情況下使用才能規(guī)避同源政策,而在主域名不相同的情況下是沒有辦法做到的

          我們來了解另一種辦法 window.location.hash,它拿到的是 URL 的#號后面的部分,它叫片段標識符(fragment identifier)

          比如 http://hahaha.com/a.html#fragment#fragment ,如果只是改變片段標識符,頁面是不會重新刷新的,就像大名鼎鼎的Vue中的hash路由就是用的這種方式

          通過 location.hash + iframe 我們可以做到在不同主域下也可以拿到對方的數(shù)據(jù)

          示例

          首先,我們要實現(xiàn)頁面a和頁面b的跨域相互通信,因為不同域所以利用 iframe 加上 location.hash 傳值,但是這個傳值是單向的,只能由一方向另一方傳值,不同域時子頁面并不能獲取到父頁面,也就不能相互通信,所以我們需要一個中間人頁面c來幫忙

          不同域之間利用 iframelocation.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使用的回調(diào)方法
            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');

            // 監(jiān)聽a傳來的hash值,傳給c.html
            window.onhashchange = function ({
              frame.src = frame.src + location.hash + '+b';
            };
          </script>

          頁面c:http://www.hahaha0.com/c.html

          <script>
            // 監(jiān)聽 b 的hash值變化
            window.onhashchange = function ({
              // c調(diào)用父親的父親,來操作同域a的js回調(diào),將結(jié)果傳回
              window.parent.parent.cb(location.hash.replace('#a='''));
            };
          </script>

          No.3 window.name + iframe跨域

          簡介

          window 對象有一個 name 屬性,該屬性有一個特征,即在一個窗口的生命周期內(nèi),窗口載入所有的頁面都是共享一個 window.name 的,每一個頁面對 window.name 都有讀寫的權(quán)限

          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后,跳轉(zhuǎn)到 b 頁面,b 會在控制臺輸出 我是a

          不過 window.name 的值只能是字符串的形式,最大允許2M左右,具體取決于不同的瀏覽器,但是一般是夠用了

          那么我們就可以利用它這一特性來實現(xiàn)跨域,看標題就知道是使用 window.nameiframe ,那么你能想到要如何投機取巧,哦不,是巧妙的規(guī)避跨域而不留痕跡嗎?

          經(jīng)歷過上文的摧殘我們知道,不同域情況下的 a 頁面和 b 頁面,使用 iframe 嵌入一個頁面,數(shù)據(jù)也是互通不了的,因為會跨域,這里我們要使用 window.name + iframe 來實現(xiàn)跨域數(shù)據(jù)互通,顯然我們不能直接在 a 頁面中通過改變 window.location 來載入b 頁面,因為我們現(xiàn)在需要實現(xiàn)的是 a 頁面不跳轉(zhuǎn),但是也能夠獲取到 b 中的數(shù)據(jù)

          ?

          究竟要怎么實現(xiàn)呢?其實還是要靠一個中間人頁面 c

          首先中間人 c 要和 a 是同域

          a 頁面中通過 iframe 加載了 b ,在 b 頁面中把數(shù)據(jù)留在了當前 iframe 窗口的  window.name 屬性里

          這個時候 a 是讀取不了 iframe 的,因為不同域,但是我們可以在 a 中動態(tài)的把 iframesrc 改為 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事件會觸發(fā)2次
            // 第1次onload跨域頁b成功后,留下數(shù)據(jù)window.name,后切換到同域代理頁面
            // 第2次onload同域頁c成功后,讀取同域window.name中數(shù)據(jù)
            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>

          No.4 window.postMessage跨域

          簡介

          我們上面說的幾種窗口跨域做法是可以適用相應(yīng)場景且安全可靠的,但是它們都是屬于投機取巧,不對,是另辟捷徑,但是HTML5 XMLHttpRequest Level 2中為了解決這個問題,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)

          這個API為 window 對象新增了一個 window.postMessage 方法,可以允許來自不同源的腳本采用異步方式進行有限的通信,可以實現(xiàn)跨文本檔、多窗口、跨域消息傳遞

          主流瀏覽器的兼容情況也非常可觀

          我們來看下它的使用,先來看看它怎么發(fā)送數(shù)據(jù)

          otherWindow.postMessage(message, targetOrigin, [transfer]);
          • 「otherWindow」
            • 窗口的一個引用,比如 iframecontentWindow 屬性,執(zhí)行 window.open 返回的窗口對象,或者是命名過的或數(shù)值索引的 window.frames
          • 「message」
            • 要發(fā)送到其他窗口的數(shù)據(jù),它將會被 結(jié)構(gòu)化克隆算法 [3] 序列化,這意味著你可以不受什么限制的將數(shù)據(jù)對象安全的傳送給目標窗口而無需自己序列化
          • 「targetOrigin」
            • 通過窗口的 origin 屬性來指定哪些窗口能接收到消息事件,指定后只有對應(yīng) origin 下的窗口才可以接收到消息,設(shè)置為通配符 * 表示可以發(fā)送到任何窗口,但通常處于安全性考慮不建議這么做,如果想要發(fā)送到與當前窗口同源的窗口,可設(shè)置為 /
          • 「transfer | 可選屬性」
            • 是一串和 message 同時傳遞的 「Transferable」 對象,這些對象的所有權(quán)將被轉(zhuǎn)移給消息的接收方,而發(fā)送一方將不再保有所有權(quán)

          它也可以監(jiān)聽 message 事件的發(fā)生來接收數(shù)據(jù)

          window.addEventListener("message", receiveMessage, false)
          function receiveMessage(event{
            let origin= event.origin
            console.log(event)
          }

          接下來我們實戰(zhàn)下跨域情況下,通過 window.postMessage 來互通數(shù)據(jù)

          示例

          還是以不同域的頁面 a 和 b 為例子

          頁面a:http://www.hahaha1.com/abc/a.html,創(chuàng)建跨域 iframe 并發(fā)送信息

          <iframe src="http://www.hahaha2.com/abc/b.html" id="frame" onload="load()"></iframe>
          <script>
            function load({
              let frame = document.getElementById('frame')
              // 發(fā)送
              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,接收數(shù)據(jù)并返回信息

          <script>
            // 接收
            window.onmessage = function(e{
              console.log(e.data) // 哈嘍,我是a
              // 返回數(shù)據(jù)
              e.source.postMessage('你好,我是b', e.origin)
            }
          </script>

          No.5 JSONP跨域

          寫在前面

          對于 JSONP 這塊,雖然不常用,我們好好的提一下,因為遇到過一些初學者,把 AJAXJSONP 混為一談了,提起 JSONP ,會說很 easy,就是在 AJAX 請求里設(shè)置一下字段就行了,可能你用過 JQuery 封裝后的 JSONP 跨域方式,確實只是在請求里加個字段,但是,那是 JQ 封裝好的一種使用方式而已,可不能被表象迷惑,你真的懂它的原理嗎(JQ:我可不背鍋!!!)

          AJAX工作原理

          Ajax 的原理簡單來說通過瀏覽器的 javascript 對象 XMLHttpRequest (Ajax引擎)對象向服務(wù)器發(fā)送異步請求并接收服務(wù)器的響應(yīng)數(shù)據(jù),然后用 javascript 來操作 DOM 而更新頁面

          這其中最關(guān)鍵的一步就是從服務(wù)器獲得請求數(shù)據(jù),即用戶的請求間接通過 Ajax 引擎發(fā)出而不是通過瀏覽器直接發(fā)出,同時 Ajax 引擎也接收服務(wù)器返回響應(yīng)的數(shù)據(jù),所以不會導(dǎo)致瀏覽器上的頁面全部刷新

          使用方式也很簡單

          一:創(chuàng)建XMLHttpRequest對象,也就是創(chuàng)建一個異步調(diào)用對象

          二:創(chuàng)建一個新的HTTP請求,并指定該HTTP請求的方法、URL及驗證信息

          三:設(shè)置響應(yīng)HTTP請求狀態(tài)變化的函數(shù)

          四:發(fā)送HTTP請求

          五:獲取異步調(diào)用返回的數(shù)據(jù)

          JSONP,JSON?

          JSON(JavaScript Object Notation) 大家應(yīng)該是很了解,就是一種輕量級的數(shù)據(jù)交換格式,不了解的同學可以去json.org [4] 上了解下,分分鐘搞定

          JSONP(JSON with Padding) ,它是一個 「非官方」 的協(xié)議,它允許在服務(wù)器端集成 Script tags 返回至客戶端,通過 javascript callback 的形式實現(xiàn)跨域訪問,這就是簡單的JSONP實現(xiàn)形式,這么說可能不太明白,那我們來看下它到底是怎么個原理

          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 屬性并不被同源策略所約束,所以可以獲取任何服務(wù)器上腳本并執(zhí)行它,這就是 JSONP 最核心的原理了,至于它如何傳遞數(shù)據(jù),我們來簡單實現(xiàn)一個

          JSONP的CallBack實現(xiàn)

          剛才的例子說了跨域的原理,而且我們之前有講到 javascript callback 的形式實現(xiàn)跨域訪問,那我們就來修改下代碼,如何實現(xiàn) JSONPjavascript callback 的形式

          頁面a:http://www.hahaha1.com/abc/a.html

          <script type="text/javascript">
            //回調(diào)函數(shù)
            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"

          // 調(diào)用cb函數(shù),并以json數(shù)據(jù)形式作為參數(shù)傳遞
          cb({
            code:200
            msg:"success",
            data:{
              b: b
            }
          })

          創(chuàng)建一個回調(diào)函數(shù),然后在遠程服務(wù)上調(diào)用這個函數(shù)并且將JSON 數(shù)據(jù)形式作為參數(shù)傳遞,完成回調(diào),就是 JSONP 的簡單實現(xiàn)模式,或者說是 JSONP 的原型,是不是很簡單呢

          JSON 數(shù)據(jù)填充進回調(diào)函數(shù),現(xiàn)在懂為什么 JSONPJSON with Padding 了吧

          上面這種實現(xiàn)很簡單,通常情況下,我們希望這個 script 標簽?zāi)軌騽討B(tài)的調(diào)用,而不是像上面因為固定在 HTML 里面加載時直接執(zhí)行了,很不靈活,我們可以通過 javascript 動態(tài)的創(chuàng)建 script 標簽,這樣我們就可以靈活調(diào)用遠程服務(wù)了,那么我們簡單改造下頁面 a 如下

          <script type="text/javascript">
            function cb(res{
              console.log(res.data.b)  // 我是b
            }
            
            // 動態(tài)添加 <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>

          如上所示,只是些基礎(chǔ)操作,就不解釋了,現(xiàn)在我們就可以優(yōu)雅的控制執(zhí)行了,再想調(diào)用一個遠程服務(wù)的話,只要添加 addScriptTag 方法,傳入遠程服務(wù)的 src 值就可以

          接下來我們就可以愉快的進行一次真正意義上的 JSONP 服務(wù)調(diào)取了

          我們使用 jsonplaceholdertodos 接口作為示例,接口地址如下

          https://jsonplaceholder.typicode.com/todos?callback=?

          callback=? 這個拼在接口后面表示回調(diào)函數(shù)的名稱,也就是將你自己在客戶端定義的回調(diào)函數(shù)的函數(shù)名傳送給服務(wù)端,服務(wù)端則會返回以你定義的回調(diào)函數(shù)名的方法,將獲取的 JSON 數(shù)據(jù)傳入這個方法完成回調(diào),我們的回調(diào)函數(shù)名字叫 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>

          可以看到,頁面在加載完成后,輸出了接口返回的數(shù)據(jù),這個時候我們再來看 JQ 中的 JSONP 實現(xiàn)

          JSONP的JQuery實現(xiàn)

          還是用上面的接口,我們來看 JQ 怎么拿數(shù)據(jù)

          $.ajax({
            url:"https://jsonplaceholder.typicode.com/todos?callback=?",   
            dataType:"jsonp",
            jsonpCallback:"cb",
            successfunction(res){
              console.log(res)
            }
          });

          可以看到,為了讓 JQ 按照 JSONP 的方式訪問,dataType 字段設(shè)置為 jsonpjsonpCallback 屬性的作用就是自定義我們的回調(diào)方法名,其實內(nèi)部和我們上面寫的差不多

          JSONP和AJAX對比

          • 調(diào)用方式上

            • AJAXJSONP 很像,都是請求url,然后把服務(wù)器返回的數(shù)據(jù)進行處理
            • 所以類 JQuery 的庫只是把 JSONP 作為 AJAX 請求的一種形式進行封裝,不要搞混
          • 核心原理上

            • AJAX 的核心是通過 xmlHttpRequest 獲取非本頁內(nèi)容
            • JSONP的核心是動態(tài)添加 script 標簽調(diào)用服務(wù)器提供的 JS 腳本,后綴 .json
          • 兩者區(qū)別上,

            • AJAX 不同域會報跨域錯誤,不過也可以通過服務(wù)端代理、CORS 等方式跨域,而 JSONP 沒有這個限制,同域不同域都可以
            • JSONP 是一種方式或者說非強制性的協(xié)議,AJAX 也不一定非要用 json 格式來傳遞數(shù)據(jù)
            • JSONP 只支持 GET 請求,AJAX 支持 GETPOST

          最后,JSONP是很老的一種跨域方式了,現(xiàn)在基本沒什么人用,所以,我們了解懂它即可

          一般情況下,我們希望這個script標簽?zāi)軌騽討B(tài)的調(diào)用,而不是像上面因為固定在html里面所以沒等頁面顯示就執(zhí)行了,很不靈活。我們可以通過javascript動態(tài)的創(chuàng)建script標簽,這樣我們就可以靈活調(diào)用遠程服務(wù)了

          No.6 CORS跨域資源共享

          什么是CORS?

          在出現(xiàn) CORS 之前,我們都是使用 JSONP 的方式實現(xiàn)跨域,但是這種方式僅限于 GET 請求,而 CORS 的出現(xiàn),為我們很好的解決了這個問題,這也是它成為一個趨勢的原因

          CORS 是一個W3C標準,全稱是 跨域資源共享(Cross-origin resource sharing)

          它允許瀏覽器向跨源服務(wù)器,發(fā)出 XMLHttpRequest 請求,從而克服了 AJAX 只能同源使用的限制

          CORS 需要瀏覽器和服務(wù)器同時支持,目前基本所有瀏覽器都支持該功能,IE瀏覽器不低于 IE10 即可

          整個 CORS 通信過程,都是瀏覽器自動完成,是不需要用戶參與的,對于我們開發(fā)者來說,CORS 通信與同源的 AJAX 通信沒有差別,代碼完全一樣,瀏覽器一旦發(fā)現(xiàn) AJAX 請求跨源,就會自動添加一些附加的頭信息,有的時候還會多出一次附加的請求,但這個過程中用戶是無感的

          因此,實現(xiàn) CORS 通信的關(guān)鍵是服務(wù)器,只要服務(wù)器設(shè)置了允許的 CORS 接口,就可以進行跨源通信,要了解怎么實現(xiàn) CORS 跨域通信,我們還要先了解瀏覽器對每個請求都做了什么

          瀏覽器會將 CORS 請求分成兩類,簡單請求(simple request)和非簡單請求(not-so-simple request),瀏覽器對這兩種請求的處理,是不一樣的

          簡單請求

          什么是簡單請求,其實很好理解記住兩條就好了

          • 請求方法是 HEAD、GET、POST 三種方法之一
          • HTTP的頭信息不超出以下幾種字段
            • Accept
            • Accept-Language
            • Content-Language
            • Last-Event-ID
            • Content-Type(只限于三個值application/x-www-form-urlencodedmultipart/form-datatext/plain

          只要同時滿足這兩個條件,那么這個請求就是一個簡單請求

          對于簡單請求來說,瀏覽器會直接發(fā)出CORS請求,就是在這個請求的頭信息中,自動添加一個 Origin 字段來說明本次請求的來源(協(xié)議 + 域名 + 端口),而后服務(wù)器會根據(jù)這個值,決定是否同意這次請求

          非簡單請求

          知道了簡單請求的定義,非簡單請求就比較簡單了,因為只要不是簡單請求,它就是非簡單請求

          瀏覽器應(yīng)對非簡單請求,會在正式通信之前,做一次查詢請求,叫預(yù)檢請求(preflight),也叫 OPTIONS 請求,因為它使用的請求方式是 OPTIONS ,這個請求是用來詢問的

          瀏覽器會先詢問服務(wù)器,當前網(wǎng)頁所在的域名是否在服務(wù)器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段,只有得到肯定答復(fù),瀏覽器才會發(fā)出正式的 XMLHttpRequest 請求,否則就會報跨域錯誤

          在這個預(yù)檢請求里,頭信息除了有表明來源的 Origin 字段外,還會有一個 Access-Control-Request-Method 字段和 Access-Control-Request-Headers 字段,它們分別表明了該瀏覽器 CORS 請求用到的 HTTP 請求方法和指定瀏覽器 CORS 請求會額外發(fā)送的頭信息字段,如果你看的云里霧里,不要著急,我們看個例子

          如下為一個 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()

          這個例子中,我們發(fā)送了一個POST請求,并在它的請求頭中添加了一個自定義的 X-TokenX-Test 字段,因為添加了自定義請求頭字段,所以它是一個非簡單請求

          那么這個非簡單請求在預(yù)檢請求頭信息中就會攜帶以下信息

          // 來源
          Origin: http://www.hahaha.com
          // 該CORS請求的請求方法
          Access-Control-Request-Method: POST
          // 額外發(fā)出的頭信息字段
          Access-Control-Request-Headers: X-Token, X-Test

          withCredentials屬性

          CORS 請求默認不發(fā)送 Cookie 和 HTTP 認證信息

          如果要把 Cookie 發(fā)到服務(wù)端,首先要服務(wù)端同意,指定Access-Control-Allow-Credentials 字段

          Access-Control-Allow-Credentials: true

          其次,客戶端必須在發(fā)起的請求中打開 withCredentials 屬性

          xhr = new XMLHttpRequest()
          xhr.withCredentials = true

          不然的話,服務(wù)端和客戶端有一個沒設(shè)置,就不會發(fā)送或處理Cookie

          雖說瀏覽器默認不發(fā)送 Cookie 和 HTTP 認證信息,但是有的瀏覽器,還是會一起發(fā)送Cookie,這時你也可以顯式關(guān)閉 withCredentials

          xhr.withCredentials = false

          注意,如要發(fā)送 CookieAccess-Control-Allow-Origin 字段就不能設(shè)為星號,必須指定明確的、與請求網(wǎng)頁一致的域名,同時,Cookie 依然遵循同源政策,只有用服務(wù)器域名設(shè)置的 Cookie 才會上傳,其他域名的 Cookie 并不會上傳,且(跨源)原網(wǎng)頁代碼中的 document.cookie 也無法讀取服務(wù)器域名下的 Cookie ,下面還會提到

          服務(wù)端CORS跨域配置

          上面的東西只是為了讓我們理解CORS,但是要解決它還是需要服務(wù)端配置的,不同語言的配置項語法上可能有差異,但是內(nèi)容肯定都是一樣的

          「配置允許跨域的來源」

          Access-Control-Allow-Origin: *

          CORS 跨域請求中,最關(guān)鍵的就是 Access-Control-Allow-Origin 字段,是必需項,它表示服務(wù)端允許跨域訪問的地址來源,你可以寫入需要跨域的域名,也可以設(shè)為星號,表示同意任意跨源請求

          注意,將此字段設(shè)置為 * 是很不安全的,建議指定來源,并且設(shè)置為 * 號后,游覽器將不會發(fā)送 Cookie,即使你的 XHR 設(shè)置了 withCredentials,也不會發(fā)送 Cookie

          「配置允許跨域請求的方法」

          Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT...

          該字段也是必需項,它的值是逗號分隔的一個字符串,表明服務(wù)器支持的所有跨域請求的方法

          「配置允許的請求頭字段」

          Access-Control-Allow-Headers: x-requested-with,content-type...

          如果你的請求中有自定義的請求頭字段,那么此項也是必須的,它也是一個逗號分隔的字符串,表明服務(wù)器支持的所有頭信息字段,不限于瀏覽器在預(yù)檢中請求的字段

          「配置是否允許發(fā)送Cookie」

          Access-Control-Allow-Credentials: true

          該字段可選,它的值是一個布爾值,表示是否允許發(fā)送Cookie,默認情況下,Cookie不包括在CORS請求之中

          設(shè)為true,即表示服務(wù)器明確許可,Cookie可以包含在請求中,一起發(fā)給服務(wù)器

          該字段只能設(shè)為true,如果服務(wù)器不要瀏覽器發(fā)送Cookie,刪除該字段即可

          「配置本次預(yù)檢請求的有效期」

          Access-Control-Max-Age: 1728000

          該字段可選,用來指定本次預(yù)檢請求的有效期,單位為秒,上面結(jié)果中,有效期是20天(1728000秒),即允許緩存該條回應(yīng)20天,在此期間如果你再次發(fā)出了這個接口請求,就不用發(fā)預(yù)檢請求了,節(jié)省服務(wù)端資源

          常見的跨域預(yù)檢請求拋錯

          對于我們開發(fā)時,在跨域中最容易碰釘子的地方就是預(yù)檢請求,所以列舉幾個預(yù)檢請求錯誤的原因,知道哪錯了可以直接找后端同學理論,關(guān)于預(yù)檢請求,最終目的只有一個,客戶端發(fā)送預(yù)檢,服務(wù)端允許并返回200即可

          「OPTIONS 404」

          No 'Access-Control-Allow-Origin' header is present on the requested resource
          且 The response had HTTP status code 404

          服務(wù)端沒有設(shè)置允許 OPTIONS 請求,那么在發(fā)起該預(yù)檢請求時響應(yīng)狀態(tài)碼會是404,因為無法找到對應(yīng)接口地址

          那么你可能需要找到后端,優(yōu)雅的告訴他,請允許下 OPTIONS 請求

          「OPTIONS 405」

          No 'Access-Control-Allow-Origin' header is present on the requested resource
          且 The response had HTTP status code 405

          服務(wù)端已經(jīng)允許了 OPTIONS 請求,但是一些配置文件中(如安全配置)阻止了 OPTIONS 請求

          那么你可能需要找到后端,優(yōu)雅的告訴他,請關(guān)閉對應(yīng)的安全配置

          「OPTIONS 200」

          No 'Access-Control-Allow-Origin' header is present on the requested resource
          且 OPTIONS 請求 status 為 200

          服務(wù)器端允許了 OPTIONS 請求,配置文件中也沒有阻止,但是頭部匹配時出現(xiàn)不匹配現(xiàn)象

          所謂頭部匹配,就比如 Origin 頭部檢查不匹配,或者少了一些頭部的支持(如 X-Requested-With 等),然后服務(wù)端就會將 Response 返回給前端,前端檢測到這個后就觸發(fā) XHR.onerror ,從而導(dǎo)致報錯

          那么你可能需要找到后端,優(yōu)雅的告訴他,請增加對應(yīng)的頭部支持

          「OPTIONS 500」

          這個就更簡單了,服務(wù)端針對 OPTIONS 請求的代碼出了問題,或者沒有響應(yīng)

          那么你可能需要找到后端,將 Network 中的錯誤信息截一圖發(fā)給他,優(yōu)雅的告訴他,檢測到預(yù)檢請求時,請把它搞成200

          No.7 Nginx代理跨域

          iconfont跨域解決

          瀏覽器跨域訪問 js/css/img 等常規(guī)靜態(tài)資源時被同源策略許可的,但 iconfont 字體文件比如 eot|otf|ttf|woff|svg 例外,此時可在 Nginx 的靜態(tài)資源服務(wù)器中加入以下配置來解決

          location / {
          add_header Access-Control-Allow-Origin *;
          }

          反向代理接口跨域

          我們知道同源策略只是 「瀏覽器」 的安全策略,不是 HTTP 協(xié)議的一部分, 服務(wù)器端調(diào)用 HTTP 接口只是使用 HTTP 協(xié)議,不會執(zhí)行 JS 腳本,不需要同源策略,也就不存在跨越問題

          通俗點說就是客戶端瀏覽器發(fā)起一個請求會存在跨域問題,但是服務(wù)端向另一個服務(wù)端發(fā)起請求并無跨域,因為跨域問題歸根結(jié)底源于同源策略,而同源策略只存在于瀏覽器

          那么我們是不是可以通過 Nginx 配置一個代理服務(wù)器,反向代理訪問跨域的接口,并且我們還可以修改 Cookiedomain 信息,方便當前域 Cookie 寫入

          Nginx 其實就是各種配置,簡單易學,就算沒接觸過,也很好理解,我們來看示例

          首先假如我們的頁面 a 在 http://www.hahaha.com 域下,但是我們的接口卻在 http://www.hahaha1.com:9999 域下

          接著我們在頁面 a 發(fā)起一個 AJAX 請求時,就會跨域,那么我們就可以通過 Nginx 配置一個代理服務(wù)器,域名和頁面 a 相同,都是 http://www.hahaha.com ,用它來充當一個跳板的角色,反向代理訪問  http://www.hahaha1.com  接口

          # Nginx代理服務(wù)器
          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 也能看明白,大部分都是我們上文提到過的,是不是很簡單呢

          No.8 Node代理跨域

          Node 實現(xiàn)跨域代理,與 Nginx 道理相同,都是啟一個代理服務(wù)器,就像我們常用的 Vue-CLI 配置跨域,其實也是 Node 啟了一個代理服務(wù),接下來我們來看看是如何做的

          Vue-CLI中代理的多種配置

          Vue-CLI 是基于 webpack 的,通過 webpack-dev-server 在本地啟動腳手架,也就是在本地啟動了一個 Node 服務(wù),來實時監(jiān)聽和打包編譯靜態(tài)資源,由于都是封裝好的,只需要配置即可,我們在 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 ,最終的代理結(jié)果是 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 上,且使用了無效證書的后端服務(wù)器的

          如果你想要接受,需要設(shè)置 secure: false ,如下

          module.exports = {
            //...
            devServer: {
              proxy: {
                '/api': {
                  target'https://www.hahaha.com',
                  securefalse
                }
              }
            }
          }

          「使用五」

          配置一個字段 changeOrigin ,當它為 true 時,本地就會虛擬一個服務(wù)器接收你的請求并且代你發(fā)送該請求,所以如果你要代理跨域,這個字段是必選項

          module.exports = {
            // ...
            devServer: {
              proxy: {
                "/api": {
                  target'http://www.hahaha.com',
                  changeOrigintrue,
                }
              }
            }
          }

          「使用六」

          如果你想配置多個不同的代理,也簡單,如下所示,可以在任意代理中設(shè)置對應(yīng)的代理規(guī)則

          module.exports = {
            // ...
            devServer: {
              proxy: {
                "/api1": {
                  target'http://www.hahaha1.com',
                  changeOrigintrue
                },
                "/api2": {
                  target'http://www.hahaha2.com',
                  pathRewrite: {'^/api2' : ''}
                },
                "/api3": {
                  target'http://www.hahaha3.com',
                  changeOrigintrue,
                  pathRewrite: {'^/api3' : ''}
                }
                // ...
              }
            }
          }

          注意,在本地配置代理跨域,只是解決開發(fā)時的跨域問題,當你的項目上線時,前端靜態(tài)文件和后端在一個域下沒有問題,如果并不在一個域下,依然會報跨域錯誤,這個時候還得需要后端配置跨域

          Node實現(xiàn)代理服務(wù)器

          這里我們使用 express + http-proxy-middleware 來搭建一個代理服務(wù)器,使用 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',
              changeOrigintrue,

              // 修改響應(yīng)頭信息,實現(xiàn)跨域并允許帶cookie
              onProxyRes: function(proxyRes, req, res{
                  res.header('Access-Control-Allow-Origin''http://www.hahaha.com')
                  res.header('Access-Control-Allow-Credentials''true')
              },

              // 修改響應(yīng)信息中的cookie域名,為false時,表示不修改
              cookieDomainRewrite: 'www.hahaha.com'
          }))

          app.listen(3000)

          No.9 WebSocket跨域

          WebSocket簡介

          WebSocket 是一種在單個 TCP 連接上進行全雙工通信的協(xié)議,2008年誕生,2011年被 IETF 定為標準 RFC 6455,并由 RFC7936 補充規(guī)范,WebSocket API 也被 W3C 定為標準

          WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動向客戶端推送數(shù)據(jù), 在 WebSocket API 中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進行雙向數(shù)據(jù)傳輸,同時,它也是跨域的一種解決方案

          WebSocket特點

          • 建立在 TCP 協(xié)議之上,服務(wù)器端的實現(xiàn)比較容易

          • 與 HTTP 協(xié)議有著良好的兼容性,默認端口也是 80 和 443,并且握手階段采用 HTTP 協(xié)議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務(wù)器

          • 數(shù)據(jù)格式比較輕量,性能開銷小,通信高效

          • 可以發(fā)送文本,也可以發(fā)送二進制數(shù)據(jù)

          • 沒有同源限制,客戶端可以與任意服務(wù)器通信

          • 協(xié)議標識符是 ws(如果加密,則為 wss ),服務(wù)器網(wǎng)址就是 URL

          如下

          ws://www.hahaha.com:80/abc/def

          示例

          每個服務(wù)端語言對 websocket 有相應(yīng)的支持,寫法不同罷了,這里我們使用 Node 做示例

          在客戶端我們可以直接使用 HTML5 的 websocket API ,服務(wù)端也可以使用 nodejs-websocket 實現(xiàn) websocket server ,但是不建議這樣做,因為原生 WebSocket API 使用起有些復(fù)雜,在瀏覽器的兼容性上還不夠理想,所以我們使用 Socket.io,它很好地封裝了 webSocket 接口,提供了更簡單、靈活的接口,也對不支持 webSocket 的瀏覽器提供了向下兼容,使用 Socket.io 庫實現(xiàn) websocket,在發(fā)送數(shù)據(jù)時可以直接發(fā)送可序列化的對象,也可以自定義消息,利用事件字符串來區(qū)分不同消息,整個開發(fā)過程會舒服很多

          想要了解更多看官網(wǎng)即可 Socket.io - 傳送門[5] ,我們來看示例

          客戶端: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>

          服務(wù)端: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 連接是不是超級簡單呢,跟著文檔自己動手試試吧

          最后

          按照時間線貼下了總結(jié)的比較全的幾個帖子,還有其他的瑣碎的文章,比較多就不貼了,這些文章都寫的差不多,可能之間有互相抄襲,有互相借鑒,這些都是避免不了的,此文寫的時候也借鑒了這些文章,只不過我手敲了一遍例子,又用我自己的理解碼下來了,為此花了1周的業(yè)余時間,內(nèi)容上與下面作者寫的有些許雷同,那實屬無奈,可以說是知識點就那么多,大家的總結(jié)稍有不同的地方就剩表達的語法,我也難受,還特意找了工具鑒別了下相似度,以免被誤會,畢竟我也特別反感搬運工,嗯,又是一個深夜,終于收工了,睡覺嘍,對了,碼字不易,點個在看也望轉(zhuǎn)發(fā)
          ?

          參考文章

          瀏覽器同源政策及其規(guī)避方法 - 阮一峰[6] - 2016.04

          跨域資源共享 CORS 詳解 - 阮一峰 [7] - 2016.04

          前端跨域整理 - 思否 damonare[8] - 2016.10

          前端常見跨域解決方案(全)- 思否 安靜de沉淀[9]  - 2017.07

          正確面對跨域,別慌 - 掘金 Neal_yang [10] - 2017.12

          九種跨域方式實現(xiàn)原理(完整版)- 掘金 浪里行舟 [11] - 2019.01

          9種常見的前端跨域解決方案(詳解)- 掘金 小銘子[12] - 2019.07

          ?

          Reference

          [1]

          isboyjc/blog 傳送門: https://github.com/isboyjc/blog/issues/18

          [2]

          XSS、CSFR: https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#Cross-site_request_forgery_CSRF

          [3]

          結(jié)構(gòu)化克隆算法 : https://developer.mozilla.org/en-US/docs/DOM/The_structured_clone_algorithm

          [4]

          json.org : http://www.json.org/json-zh.html

          [5]

          Socket.io - 傳送門: https://socket.io/

          [6]

          瀏覽器同源政策及其規(guī)避方法 - 阮一峰: http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

          [7]

          跨域資源共享 CORS 詳解 - 阮一峰 : http://www.ruanyifeng.com/blog/2016/04/cors.html

          [8]

          前端跨域整理 - 思否 damonare: https://segmentfault.com/a/1190000007326671

          [9]

          前端常見跨域解決方案(全)- 思否 安靜de沉淀: https://segmentfault.com/a/1190000011145364

          [10]

          正確面對跨域,別慌 - 掘金 Neal_yang : https://juejin.im/post/5a2f92c65188253e2470f16d#heading-18

          [11]

          九種跨域方式實現(xiàn)原理(完整版)- 掘金 浪里行舟 : https://juejin.im/post/5c23993de51d457b8c1f4ee1

          [12]

          9種常見的前端跨域解決方案(詳解)- 掘金 小銘子: https://juejin.im/post/5d1ecb96f265da1b6d404433


          瀏覽 47
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91玉足脚交白嫩脚丫 | 国产毛片毛片毛片毛片毛片 | 开心成人激情 | 狠狠干狠狠搞 | 丁香五月天国产 |