進階13 JSONP 和 跨域

1. 跨域和同源

首先來看摘自MDN上對于跨域,較為標準的解釋:

當一個資源從與該資源本身所在的服務器不同的域或端口請求一個資源時,資源會發起一個跨域 HTTP 請求。
比如,站點 http://domain-a.com 的某 HTML 頁面通過 <img> 的 src 請求 http://domain-b.com/image.jpg。網絡上的許多頁面都會加載來自不同域的CSS樣式表,圖像和腳本等資源。
出于安全考慮,瀏覽器會限制從腳本內發起的跨域HTTP請求。例如,XMLHttpRequest 和 Fetch 遵循同源策略。因此,使用 XMLHttpRequest或 Fetch 的Web應用程序如果不使用跨域技術,只能將HTTP請求發送到其自己的域。

同源策略(same-origin policy):
瀏覽器出于安全方面的考慮,只允許與本域下的接口交互。不同源的客戶端腳本在沒有明確授權的情況下,不能讀寫對方的資源,防止惡意的網站竊取數據、cookie等。

但不一定是瀏覽器限制了發起跨站請求,也可能是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。

比如我現在用的Chrome/61.0.3163.91,雖然有同源策略的存在,但是在調試工具的Network下,Status Code 200 OK, 說明數據是返回回來了, 并且可以在Preview 或者 Response里看到數據。

什么才是同源:
字符串完全匹配才是同源,協議不同 域名不同(子域名和主域名并不是同源)端口不同,都不算是同源。

本地調試時:
一個http-server服務器只能監聽一個端口,監聽多個可以設置不同端口,比如:
http-server -c-1 -p 80
http-server -c-1 -p 81

是跨域還是本域同源,看2個點:

  1. 發送AJAX 請求的當前頁面 URL 是什么
  2. AJAX 請求的 URL 是什么

這兩個 URL 同源,則不是跨域。

此外, <iframe> 標簽,也是受同源策略限制的。

2. 跨域的幾種方式

- JSONP

JSONP是服務器與客戶端跨源通信的常用方法。最大特點就是簡單適用,老式瀏覽器全部支持,服務器改造非常小。

html中 <script> 標簽可以引入其他域下的js,比如引入線上的jquery庫。利用這個特性,可實現跨域訪問接口, 但是需要后端支持。

首先引入標簽,參數中指定回調函數名:

  <script src="http://weather.com.cn?city=hefei&callback=showWeather">

請求到的數據, 由于受到后端支持,所以類似如下結構,后端把原始數據放在要執行的函數的參數里面返回給前端:

    showWeather({
      "city": "hefei",
      weatheer: {xxx}
    })

當數據返回到了客戶端,由于是<script>標簽,所以會自動當作js代碼執行。

但是,我們雖然引入script標簽時,在URL的中指定了callback的函數名,以便后端能用正確的函數名去‘包裝’,可是這個傳入了原始數據的函數在我們的瀏覽器中運行起來,到底要得到什么結果呢?

答案幾乎是顯而易見的,就像非跨域請求一樣,我們需要對數據進行處理,比如解析數據,進行html的拼接,并最終展示到頁面上。

所以我們要聲明并完善這個callback函數:

    function showWeather(json){
      // do something
    }

以上有個問題,就是在引入script標簽的時候,是直接提前寫在HTML文檔里的,而我們拿到數據處理完后(js執行完畢后),這個標簽就沒有用處了,而且實際場景中有多個類似請求的情況時,如何才能保持HTML的干凈利索呢?

可以考慮下面的方式,使用js創建script標簽,引用到資源后,這里由于是立即并且逐條執行的,所以看似下一句立刻刪除了標簽,但實際上中間已經執行過了我們起初定義的函數。

    document.querySelector('.change').addEventListener('click', function(){
      var script = document.createElement('script')
      script.src = 'http://127.0.0.1:8080/getNews?callback=appendHtml';
      document.head.appendChild(script)
      document.head.removeChild(script) //請求數據,執行完畢后,就立即刪除script的引入標簽
    })

而后端之前也提到了,只需要在同源請求中,增加判斷語句,如果有callback參數,則返回使用函數‘包裝’后的數據:

  var cb = req.query.callback
  if(cb){
    res.send(cb + '(' + JSON.stringify(data) + ')')
  }else{
    res.send(data)
  }

點擊查看完整實例代碼

!注意演示中,使用了 server-mock 工具,使用隨同NodeJS一起安裝的包管理工具NPM進行server-mock的安裝,然后把index.html 和router.js 放在一個文件夾,接著終端里進入當前文件夾, 使用 mock start,開啟本地服務器即可。

- CORS(Cross-origin resource sharing) 跨域

CORS 也叫跨域資源共享,它是W3C標準,是跨源AJAX請求的根本解決方法,克服了AJAX只能同源使用的限制。相比JSONP只能發GET請求,CORS允許任何類型的請求,可以說是老式JSONP的現代升級版。

目前,除了 IE瀏覽器IE10以下外,所有瀏覽器都支持該功能。

對于開發者來說,CORS通信與同源的AJAX通信沒有差別,實現CORS通信的關鍵在與服務器支持與否,只要服務器實現了CORS接口,就可以跨域通信。

還是上一個實例,服務器端開啟CORS的方法是,在響應頭信息中添加 Access-Control-Allow-Origin:

 res.header('Access-Control-Allow-Origin', 'http://wangpeng.com:8080')
 //res.header('Access-Control-Allow-Origin', '*')  

第二個參數用來聲明哪些源站有權限訪問哪些資源。
星號 *代表來自任意域名的請求,都不會受到瀏覽器同源策略限制。

- 降域實現跨域

和上面都是請求資源的場景不同。對于兩個不同頁面的腳本,只有當執行它們的頁面位于具有相同的協議,端口號,以及主機(document.domain 也就是原始域名----origin domain 設置為相同的值,也就是同時降域成一致)時,這兩個腳本才能相互通信。

降域主要場景是, a.xxx.com 和 b.xxx.com 之間的訪問,雖然都是xxx.com 子域名,但是也存在瀏覽器同源策略的限制, 也無法直接獲取對方信息。此時,使用降域即可解決。

方法是: a.xxx.com 和 b.xxx.com 的頁面JS中, 同時加入 document.domain = "xxx.com" 彼此都降域到主域名。

于是二者可以在各自的頁面中,使用<iframe>引入另一個域名下同時做了降域的頁面,并可以進行相互操作。

但是,如果不是一個主域名下的兩個二級域名,那么是不可能降域到一樣的, 比如 a.baidu.com 和 b.taobao.com, 降域后分別是 baidu.com 和 taobao.com ,這顯然不是同源。

降域a頁面
降域b頁面

使用server-mock工具 或者http-server 搭建本地服務,任意打開a頁面,兩個input中任意一個輸入value值,另一個input會隨之改變,說明實現了跨域操作。

- postMessage

上例中通過降域,向其他窗口比如 iframe 、執行window.open返回的窗口對象等發送數據,實現兩個不同頁面的腳本跨域通信,是存在局限性的,因為降域也要看域名條件。

HTML5為了解決這個問題,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。
這個API為window對象新增了一個window.postMessage方法,允許跨窗口通信,不論這兩個窗口是否同源。
舉例來說,父窗口http://aaa.com向子窗口http://bbb.com發消息,調用postMessage方法就可以了。
對于不同的域下,可以向其發送數據,如果對方認可接受這個數據,那么就可以使用,如果對方沒有監聽接受這個數據,那么就沒有任何效果。

postMessage方法的第一個參數是具體的信息內容,第二個參數是接收消息的窗口的源(origin),即"協議 + 域名 + 端口"。也可以設為*,表示不限制域名,向所有窗口發送。

還是上一個應用了降域的例子,這一次我們換用postMessage方法。

a頁面:

    //發送
    document.querySelector('.main input').addEventListener('input', function(){
      console.log(this.value)
      //把輸入框的值發給兒子iframe,第二個參數指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示無限制)或者一個URI
      window.frames[0].postMessage(this.value, '*') 
    })
    //監聽iframe的消息
    window.addEventListener('message', function(e){ 
      document.querySelector('.main input').value = e.data
      console.log(e.data)
    })
    //關于postMessage 的使用,MDN文檔有詳細描述,已經更規范更安全的建議,本文只是做跨域的簡單探討,簡化了很多細節。

b頁面:

    //發送
    document.querySelector('#input').addEventListener('input', function(){
      window.parent.postMessage(this.value, '*') //把輸入框的值發給parent
    })
    //監聽parent的消息
    window.addEventListener('message', function(e){
      document.querySelector('#input').value = e.data
      console.log(e.data)
    })

關于跨域問題,MDN文檔有詳細使用描述,以及更規范更安全的建議,
另外,阮老師的這兩篇博文也做了詳盡的闡述。

阮一峰-瀏覽器同源政策及其規避方法
阮一峰-跨域資源共享 CORS 詳解

P.S. 最近學習了AJAX和跨域,剛好試著調用下API,發現很好玩! 可以做很多有趣的事情,唯一的局限是自身水平和想象力的局限。就在剛剛寫這篇拙文的時候,就發現放在github上的音樂頁面,不能請求到資源,說我請求的mixed content(混合內容,確切說是音頻資源) 被block掉了。google一下,才恍然想起來,github pages 是 https協議的。趕緊跑去換了個https協議的API,搞定。

音樂API調用演示


如有任何想法或疑惑,歡迎評論區提出,我們一起探討:D

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 題目1.什么是同源策略? 同源策略(Same origin Policy): 瀏覽器出于安全方面的考慮,只允許與本...
    FLYSASA閱讀 1,752評論 0 6
  • 1.什么是同源策略瀏覽器出于安全方面的考慮,只允許與本域下的接口交互。不同源的客戶端腳本在沒有明確授權的情況下,不...
    24_Magic閱讀 520評論 0 0
  • 1. 跨域和同源 首先來看摘自MDN上對于跨域,較為標準的解釋: 當一個資源從與該資源本身所在的服務器不同的域或端...
    曉風殘月1994閱讀 430評論 0 0
  • 1. 什么是同源策略 瀏覽器限制不同源的兩個網站間腳本和文本的相互訪問,只允許訪問同源下的內容。所謂同源,就是指兩...
    熊蛋子17閱讀 698評論 1 6
  • 久聞西界彩云深, 綠葉紅花醉美人。 澧水遠來惆悵去, 芳心難許隘層層。 丙申五月十七
    東林梁閱讀 235評論 2 31