同源策略那些事兒

原文首發(fā)于 同源策略那些事兒锁荔。

Tips
在本文中锌畸,指代的都是 origin泌辫,換著講純粹是出于通用的習(xí)慣随夸。另外,出于學(xué)習(xí)的目的且為了避免瀏覽器的差異導(dǎo)致的麻煩震放,請(qǐng)?jiān)?Chrome 下運(yùn)行以下所有客戶端的代碼宾毒。

重現(xiàn)

我們先來(lái)寫個(gè)用 ajax 提交表單的小小小的 demo,這畢竟太常見(jiàn)了殿遂。

/Test/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>index</title>
  </head>
  <body>
    <script>
    const xhr = new XMLHttpRequest();

    xhr.open('post', 'http://127.0.0.1/Test/index.php', true);

    xhr.onreadystatechange = () => {
      if(xhr.readyState === 4 && xhr.status === 200) {
        document.write(xhr.responseText);
      }
    };

    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.send('username=yang');
    </script>
  </body>
</html>

/Test/index.php

<?php
echo $_POST['username'];
?>

Tips
請(qǐng)將上述代碼放在本地服務(wù)器中運(yùn)行诈铛。

不出意外,你會(huì)得到以下大禮包:

XMLHttpRequest cannot load http://127.0.0.1/Test/index.php. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access.

此刻墨礁,有的人微微一笑幢竹,有的人一臉懵逼。

怎么回事恩静?

之所以會(huì)出現(xiàn)這個(gè)問(wèn)題焕毫,是因?yàn)闉g覽器有同源策略 (Same-origin policy) 的限制,一個(gè)域 (origin) 的腳本驶乾,在未經(jīng)允許的情況下邑飒,不得通過(guò) DOM 讀取另一個(gè)域的文檔 (document) 的內(nèi)容或?qū)傩浴?/p>

同源策略在 Web 應(yīng)用安全中扮演著重要的角色,它能保護(hù)一個(gè)網(wǎng)站的敏感信息轻掩,防止惡意腳本的竊取幸乒。同源策略中的同源,指的是協(xié)議唇牧、host罕扎、端口相同。同源下的文檔內(nèi)容及屬性可以共享崖蜜,不同源下的文檔內(nèi)容及屬性在未經(jīng)允許時(shí)不可以直接獲取洲劣。

Tips
同源中的host可以為主機(jī)名 (hostname) 或 IP 地址战惊。

如果有一個(gè)地址為:http://example.com茎刚,則:

http://example.com/test/a.html # 同源
https://example.com # 不同源互例,協(xié)議不同
http://www.example.com # 不同源武福,host 不同
http://example.com:8080 # 不同源伍纫,端口不同

規(guī)避同源策略的方法

然而我們有些時(shí)候是需要在不同源的地址間進(jìn)行通信的腻扇,有以下的方法可以用來(lái)規(guī)避同源策略债热。

document.domain

  • 適用情況:不同的窗口 (window) 或 內(nèi)斂框架元素 <iframe> 之間互相訪問(wèn)文檔內(nèi)容,包括 cookie
  • 實(shí)現(xiàn)前提:
    • 協(xié)議幼苛、端口相同
    • 當(dāng)前域名 (domain name) 的父域 (superdomain) 相同
    • 其中一個(gè)窗口或 iframe 能得到另一個(gè)窗口或 iframe 的引用

Tips
如果兩個(gè)地址為one.example.comtwo.example.com窒篱,則它們的父域?yàn)?code>example.com。注意上面所述父域不能為頂級(jí)域名 (top-level domain)舶沿,或者說(shuō)不能為一級(jí)域名 (first-level domain)墙杯。關(guān)于域名分級(jí),詳見(jiàn)domain name space括荡。

無(wú)碼言*高镐。我們來(lái)試試。

/Test/main.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Main window</title>
  </head>
  <body>
    <p>This is the main window</p>
    <iframe src="http://w3.w1.localhost/Test/iframe.html" width="300px" height="250px" id="child-iframe" name="my"></iframe>
    <script>
      document.domain = 'w1.localhost';
    </script>
  </body>
</html>

/Test/iframe.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Iframe window</title>
  </head>
  <body>
    <p>This is the iframe window</p>
    <button id="btn">Close this iframe</button>
    <script>
    document.domain = 'w1.localhost';
    const btn = document.querySelector('#btn');
    const parent = window.parent.document;
    const frame = parent.querySelector('#child-iframe');

    btn.onclick = () => {
      parent.body.removeChild(frame);
    };
    </script>
  </body>
</html>

Tips
跨域通信中畸冲,有些窗口屬性是允許跨域訪問(wèn)的嫉髓,詳見(jiàn)這里

Tips
注意localhost是預(yù)留的頂級(jí)域名邑闲,不可以把它設(shè)置為document.domain算行。

訪問(wèn)http://w2.w1.localhost/Test/main.html,點(diǎn)擊按鈕苫耸,iframe 窗口消失了州邢,再注釋掉任何一個(gè) html 文件的 document.domain = 'w1.localhost'看看,emmmm..

另外褪子,我們可以通過(guò)在服務(wù)器中如下設(shè)置Set-Cookie頭來(lái)實(shí)現(xiàn)在多個(gè)擁有相同父域的子域名間共享 cookie量淌。

<?php
# 假設(shè)發(fā)起請(qǐng)求的域與此域是同域

# 指定了域名的話就相當(dāng)于包含了所有子域名,所有子域名和此父域名都可以共享 cookie
setcookie('username', 'Sam Yang', time() + 24 * 3600, '/', "w1.localhost");
# setcookie('user', 'Sam Yang', time() + 24 * 3600, '/'); // 不指定則默認(rèn)當(dāng)前域名褐筛,cookie 不可被子域名共享
?>

然后你在客戶端的所有子域名下的頁(yè)面都可以通過(guò)document.cookie獲得共享的 cookie类少。

跨文檔消息傳遞 (cross-document messaging)

  • 適用情況:不同的窗口 (window) 或 內(nèi)斂框架元素 <iframe> 之間跨域傳遞數(shù)據(jù)
  • 實(shí)現(xiàn)前提:瀏覽器支持 HTML5 的window.postMessage

通過(guò) HTML5 提供的window.postMessage方法,可以讓一個(gè)頁(yè)面的腳本渔扎,傳遞數(shù)據(jù)給另一個(gè)頁(yè)面的腳本硫狞,而無(wú)需理會(huì)腳本所在的頁(yè)面是否跨域。

這個(gè)方法的主要語(yǔ)法是這樣的:

otherWindow.postMessage(message, targetOrigin)

  • otherWindow - 另一個(gè)窗口的引用晃痴,換言之残吩,是接收數(shù)據(jù)的窗口的引用
  • message - 發(fā)送給其他窗口的數(shù)據(jù),數(shù)據(jù)在發(fā)送前會(huì)被自動(dòng)序列化倘核,而且數(shù)據(jù)類型不受限制
  • targetOrigin - otherWindow的源 (origin)泣侮,可以是字符串*(可以發(fā)送給任何源)或一個(gè) URI,注意只有當(dāng)這里指定的targetOrigin的值和想要接收數(shù)據(jù)的窗口的源 (origin) 完全匹配紧唱,window.postMessage觸發(fā)的事件才會(huì)被發(fā)送

接收數(shù)據(jù)的窗口可以監(jiān)聽(tīng)message事件活尊,這個(gè)事件接收到的數(shù)據(jù)參數(shù)包含三個(gè)重要屬性:

  • data - 發(fā)送數(shù)據(jù)的窗口傳來(lái)的數(shù)據(jù)對(duì)象
  • origin - 發(fā)送數(shù)據(jù)的窗口的源 (origin)
  • source - 發(fā)送數(shù)據(jù)的窗口的引用隶校,可以用這個(gè)屬性建立兩個(gè)非同源窗口間的雙向通信

我們來(lái)用一下:

/Test/main.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Main window</title>
  </head>
  <body>
    <p>This is the main window</p>
    <iframe src="http://w3.w1.localhost/Test/iframe.html" width="300px" height="250px" id="child-iframe" name="my"></iframe>
    <script>
      const iframe = document.querySelector('#child-iframe');
      const iframeWin = iframe.contentWindow; // 獲得 iframe 元素的窗口

      iframe.onload = () => { // 等待 iframe 窗口完全加載完再發(fā)送消息
        iframeWin.postMessage('hello, my friend', 'http://w3.w1.localhost');
      };
    </script>
  </body>
</html>

/Test/iframe.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Iframe window</title>
  </head>
  <body>
    <p>This is the iframe window</p>
    <script>
    window.onmessage = (event) => {
      if(event.origin !== 'http://w2.w1.localhost') return;

      document.write(event.data);
    };
    </script>
  </body>
</html>

訪問(wèn)http://w2.w1.localhost/Test/main.html,可以看到子窗口已經(jīng)接收到了父窗口傳來(lái)的數(shù)據(jù)蛹锰,你還可以嘗試傳輸其他更復(fù)雜的數(shù)據(jù)形式深胳。

Tips
這個(gè)方法雖然很棒,但是需要注意以下幾點(diǎn)安全問(wèn)題铜犬,否則你的站點(diǎn)可能被爆得體無(wú)完膚舞终。

  • 不希望從其他站點(diǎn)接收數(shù)據(jù)時(shí),不要設(shè)置任何message事件的監(jiān)聽(tīng)器
  • 希望從其他站點(diǎn)接收數(shù)據(jù)時(shí)癣猾,接收方務(wù)必使用origin屬性 (有必要的話再加上source屬性) 驗(yàn)證發(fā)送者的身份敛劝,避免惡意代碼的攻擊,可保平安
  • 發(fā)送方應(yīng)總是指定精確的接收方源纷宇,即targetOrigin屬性夸盟,而不是*,因?yàn)楹笳呖梢宰屸嵉恼军c(diǎn)惡意改變你發(fā)送的數(shù)據(jù)的相關(guān)屬性進(jìn)而攔截你的數(shù)據(jù)

JSONP (JSON with padding)

  • 適用情況:客戶端與服務(wù)器端的跨域通信
  • 實(shí)現(xiàn)前提:無(wú)

Tips
由于內(nèi)在的風(fēng)險(xiǎn)呐粘,JSONP 正逐漸被 CORS (見(jiàn)下文) 取代满俗,此處僅出于了解的目的講解此技巧,你可以選擇跳過(guò)這個(gè)部分作岖。

直譯這個(gè)東東唆垃,就是“填充的 JSON”,這是跟 Ajax 一樣的老爺爺了痘儡,不同的是辕万,前者要退休了。

這項(xiàng)技術(shù)之所以出現(xiàn)沉删,是因?yàn)?code><script src="..."></script>標(biāo)簽不受同源策略的限制渐尿。利用<script>元素,頁(yè)面可以向服務(wù)器端請(qǐng)求 JSON 數(shù)據(jù)矾瑰,服務(wù)器端收到請(qǐng)求后砖茸,將 JSON 數(shù)據(jù)傳傳入一個(gè)指定名字的回調(diào)函數(shù)里再傳回給客戶端,這種使用模式就是所謂的 JSONP殴穴。

/Test/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>index</title>
  </head>
  <body>
    <script>
    function addScriptTag(src) {
      const script = document.createElement('script');
      script.src = src;
      document.body.appendChild(script);
    }

    window.onload = () => {
      addScriptTag('http://127.0.0.1/Test/index.php?callback=sayHello'); // 這里的 `callback` 換成其他名字也可以
    };
    
    function sayHello(data) { // 瀏覽器會(huì)自動(dòng)解析得到的 JSON 數(shù)據(jù)凉夯,無(wú)需手動(dòng)解析
      document.body.append(`Hello, ${data.username}`);
    }
    </script>
  </body>
</html>

/Test/index.php

<?php
$callback = $_GET['callback'];
$data = array(
  'username' => 'samyang'
);
$result = json_encode($data);

echo "{$callback}({$result})";
?>

訪問(wèn)http://localhost/Test/index.html

當(dāng)然啦采幌,JSONP 是很容易遭到跨站請(qǐng)求偽造攻擊的劲够,所以你懂的。

WebSocket

  • 適用情況:實(shí)時(shí)通信 (無(wú)論是否跨域)
  • 實(shí)現(xiàn)前提:瀏覽器和服務(wù)器支持 HTML5 的 WebSocket

WebSocket 是一種基于 ws(非加密) / wss(加密) 協(xié)議的技術(shù)休傍,使用這種技術(shù)可以建立客戶端和服務(wù)器端雙向且持續(xù)的通信連接征绎,并且這不受同源策略限制。但是磨取,一旦你使用了 WebSocket 的 URI人柿,請(qǐng)求頭中就會(huì)加入`Origin字段柴墩,指明請(qǐng)求連接的腳本所在的源 (origin),服務(wù)器用這個(gè)字段來(lái)檢驗(yàn)跨站請(qǐng)求是否安全凫岖。

這項(xiàng)技術(shù)仍然不穩(wěn)定拐邪,但有一些成熟的相應(yīng)實(shí)現(xiàn),如 Socket.IO隘截,有興趣可以參見(jiàn)我寫的 Socket.IO 的教程

這項(xiàng)技術(shù)主要用于實(shí)時(shí)通信汹胃,此處不做進(jìn)一步詳述婶芭,想進(jìn)一步探索,見(jiàn) WebSocket着饥。

跨域資源共享 (Cross-Origin Resource Sharing, CORS)

好了犀农,請(qǐng)出我們的大 boss。與 JSONP 的只支持 GET 請(qǐng)求相比宰掉,CORS 支持所有類型的 HTTP 請(qǐng)求呵哨。

  • 適用情況:客戶端與服務(wù)器端的跨域通信
  • 實(shí)現(xiàn)前提:客戶端和服務(wù)器端同時(shí)支持 CORS

ajax 受到同源策略的限制,使用 ajax 技術(shù)時(shí)轨奄,在未經(jīng)允許的情況下孟害,如果跨域請(qǐng)求發(fā)出給了服務(wù)器端并返回了數(shù)據(jù) (視瀏覽器情況而定,有的在發(fā)出時(shí)即攔截)挪拟,則客戶端無(wú)法讀取服務(wù)器端返回的數(shù)據(jù)挨务。CORS 允許服務(wù)器端進(jìn)行跨域訪問(wèn)控制,從而使跨域數(shù)據(jù)傳輸?shù)靡园踩M(jìn)行玉组。

我們首先來(lái)了解下為了支持 CORS谎柄,增加了哪些 HTTP 首部字段。

HTTP 請(qǐng)求首部字段

Tips
這些請(qǐng)求字段無(wú)需手動(dòng)設(shè)置惯雳,當(dāng)你使用 XMLHttpRequest 發(fā)起跨域請(qǐng)求時(shí)朝巫,瀏覽器已自動(dòng)幫你設(shè)置好了它們。

Origin
Origin: <origin>

表明發(fā)起請(qǐng)求的源 (origin)石景,這是一個(gè) URI劈猿,不包含任何路徑信息,只是服務(wù)器的名字鸵钝。

Access-Control-Request-Method
Access-Control-Request-Method: <method>

這個(gè)字段用于預(yù)檢請(qǐng)求 (下文會(huì)講)糙臼,其作用是將實(shí)際請(qǐng)求所使用的 HTTP 方法告訴服務(wù)器。

Access-Control-Request-Headers
Access-Control-Request-Headers: <field-name>[, <field-name>]*

這個(gè)字段用于預(yù)檢請(qǐng)求恩商,其作用是將實(shí)際請(qǐng)求所攜帶的自定義首部字段告訴服務(wù)器变逃。

HTTP 響應(yīng)首部字段

Access-Control-Allow-Origin
Access-Control-Allow-Origin: <origin> | *

指定可以訪問(wèn)當(dāng)前資源的外源 (origin),可以為不含路徑信息的一個(gè) URI怠堪,也可以是通配符*揽乱。后者表示當(dāng)前資源可以被任何外源訪問(wèn)名眉,即允許來(lái)自所有域的請(qǐng)求。注意凰棉,當(dāng)請(qǐng)求攜帶有身份憑證時(shí) (下文講)损拢,服務(wù)器端不可以指定該值為通配符

Access-Control-Expose-Headers

用 XMLHttpRequest 對(duì)象的 getResponseHeader() 方法可以獲取一些基本的響應(yīng)頭撒犀,當(dāng)想要獲取一些額外的響應(yīng)頭時(shí)福压,可以用這個(gè)字段指定。

Access-Control-Max-Age
Access-Control-Max-Age: <delta-seconds>

指定預(yù)檢請(qǐng)求的結(jié)果能被緩存多少秒或舞。在這個(gè)緩存時(shí)間內(nèi)荆姆,瀏覽器無(wú)須為同一請(qǐng)求再次發(fā)起預(yù)檢請(qǐng)求。

Access-Control-Allow-Methods
Access-Control-Allow-Methods: <method>[, <method>]*

這個(gè)字段用于預(yù)檢請(qǐng)求響應(yīng)映凳,其指明了實(shí)際請(qǐng)求時(shí)所允許使用的 HTTP 方法胆筒。

Access-Control-Allow-Headers
Access-Control-Allow-Headers: <field-name>[, <field-name>]*

這個(gè)字段用于預(yù)檢請(qǐng)求響應(yīng),其指明了實(shí)際請(qǐng)求時(shí)允許攜帶的自定義首部字段诈豌。

Access-Control-Allow-Credentials

講這個(gè)字段前仆救,我們先講下 XMLHttpRequest 對(duì)象的 withCredentials 屬性來(lái)用,withCredentials 設(shè)置為true時(shí)矫渔,身份憑證 (cookies彤蔽、HTTP 認(rèn)證信息等) 就會(huì)被瀏覽器包含在跨域請(qǐng)求中,設(shè)置為false則排除在跨域請(qǐng)求之外并且瀏覽器忽略響應(yīng)中設(shè)置 cookie 的字段蚌斩,這個(gè)屬性默認(rèn)為false铆惑,也就是說(shuō)一般而言,在跨域請(qǐng)求中送膳,瀏覽器不會(huì)發(fā)送身份憑證信息员魏。注意,在同源請(qǐng)求中叠聋,設(shè)置這個(gè)屬性沒(méi)有任何影響撕阎。

Access-Control-Allow-Credentials 字段指定了當(dāng)客戶端設(shè)置了 withCredentials 為true時(shí)是否允許瀏覽器讀取響應(yīng)體 (response) 的內(nèi)容,為true時(shí)表示允許碌补。當(dāng)此字段作為預(yù)檢請(qǐng)求中的響應(yīng)頭的一部分時(shí)虏束,它指示的是實(shí)際請(qǐng)求是否可以使用身份憑證 (credentials)。

當(dāng)允許攜帶身份憑證時(shí)厦章,請(qǐng)求頭中包含了可能含有隱私信息的 cookie 數(shù)據(jù)镇匀,所以服務(wù)器端不得設(shè)置 Access-Control-Allow-Origin 的值為*,只能設(shè)置為準(zhǔn)確的域 (origin)袜啃。

Tips
雖然 CORS 允許跨域請(qǐng)求汗侵,但是 cookie 仍然受限于瀏覽器的同源策略,這意味著除了使用前文所講的document.domain方法外,只有來(lái)自同一個(gè)源的頁(yè)面可以讀寫這個(gè)源的 cookie晰韵,你無(wú)法通過(guò) JavaScript 讀寫跨域的 cookie发乔。設(shè)置withCredentialstrue只能讓你把跨域請(qǐng)求的那個(gè)服務(wù)器端設(shè)置在客戶端的 cookie 發(fā)送回給服務(wù)器端,不能讓你把客戶端設(shè)置的 cookie 發(fā)送給服務(wù)器端雪猪。詳情見(jiàn)這里栏尚。

Tips
當(dāng)服務(wù)器端設(shè)置Set-Cookie字段時(shí),如果設(shè)置的域名 (domain) 不包含服務(wù)器的地址只恨,那么設(shè)置的這個(gè) cookie 就會(huì)被用戶代理拒絕保存译仗。比如說(shuō)你服務(wù)器端的地址為http://w1.localhost,下面設(shè)置的 cookie 就會(huì)被用戶代理拒絕保存:

setcookie('user', 'Sam Yang', time() + 24 * 3600, '/', "w2.localhost");

詳情見(jiàn) Invalid domains官觅。

簡(jiǎn)單請(qǐng)求 (simple request)

不會(huì)觸發(fā) CORS 預(yù)檢請(qǐng)求 (下文會(huì)提到) 的請(qǐng)求即為簡(jiǎn)單請(qǐng)求古劲,具體來(lái)說(shuō),就是同時(shí)滿足下列條件的請(qǐng)求:

  • 使用下列請(qǐng)求方法之一:
    • GET
    • HEAD
    • POST
  • 除了用戶代理自動(dòng)設(shè)置的頭部字段缰猴,只允許手動(dòng)設(shè)置以下集合中的首部字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (值為下面三種之一)
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • 請(qǐng)求時(shí)沒(méi)有在任何 XMLHttpRequestUpload 對(duì)象上注冊(cè)事件監(jiān)聽(tīng)器
  • 沒(méi)有在請(qǐng)求中使用 ReadableStream 對(duì)象

/Test/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>index</title>
  </head>
  <body>
    <script>
    const xhr = new XMLHttpRequest();
    xhr.open('get', 'http://w1.localhost/Test/index.php', true);
    xhr.send();
    </script>
  </body>
</html>

/Test/index.php

<?php
header('Access-Control-Allow-Origin: *');
?>

訪問(wèn)http://w2.localhost/Test/main.html,下面分別是請(qǐng)求頭和響應(yīng)頭:

# 請(qǐng)求頭
GET /Test/index.php HTTP/1.1
Host: w1.localhost
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Origin: http://w2.localhost # 注意這個(gè)字段
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
Accept: */*
DNT: 1
Referer: http://w2.localhost/Test/main.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2
HT-Ver: 1.1.2
HT-Sid: KtctXse5-mUErYXtS-aUEwI5XF-NOSNsGw+-vH+sk2L4-8852iahF-tMQulEKm-0Hvpoi9J

# 響應(yīng)頭
HTTP/1.1 200 OK
Date: ****** # 已打碼
Server: Apache/2.4.25 (Unix) PHP/5.6.30
X-Powered-By: PHP/5.6.30
Access-Control-Allow-Origin: * # 注意這個(gè)字段
Content-Length: 0
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

根據(jù)上述條件疤剑,這是一個(gè)簡(jiǎn)單請(qǐng)求滑绒,我們使用 OriginAccess-Control-Allow-Origin 就能完成最簡(jiǎn)單的訪問(wèn)控制芽狗。

預(yù)檢請(qǐng)求 (preflight request) 和 實(shí)際請(qǐng)求 (actual request)

為了避免那些可能對(duì)服務(wù)器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請(qǐng)求方法晤揣,瀏覽器必須首先先使用 OPTIONS 方法發(fā)起一個(gè)預(yù)檢請(qǐng)求,從而獲知服務(wù)器端是否允許該跨域請(qǐng)求俏讹,獲得允許之后才發(fā)起實(shí)際請(qǐng)求弯菊。滿足下述任一條件時(shí)纵势,即應(yīng)首先發(fā)送預(yù)檢請(qǐng)求:

  • 使用下列請(qǐng)求方法之一
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 除了用戶代理自動(dòng)設(shè)置的頭部字段,手動(dòng)設(shè)置了以下集合 之外 的首部字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (值為下面三種之一)
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • 請(qǐng)求時(shí)在一個(gè) XMLHttpRequestUpload 對(duì)象上注冊(cè)了一個(gè)至多個(gè)事件監(jiān)聽(tīng)器
  • 在請(qǐng)求中使用 ReadableStream 對(duì)象

/Test/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>index</title>
  </head>
  <body>
    <script>
    const xhr = new XMLHttpRequest();
    const body = '<?xml version="1.0"?><person><name>Arun</name></person>';

    xhr.open('post', 'http://w1.localhost/Test/index.php', true);
    xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
    xhr.setRequestHeader('Content-Type', 'application/xml');

    xhr.send(body);
    </script>
  </body>
</html>

/Test/index.php

<?php
header('Access-Control-Allow-Origin: http://w2.localhost');
header('Access-Control-Allow-Headers: X-PINGOTHER, Content-Type');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Max-Age: 86400');
?>

訪問(wèn)http://w2.localhost/Test/main.html管钳,下面分別是預(yù)檢請(qǐng)求頭和響應(yīng)頭:

# 請(qǐng)求頭
OPTIONS /Test/index.php HTTP/1.1
Host: w1.localhost
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: POST # 關(guān)注它
Origin: http://w2.localhost # 關(guān)注它
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
Access-Control-Request-Headers: content-type,x-pingother # 關(guān)注它
Accept: */*
DNT: 1
Referer: http://w2.localhost/Test/main.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2
HT-Ver: 1.1.2
HT-Sid: KtctXse5-mUErYXtS-aUEwI5XF-NOSNsGw+-vH+sk2L4-8852iahF-tMQulEKm-0Hvpoi9J

# 響應(yīng)頭
HTTP/1.1 200 OK
Date: ****** # 已打碼
Server: Apache/2.4.25 (Unix) PHP/5.6.30
X-Powered-By: PHP/5.6.30
Access-Control-Allow-Origin: http://w2.localhost # 關(guān)注它
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type # 關(guān)注它
Access-Control-Allow-Methods: POST, GET, OPTIONS # 關(guān)注它
Access-Control-Max-Age: 86400 # 關(guān)注它
Content-Length: 0
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

預(yù)檢請(qǐng)求之后的實(shí)際請(qǐng)求頭和響應(yīng)頭:

# 請(qǐng)求頭
POST /Test/index.php HTTP/1.1
Host: w1.localhost
Connection: keep-alive
Content-Length: 55
Pragma: no-cache
Cache-Control: no-cache
X-PINGOTHER: pingpong # 關(guān)注它
Origin: http://w2.localhost
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
Content-Type: application/xml # 關(guān)注它
Accept: */*
DNT: 1
Referer: http://w2.localhost/Test/main.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2
HT-Ver: 1.1.2
HT-Sid: KtctXse5-mUErYXtS-aUEwI5XF-NOSNsGw+-vH+sk2L4-8852iahF-tMQulEKm-0Hvpoi9J

# 響應(yīng)頭
HTTP/1.1 200 OK
Date: ****** # 已打碼
Server: Apache/2.4.25 (Unix) PHP/5.6.30
X-Powered-By: PHP/5.6.30
Access-Control-Allow-Origin: http://w2.localhost
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Max-Age: 86400
Content-Length: 0
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

一些小伎倆

之所以叫小伎倆钦铁,是因?yàn)檫@里所講的方法都只是在小數(shù)據(jù)量的跨域通信中比較方便,大數(shù)據(jù)量則不宜使用才漆。

  • 適用情況:小數(shù)據(jù)量的跨域通信

Tips
如果對(duì) URL 的結(jié)構(gòu)不甚清晰牛曹,可以參見(jiàn) URL

片段標(biāo)識(shí)符 (fragment identifier)

  • 實(shí)現(xiàn)前提:無(wú)醇滥。

片段標(biāo)識(shí)符是網(wǎng)址 URL 中#符后面的部分黎比。我們可以這樣來(lái)使用之。

/Test/main.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Main window</title>
  </head>
  <body>
    <p>This is the main window</p>
    <iframe src="http://w3.w1.localhost/Test/iframe.html" width="300px" height="250px" id="child-iframe" name="my"></iframe>
  </body>
</html>

/Test/iframe.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Iframe window</title>
  </head>
  <body>
    <p>This is the iframe window</p>
    <script>
    window.onhashchange = () => {
      document.write(`hello, ${window.location.hash.substr(1)}`);
    };
    </script>
  </body>
</html>

訪問(wèn)http://w2.w1.localhost/Test/main.html鸳玩,打開(kāi)控制臺(tái)阅虫,輸入:

document.querySelector('#child-iframe').src += '#samyang'

emmmm... 小窗口內(nèi)容變了!

但是呢不跟,這種方法是比較雞肋的颓帝,雖然#有其他的用途 (見(jiàn) 這里),但它一般是用于定位頁(yè)面中某個(gè)部分的,如果頁(yè)面中有一些標(biāo)簽含有相同的片段標(biāo)識(shí)符躲履,便會(huì)產(chǎn)生一些意料之外的行為见间。再者,這種方法傳輸?shù)臄?shù)據(jù)量太少了工猜。

查詢字符串 (search string, aka, query string)

  • 實(shí)現(xiàn)前提:無(wú)米诉。

這是另一種用于小數(shù)據(jù)量跨域通信的方法,也是我所常用的方法篷帅,也是相比于前者更推薦的做法史侣。查詢字符串是 URL 中?及其后面但不包括#及片段標(biāo)識(shí)符的部分。

/Test/main.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Main window</title>
  </head>
  <body>
    <p>This is the main window</p>
    <button>Click me</button>
    <script>
      document.querySelector('button').onclick = () => {
        location.href = `http://w3.w1.localhost/Test/iframe.html?username=samyang`;
      };
    </script>
  </body>
</html>

/Test/iframe.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Iframe window</title>
  </head>
  <body>
    <p>This is the iframe window</p>
    <button id="btn">Close this iframe</button>
    <script>
    window.onload = () => {
      document.write(location.search.substr(1))
    };
    </script>
  </body>
</html>

訪問(wèn)http://w2.w1.localhost/Test/main.html魏身,點(diǎn)擊按鈕惊橱。

window.name

  • 實(shí)現(xiàn)前提:跨域頁(yè)面在同一個(gè)窗口中。

這個(gè)屬性是屬于窗口的箭昵,只要窗口不變税朴,即使頁(yè)面變?yōu)椴煌颍@個(gè)屬性的值也不變家制。

/Test/main.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Main window</title>
  </head>
  <body>
    <p>This is the main window</p>
    <script>
      window.name = 'samyang';
      location.;
    </script>
  </body>
</html>

訪問(wèn)http://w2.w1.localhost/Test/main.html正林,窗口跳到新頁(yè)面后打開(kāi)控制臺(tái)輸入window.name并回車可以看到原頁(yè)面設(shè)置的值。

結(jié)尾

好了颤殴,大概是這么多了(其實(shí)還有很多)觅廓,如有疏漏請(qǐng)指出。

Reference

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末涵但,一起剝皮案震驚了整個(gè)濱河市杈绸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌矮瘟,老刑警劉巖瞳脓,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異澈侠,居然都是意外死亡篡殷,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門埋涧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)板辽,“玉大人,你說(shuō)我怎么就攤上這事棘催【⑾遥” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵醇坝,是天一觀的道長(zhǎng)邑跪。 經(jīng)常有香客問(wèn)我次坡,道長(zhǎng),這世上最難降的妖魔是什么画畅? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任砸琅,我火速辦了婚禮,結(jié)果婚禮上轴踱,老公的妹妹穿的比我還像新娘症脂。我一直安慰自己,他們只是感情好淫僻,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布诱篷。 她就那樣靜靜地躺著,像睡著了一般雳灵。 火紅的嫁衣襯著肌膚如雪棕所。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天悯辙,我揣著相機(jī)與錄音琳省,去河邊找鬼。 笑死躲撰,一個(gè)胖子當(dāng)著我的面吹牛岛啸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茴肥,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼荡灾!你這毒婦竟也來(lái)了瓤狐?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤批幌,失蹤者是張志新(化名)和其女友劉穎础锐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體荧缘,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡皆警,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了截粗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片信姓。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖绸罗,靈堂內(nèi)的尸體忽然破棺而出意推,到底是詐尸還是另有隱情,我是刑警寧澤珊蟀,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布菊值,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏腻窒。R本人自食惡果不足惜昵宇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望儿子。 院中可真熱鬧瓦哎,春花似錦、人聲如沸典徊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)卒落。三九已至羡铲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間儡毕,已是汗流浹背也切。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腰湾,地道東北人雷恃。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像费坊,于是被迫代替她去往敵國(guó)和親倒槐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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