進(jìn)階13 JSONP 和 跨域

1. 跨域和同源

首先來(lái)看摘自MDN上對(duì)于跨域,較為標(biāo)準(zhǔn)的解釋:

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

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

但不一定是瀏覽器限制了發(fā)起跨站請(qǐng)求,也可能是跨站請(qǐng)求可以正常發(fā)起,但是返回結(jié)果被瀏覽器攔截了。

比如我現(xiàn)在用的Chrome/61.0.3163.91,雖然有同源策略的存在,但是在調(diào)試工具的Network下,Status Code 200 OK, 說(shuō)明數(shù)據(jù)是返回回來(lái)了, 并且可以在Preview 或者 Response里看到數(shù)據(jù)。

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

本地調(diào)試時(shí):
一個(gè)http-server服務(wù)器只能監(jiān)聽(tīng)一個(gè)端口,監(jiān)聽(tīng)多個(gè)可以設(shè)置不同端口,比如:
http-server -c-1 -p 80
http-server -c-1 -p 81

是跨域還是本域同源,看2個(gè)點(diǎn):

  1. 發(fā)送AJAX 請(qǐng)求的當(dāng)前頁(yè)面 URL 是什么
  2. AJAX 請(qǐng)求的 URL 是什么

這兩個(gè) URL 同源,則不是跨域。

此外, <iframe> 標(biāo)簽,也是受同源策略限制的。

2. 跨域的幾種方式

- JSONP

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

html中 <script> 標(biāo)簽可以引入其他域下的js,比如引入線上的jquery庫(kù)。利用這個(gè)特性,可實(shí)現(xiàn)跨域訪問(wèn)接口, 但是需要后端支持。

首先引入標(biāo)簽,參數(shù)中指定回調(diào)函數(shù)名:

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

請(qǐng)求到的數(shù)據(jù), 由于受到后端支持,所以類似如下結(jié)構(gòu),后端把原始數(shù)據(jù)放在要執(zhí)行的函數(shù)的參數(shù)里面返回給前端:

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

當(dāng)數(shù)據(jù)返回到了客戶端,由于是<script>標(biāo)簽,所以會(huì)自動(dòng)當(dāng)作js代碼執(zhí)行。

但是,我們雖然引入script標(biāo)簽時(shí),在URL的中指定了callback的函數(shù)名,以便后端能用正確的函數(shù)名去‘包裝’,可是這個(gè)傳入了原始數(shù)據(jù)的函數(shù)在我們的瀏覽器中運(yùn)行起來(lái),到底要得到什么結(jié)果呢?

答案幾乎是顯而易見(jiàn)的,就像非跨域請(qǐng)求一樣,我們需要對(duì)數(shù)據(jù)進(jìn)行處理,比如解析數(shù)據(jù),進(jìn)行html的拼接,并最終展示到頁(yè)面上。

所以我們要聲明并完善這個(gè)callback函數(shù):

    function showWeather(json){
      // do something
    }

以上有個(gè)問(wèn)題,就是在引入script標(biāo)簽的時(shí)候,是直接提前寫(xiě)在HTML文檔里的,而我們拿到數(shù)據(jù)處理完后(js執(zhí)行完畢后),這個(gè)標(biāo)簽就沒(méi)有用處了,而且實(shí)際場(chǎng)景中有多個(gè)類似請(qǐng)求的情況時(shí),如何才能保持HTML的干凈利索呢?

可以考慮下面的方式,使用js創(chuàng)建script標(biāo)簽,引用到資源后,這里由于是立即并且逐條執(zhí)行的,所以看似下一句立刻刪除了標(biāo)簽,但實(shí)際上中間已經(jīng)執(zhí)行過(guò)了我們起初定義的函數(shù)。

    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) //請(qǐng)求數(shù)據(jù),執(zhí)行完畢后,就立即刪除script的引入標(biāo)簽
    })

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

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

點(diǎn)擊查看完整實(shí)例代碼

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

- CORS(Cross-origin resource sharing) 跨域

CORS 也叫跨域資源共享,它是W3C標(biāo)準(zhǔn),是跨源AJAX請(qǐng)求的根本解決方法,克服了AJAX只能同源使用的限制。相比JSONP只能發(fā)GET請(qǐng)求,CORS允許任何類型的請(qǐng)求,可以說(shuō)是老式JSONP的現(xiàn)代升級(jí)版。

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

對(duì)于開(kāi)發(fā)者來(lái)說(shuō),CORS通信與同源的AJAX通信沒(méi)有差別,實(shí)現(xiàn)CORS通信的關(guān)鍵在與服務(wù)器支持與否,只要服務(wù)器實(shí)現(xiàn)了CORS接口,就可以跨域通信。

還是上一個(gè)實(shí)例,服務(wù)器端開(kāi)啟CORS的方法是,在響應(yīng)頭信息中添加 Access-Control-Allow-Origin:

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

第二個(gè)參數(shù)用來(lái)聲明哪些源站有權(quán)限訪問(wèn)哪些資源。
星號(hào) *代表來(lái)自任意域名的請(qǐng)求,都不會(huì)受到瀏覽器同源策略限制。

- 降域?qū)崿F(xiàn)跨域

和上面都是請(qǐng)求資源的場(chǎng)景不同。對(duì)于兩個(gè)不同頁(yè)面的腳本,只有當(dāng)執(zhí)行它們的頁(yè)面位于具有相同的協(xié)議,端口號(hào),以及主機(jī)(document.domain 也就是原始域名----origin domain 設(shè)置為相同的值,也就是同時(shí)降域成一致)時(shí),這兩個(gè)腳本才能相互通信。

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

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

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

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

降域a頁(yè)面
降域b頁(yè)面

使用server-mock工具 或者h(yuǎn)ttp-server 搭建本地服務(wù),任意打開(kāi)a頁(yè)面,兩個(gè)input中任意一個(gè)輸入value值,另一個(gè)input會(huì)隨之改變,說(shuō)明實(shí)現(xiàn)了跨域操作。

- postMessage

上例中通過(guò)降域,向其他窗口比如 iframe 、執(zhí)行window.open返回的窗口對(duì)象等發(fā)送數(shù)據(jù),實(shí)現(xiàn)兩個(gè)不同頁(yè)面的腳本跨域通信,是存在局限性的,因?yàn)榻涤蛞惨从蛎麠l件。

HTML5為了解決這個(gè)問(wèn)題,引入了一個(gè)全新的API:跨文檔通信 API(Cross-document messaging)。
這個(gè)API為window對(duì)象新增了一個(gè)window.postMessage方法,允許跨窗口通信,不論這兩個(gè)窗口是否同源。
舉例來(lái)說(shuō),父窗口http://aaa.com向子窗口http://bbb.com發(fā)消息,調(diào)用postMessage方法就可以了。
對(duì)于不同的域下,可以向其發(fā)送數(shù)據(jù),如果對(duì)方認(rèn)可接受這個(gè)數(shù)據(jù),那么就可以使用,如果對(duì)方?jīng)]有監(jiān)聽(tīng)接受這個(gè)數(shù)據(jù),那么就沒(méi)有任何效果。

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

還是上一個(gè)應(yīng)用了降域的例子,這一次我們換用postMessage方法。

a頁(yè)面:

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

b頁(yè)面:

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

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

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

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

音樂(lè)API調(diào)用演示


如有任何想法或疑惑,歡迎評(píng)論區(qū)提出,我們一起探討:D

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 177,128評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,291評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,421評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,642評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,821評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,053評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,896評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容

  • 題目1.什么是同源策略? 同源策略(Same origin Policy): 瀏覽器出于安全方面的考慮,只允許與本...
    FLYSASA閱讀 1,744評(píng)論 0 6
  • 1.什么是同源策略瀏覽器出于安全方面的考慮,只允許與本域下的接口交互。不同源的客戶端腳本在沒(méi)有明確授權(quán)的情況下,不...
    24_Magic閱讀 506評(píng)論 0 0
  • 1. 跨域和同源 首先來(lái)看摘自MDN上對(duì)于跨域,較為標(biāo)準(zhǔn)的解釋: 當(dāng)一個(gè)資源從與該資源本身所在的服務(wù)器不同的域或端...
    曉風(fēng)殘?jiān)?994閱讀 426評(píng)論 0 0
  • 1. 什么是同源策略 瀏覽器限制不同源的兩個(gè)網(wǎng)站間腳本和文本的相互訪問(wèn),只允許訪問(wèn)同源下的內(nèi)容。所謂同源,就是指兩...
    熊蛋子17閱讀 695評(píng)論 1 6
  • 久聞西界彩云深, 綠葉紅花醉美人。 澧水遠(yuǎn)來(lái)惆悵去, 芳心難許隘層層。 丙申五月十七
    東林梁閱讀 227評(píng)論 2 31