天天看點

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

本文将結合周老師的講義對同源與跨域這一前端經典問題進行系統的總結、整理。一起來坐牢,快!

1. 同源限制

1.1 曆史背景 - 含義的轉變

1995年,同源政策由 Netscape 公司引入浏覽器。目前,所有浏覽器都實行這個政策。

最初,它的含義是指,A 網頁設定的 Cookie,B 網頁不能打開,除非這兩個網頁“同源”。所謂“同源”指的是“三個相同”。

  • 協定 - http://、https://、ftp://
  • 域名 - hostname
  • 端口号 - portnumber

舉例來說,

http://www.example.com/dir/page.html

這個網址,協定是

http://

,域名是

www.example.com

,端口是

80

(預設端口可以省略),它的同源情況如下。

  • http://www.example.com/dir2/other.html

    :同源
  • http://example.com/dir/other.html

    :不同源(域名不同)
  • http://v2.www.example.com/dir/other.html

    :不同源(域名不同)
  • http://www.example.com:81/dir/other.html

    :不同源(端口不同)
  • https://www.example.com/dir/page.html

    :不同源(協定不同)

注意,标準規定端口不同的網址不是同源(比如8000端口和8001端口不是同源),但是浏覽器沒有遵守這條規定。實際上,同一個網域的不同端口,是可以互相讀取 Cookie 的。

1.2 目的

同源政策的目的,是為了保證使用者資訊的安全,防止惡意的網站竊取資料。

設想這樣一種情況:A 網站是一家銀行,使用者登入以後,A 網站在使用者的機器上設定了一個 Cookie,包含了一些隐私資訊。使用者離開 A 網站以後,又去通路 B 網站,如果沒有同源限制,B 網站可以讀取 A 網站的 Cookie,那麼隐私就洩漏了。更可怕的是,Cookie 往往用來儲存使用者的登入狀态,如果使用者沒有登出,其他網站就可以冒充使用者,為所欲為。因為浏覽器同時還規定,送出表單不受同源政策的限制。

由此可見,同源政策是必需的,否則 Cookie 可以共享,網際網路就毫無安全可言了。

同源是為了維護網站存儲在用戶端的臨時資訊的安全性,是用戶端存儲的網站資訊的安全屏障。

1.3 限制範圍

随着網際網路的發展,同源政策越來越嚴格。目前,如果非同源,共有三種行為受到限制。

(1) 無法讀取非同源網頁的 Cookie、LocalStorage 和 IndexedDB。

(2) 無法接觸非同源網頁的 DOM。

(3) 無法向非同源位址發送 AJAX 請求(可以發送,但浏覽器會拒絕接受響應)。

另外,通過 JavaScript 腳本可以拿到其他視窗的

window

對象。如果是非同源的網頁,目前允許一個視窗可以接觸其他網頁的

window

對象的九個屬性和四個方法。

九個對象 window.closed、window.frames、window.length、window.location、window.opener、window.parent、window.self、window.top、window.window
四個方法 window.blur()、window.close()、window.focus()、window.postMessage()

上面的九個屬性之中,隻有

window.location

是可讀寫的,其他八個全部都是隻讀。而且,即使是

location

對象,非同源的情況下,也隻允許調用

location.replace()

方法和寫入

location.href

屬性。(隻能進行簡單的頁面跳轉)

雖然這些限制是必要的,但是有時很不友善,合理的用途也受到影響。故我們要使用一些技術去實作跨域。

一些小概念:

Q1:什麼是網頁的DOM?

A1:文檔對象模型 (DOM) 是 HTML 和 XML 文檔的程式設計接口。它提供了對文檔的結構化的表述,并定義了一種方式可以使從程式中對該結構進行通路,進而改變文檔的結構,樣式和内容。DOM 将文檔解析為一個由節點和對象(包含屬性和方法的對象)組成的結構集合。簡言之,它會将 web 頁面和腳本或程式語言連接配接起來。

2.跨域的實作

下面就為大家介紹一些常用的跨域方案。

2.1 降域實作部分跨域通信

Cookie 是伺服器寫入浏覽器的一小段資訊,隻有同源的網頁才能共享。如果兩個網頁一級域名相同,隻是次級域名不同,浏覽器允許通過設定

document.domain

共享 Cookie。

舉例來說,A 網頁的網址是

http://w1.example.com/a.html

,B 網頁的網址是

http://w2.example.com/b.html

,那麼隻要設定相同的

document.domain

,兩個網頁就可以共享 Cookie。因為浏覽器通過

document.domain

屬性來檢查是否同源。

也就是說如果兩個頁面均設定了這一js屬性那麼就可以實作一級域名相同的網頁間的跨域請求。

下面我們呢通過一個具體的例子來實作這一操作。

2.1.1 環境搭建

我們利用apache的基于域名的虛拟主機技術來實作本次實驗的環境搭建,這裡大家可以配置一下。

1.配置網頁目錄

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

這些目錄都需要建立,并在其内部寫入首頁檔案index.html

2.apache配置檔案

将下面的内容複制到

D:\phpstudy_pro\Extensions\Apache2.4.39\conf\vhosts\default-default.conf

檔案内容去

<VirtualHost *:80>
	DocumentRoot "D:/phpstudy_pro/WWW/secbasic"
	ServerName www.security.com
    <Directory  "D:/phpstudy_pro/WWW/secbasic">
	        Options FollowSymLinks ExecCGI
			AllowOverride All
			Order allow,deny
			Allow from all
			Require all granted
			DirectoryIndex index.php index.html
	</Directory>
</VirtualHost>

#domain aaa
<VirtualHost *:80>
	DocumentRoot "D:/phpstudy_pro/WWW/aaa"
	ServerName www.aaa.com
    <Directory  "D:/phpstudy_pro/WWW/aaa">
	        Options FollowSymLinks ExecCGI
			AllowOverride All
			Order allow,deny
			Allow from all
			Require all granted
			DirectoryIndex index.php index.html
	</Directory>
</VirtualHost>

#domain bbb
<VirtualHost *:80>
	DocumentRoot "D:/phpstudy_pro/WWW/bbb"
	ServerName www.bbb.com
    <Directory  "D:/phpstudy_pro/WWW/bbb">
	        Options FollowSymLinks ExecCGI
			AllowOverride All
			Order allow,deny
			Allow from all
			Require all granted
			DirectoryIndex index.php index.html
	</Directory>
</VirtualHost>

#domain master.security.com
<VirtualHost *:80>
	DocumentRoot "D:/phpstudy_pro/WWW/master"
	ServerName master.security.com
    <Directory  "D:/phpstudy_pro/WWW/master">
	        Options FollowSymLinks ExecCGI
			AllowOverride All
			Order allow,deny
			Allow from all
			Require all granted
			DirectoryIndex index.php index.html
	</Directory>
</VirtualHost>

#domain slave.security.com
<VirtualHost *:80>
	DocumentRoot "D:/phpstudy_pro/WWW/slave"
	ServerName slave.security.com
    <Directory  "D:/phpstudy_pro/WWW/slave">
	        Options FollowSymLinks ExecCGI
			AllowOverride All
			Order allow,deny
			Allow from all
			Require all granted
			DirectoryIndex index.php index.html
	</Directory>
</VirtualHost>
           

3.配置本地host檔案将域名解析到本地伺服器上

C:\Windows\System32\drivers\etc\hosts

檔案内容添加

127.0.0.1 slave.security.com
127.0.0.1 master.security.com 
127.0.0.1 www.security.com 
127.0.0.1 www.bbb.com 
127.0.0.1 www.aaa.com
           

4.小皮面闆上重新開機apache進行測試

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作
同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

2.1.2 頁面編寫

master頁面:index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>master</title>
</head>
<body>
    <h1>this is master.security.com</h1>
    <iframe id="001" src="http://slave.security.com"></iframe>
</body>

<script>
    //設定目前頁面的域為本身的二級域名用于進行跨域通信
    document.domain = 'security.com';
    //抓取頁面内部的ifram标簽連接配接
    var ifr = document.getElementById("001");

    //等待加載完畢,将連接配接的視窗傳遞給win,取出内部的傳輸資料
    ifr.onload = function() {
        let win = ifr.contentWindow;
        alert(win.data);
    }

</script>
</html>
           

slave頁面:index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>slave</title>
</head>
<body>
    <h1>this is slave.security.com</h1>
</body>
<script>
     document.domain = 'security.com';
     window.data = 'hello,i am slave';
</script>
</html>
           

2.1.3 測試

先關閉master頁面的降域設定:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

浏覽器不允許進行跨域的資料交換,此時我們打開降域設定在此測試:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

可以看到,master頁面成功的擷取了子頁面的window.data全局變量。可以對其進行讀取。

注意,這種方法隻适用于 Cookie 和 iframe 視窗,LocalStorage 和 IndexedDB 無法通過這種方法規避同源政策,而要使用下文介紹 PostMessage API。

到此,降域進行跨域可以解決一部分問題。但很明顯,實際環境中還會遇到很多域名不一緻的跨域場景。那麼就需要新的方法來解決了。

2.2 片段識别符實作跨域

片段辨別符(fragment identifier)指的是,URL 的

#

号後面的部分,比如

http://example.com/x.html#fragment

#fragment

。如果隻是改變片段辨別符,頁面不會重新重新整理。

也就是說我們可以利用這一特性,将片段識别符作為資訊載體實作跨域。下面我們來嘗試實作。

2.2.1 環境搭建

參照2.1.1的環境進行搭建,其實已經配置完畢了。我們的測試頁面放在aaa檔案夾和bbb檔案夾内部,實作

www.aaa.com

www.bbb.com

兩個域之間的跨域通信。

2.2.2 頁面編寫

aaa目錄下的頁面:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>aaa</title>
</head>
<body>
    <h1>this is www.aaa.com</h1>
</body>
<script>
    //建立ifram标簽包含跨域通信目标網頁
    let ifr = document.createElement('iframe');
    ifr.style.display = 'none';
    ifr.src = "http://www.bbb.com/index.html#data";
    document.body.appendChild(ifr);
    
    //輸出資料
    function checkHash() {
        try {
        //擷取資料去掉'#'
        let data = location.hash ? location.hash.substring(1) : ' ';
        console.log('獲得到的資料是:', data);
        }catch(e) {
            console.log('咦,發生甚麼事了?');
        }
    }
    window.addEventListener('hashchange', function(e) {
        checkHash();
        });
    </script>
</html>
           

01.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>this is 01.html,page for foward the message</h1>
</body>
<script>
	//将資料傳遞給父頁面的父頁面
    parent.parent.location.hash = self.location.hash.substring(1)
</script>
</html>
           

bbb目錄下的頁面:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>bbb</title>
</head>
<body>
    <h1>this is www.bbb.com</h1>
</body>
<script>
    //檢查父頁面的hash辨別符,确認是否為請求資料的
    switch(location.hash) {
        case "#data":
        callback();
        break;
    }

    function callback() {
    const data = "this is a message from bbb.com";
    // parent.location.hash = data;
    try {
        //嘗試直接向父頁面的hash内寫入資料
        parent.location.hash = data;
    }catch(e) {
        // 目前主流浏覽器下的安全機制無法修改 parent.location.hash
        // 是以要利用一個中間的代理檔案 iframe 
        var ifrproxy = document.createElement('iframe');
        ifrproxy.style.display = 'none';
        ifrproxy.src = 'http://www.aaa.com/01.html#' + data;     // 該檔案在請求域名的域下
        document.body.appendChild(ifrproxy);
        }
       }
    </script>
</html>
           

2.2.3 測試效果

我們先在bbb域裡面嘗試直接修改該父頁面:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作
同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

這是因為在現在的安全内環境下,這種子頁面修改父頁面的hash辨別符的行為在跨域情況下是被禁止的。我們的解決方案就是在下面添加進入一個中轉頁面,利用中轉頁面和父頁面同源的特性,讓中轉頁面實作資料的傳遞。

大概原理如下圖:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

說白了,

bbb

找了中間人幫忙給修改他爺爺的hash片段識别符。

修改代碼之後在此進行測試:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

跨域成功,但是這樣操作的缺點顯而易見。

缺點:

  • 資料直接暴露在了url中
  • 資料容量和類型都有限
  • 需要第三方中轉實作麻煩

2.3 window.name

window.name(一般在js代碼裡出現)的值不是一個普通的全局變量,而是目前視窗的名字,要注意的是每個iframe都有包裹它的window,而這個window是top window的子視窗,而它自然也有window.name的屬性,window.name屬性的神奇之處在于name值在不同的頁面(甚至不同域名)加載後依舊存在(如果沒有修改則值不會變化),并且可以支援非常長的name值(2MB)

比如你在某個頁面的控制台輸入:

window.name = "hello world"
window.location = "http://www.baidu.com"
           

可以看到,跳轉後也被儲存了下來

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

示例:window.name實作跨域

aaa域檔案:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>aaa</title>
</head>

<body>
    <h1>this is www.aaa.com</h1>
</body>
<script>
    let data = '';
    const ifr = document.createElement('iframe');
    ifr.src = "http://www.bbb.com";
    ifr.style.display = 'none';
    document.body.appendChild(ifr);

    //中轉擷取window.name
    ifr.onload = function() {
        //更換請求位址
        ifr.src = "http://www.aaa.com/01.html";
        //嘗試取出window.name内的資料
        ifr.onload = function() {
            data = ifr.contentWindow.name;
            console.log('收到資料:', data);
        }
    }

    //此處為測試不進行中轉
    // ifr.onload = function () {
    //     data = ifr.contentWindow.name;
    //     console.log('收到資料:', data);
    // }
</script>

</html>
           

01.html - 這個檔案也可以為空,旨在提供一個可供bbb切換src的頁面,用于制造同源通路。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>this is 01.html,page for foward the message</h1>
</body>
<script>
</script>
</html>
           

bbb域檔案:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>bbb</title>
</head>
<body>
    <h1>this is www.bbb.com</h1>
</body>
<script>
    window.name = "傳輸的資料";
</script>
</html>
           

先嘗試不設定中轉頁面是否可以擷取子頁面的data:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

不允許,因為很顯然aaa包含進了bbb,進行了跨域,并且window.name屬性并未在限制範圍之外。故一定會被同源政策禁止掉,我們可以采用二次修改src的方式,将bbb的window.name數值轉移到本地同源的中轉網頁上,再傳輸進來。使用修改好的代碼再次測試:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

跨域成功。

2.4 window.postMessage()

上面的這種方法屬于破解,HTML5 為了解決這個問題,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。

這個 API 為

window

對象新增了一個

window.postMessage

方法,允許跨視窗通信,不論這兩個視窗是否同源。舉例來說,父視窗

aaa.com

向子視窗

bbb.com

發消息,調用

postMessage

方法就可以了。

postMessage

方法的第一個參數是具體的資訊内容,第二個參數是接收消息的視窗的源(origin),即“協定 + 域名 + 端口”。也可以設為

*

,表示不限制域名,向所有視窗發送。

示例1:新開頁面之間的跨域通信

aaa域首頁:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>aaa</title>
</head>

<body>
    <h1>this is www.aaa.com</h1>
</body>
<script>
    // 父視窗打開一個子視窗,取名title
    var popup = window.open('http://www.bbb.com', 'title');
    // 父視窗向子視窗發消息
    popup.postMessage('Hello, i am aaa page,who are you ?', 'http://www.bbb.com');

   // 監聽 message 消息
   window.addEventListener('message', function (e) {console.log(e.data);}, false);

</script>

</html>
           

bbb域首頁

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>bbb</title>
</head>
<body>
    <h1>this is www.bbb.com</h1>
</body>
<script>
        //子視窗向父視窗發送消息
        window.opener.postMessage('hello nice to meet you , my name is bbb', 'http://www.aaa.com');
        // 監聽 message 消息
        window.addEventListener('message', function (e) {console.log(e.data);}, false);
</script>
</html>
           

測試效果:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作
同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

可以看到其在新的視窗内進行了跨域通信。但是正常使用的時候彈出一個新頁面并不常見。更多的還是使用iframe對頁面進行嵌套處理,在使用postmessage實作平滑的跨域通信。

一些概念:

message

事件的參數是事件對象

event

,提供以下三個屬性。
  • event.source

    :發送消息的視窗
  • event.origin

    : 發送消息的網址(發送者位址)
  • event.data

    : 消息内容

示例2:結合iframe的跨域

aaa域頁面:index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>aaa</title>
</head>

<body>
    <h1>this is aaa page</h1>
    <iframe src="http://www.bbb.com" ></iframe>
</body>
<script>

    window.onload = function () {
        let targetOrigin = 'http://www.bbb.com';
        //想要操作目前iframe的時候,就像該ifranme中postMessage()一個東西。
        window.frames[0].postMessage('來自aaa大哥的輪船~~~', targetOrigin );
        //*表示任何域都可以監聽。
    }
    //當我監聽到message事件的時候,我就知道有人向我發送資料了,我獲得了資料就可以做對應的事情。内部對消息做處理
    window.addEventListener('message', function (e) {
        console.log('aaa 接收到的消息:', e.data);
    });
</script>

</html>
           

bbb域頁面:index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>bbb</title>
</head>

<body>
    <h1>this is www.bbb.com</h1>
</body>
<script>
    //建立事件監聽器,監聽message,第二個參數内寫上處理函數
    window.addEventListener('message', function (e) {
        //判斷消息來源為自己的父頁面,防止惡意資訊傳輸
        if (e.origin !== 'http://www.aaa.com') {
            return;
        }
        console.log('message.source: ', e.source);
        console.log('message.origin: ', e.origin);
        console.log('bbb 接收到的消息:', e.data);
        parent.postMessage('來自bbb大哥的火箭^|^', e.origin);
    })
</script>

</html>
           

效果測試:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

示例3:結合LocalStorage實作跨域

通過

window.postMessage

,讀寫其他視窗的 LocalStorage 也成為了可能。

大概的思路就是讓子頁面通過postmessage給父頁面提供操作子頁面LocalStorage小小資料庫的接口。進而在子頁面LocalStorage内實作和子頁面的共享資料以進行跨域通信。

aaa域首頁:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>aaa</title>
</head>

<body>
    <h1>this is aaa page</h1>
    <iframe src="http://www.bbb.com"></iframe>
</body>
<script>
    window.onload = function () {
        //抓取連接配接
        var win = document.getElementsByTagName('iframe')[0].contentWindow;
        //建立對象
        var obj = {
            id: 001,
            name: 'jack',
            age: '25',
            work: 'engineer'
        };

        // 存入對象
        win.postMessage(
            JSON.stringify({ key: obj.id, method: 'set', data: obj }),
            'http://www.bbb.com'
        );
        // 讀取對象
        win.postMessage(
            JSON.stringify({ key: obj.id, method: "get" }),
            'http://www.bbb.com'
        );

        // //删除對象
        // win.postMessage(
        //     JSON.stringify({ key: obj.id, method: "remove" ,data: obj}),
        //     'http://www.bbb.com'
        // );

        //監聽消息視窗,處理回顯的資料
        window.onmessage = function (e) {
            if (e.origin != 'http://www.bbb.com') return;
            console.log('傳回的JSON資料:',e.data);
            console.log('還原回對象模式:',JSON.parse(e.data));
        };
    }
</script>

</html>
           

bbb域首頁:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>bbb</title>
</head>

<body>
    <h1>this is www.bbb.com</h1>
</body>
<script>
    //開啟message事件監聽
    window.addEventListener('message',receivemessage);
    function receivemessage(e){
        if (e.origin !== 'http://www.aaa.com') return;
        //接受的資料進行反JSON化,用于後續的localstorge處理
        var payload = JSON.parse(e.data);
        switch (payload.method) {
            case 'set':
                localStorage.setItem(payload.key,JSON.stringify(payload.data));
                break;
            case 'get':
                var parent = window.parent;
                var data = localStorage.getItem(payload.key);
                parent.postMessage(data,'http://www.aaa.com');
                break;
            case 'remove':
                console.log('已删除',e.data);
                localStorage.removeItem(payload.key);
                break;
        }
    }
</script>

</html>
           

測試增加查詢操作:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

測試删除操作:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

可以看到,删除成功。

2.5 JSONP跨域

知道AJAX通信機制的同學肯定了解過,由于AXJX對于同源限制的十分之死。在十年前,大家為了解決跨域問題提出了JSONP這一

hakcer

的寫法,雖然已經被淘汰,但是還是可以作為經典回味一下。

JSONP 是伺服器與用戶端跨源通信的常用方法。最大特點就是簡單易用,沒有相容性問題,老式浏覽器全部支援,服務端改造非常小。

它的做法如下。

第一步,網頁添加一個

<script>

元素,向伺服器請求一個腳本,這不受同源政策限制,可以跨域請求。

注意,請求的腳本網址有一個

callback

參數(

?callback=bar

),用來告訴伺服器,用戶端的回調函數名稱(

bar

)。

第二步,伺服器收到請求後,拼接一個字元串,将 JSON 資料放在函數名裡面,作為字元串傳回(

bar({...})

)。

第三步,用戶端會将伺服器傳回的字元串,作為代碼解析,因為浏覽器認為,這是

<script>

标簽請求的腳本内容。這時,用戶端隻要定義了

bar()

函數,就能在該函數體内,拿到伺服器傳回的 JSON 資料。

為了更好的示範這個例子借鑒了另一位部落客的示例:使用JSONP解決跨域

我們需要在aaa域内建立頁面送出請求:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSONP簡單實作</title>
</head>

<body>
    <button id="btn">點選發送請求</button>
</body>
<script>

    function getJsonpData(data) {
        console.log("擷取資料成功")
        console.log(data) //{name:'tom'}
    }

    var btn = document.getElementById("btn");

    btn.onclick = function () {
        
        //建立script标簽
        var script = document.createElement("script");
        script.src = 'http://localhost:3000/user?callback=getJsonpData';
        document.body.appendChild(script);
        script.onload = function () {
            document.body.removeChild(script)
        }
    }
</script>

</html>
           

之後在本地啟動nodejs伺服器:

const express = require('express')
const app = express()
const port = 3000

//路由配置
app.get("/user",(req,res)=>{
   //1.擷取用戶端發送過來的回調函數的名字
   let fnName = req.query.callback;
   //2.得到要通過JSONP形式發送給用戶端的資料
   const data = {name:'tom'}
   //3.根據前兩步得到的資料,拼接出個函數調用的字元串
   let result = `${fnName}({name:"tom"})`
   //4.把上步拼接得到的字元串,響應給用戶端的<script> 标簽進行解析執行
   res.send(result);
})

app.listen(port, () => {
   console.log(`Example app listening on port ${port}`)
})
           

測試:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

整個過程作為發出跨域請求的頁面,建立了一個script标簽将目标伺服器上的頁面腳本包含進來。此時在發出的URL上有自己的回調函數參數。那麼伺服器收到此資訊後對其做出處理,将本地的資料和回調函數拼接到一起予以傳回。于是請求方就接收到了資料。完成跨域。

其缺點也很明顯了:

1.傳輸資料少

2.僅支援get傳輸

2.6 websocket完成跨域

2.6.1 websocket簡介

WebSocket 是一種通信協定,使用

ws://

(非加密)和

wss://

(加密)作為協定字首。該協定不實行同源政策,隻要伺服器支援,就可以通過它進行跨源通信。

說白了在websocket通信裡隻要支援這種通信方式無所謂跨不跨域。

下面是一個websocket通信的請求頭

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
           

上面代碼中,有一個字段是

Origin

,表示該請求的請求源(origin),即發自哪個域名。

正是因為有了

Origin

這個字段,是以 WebSocket 才沒有實行同源政策。因為伺服器可以根據這個字段,判斷是否許可本次通信。如果該域名在白名單内,伺服器就會做出如下回應。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
           

2.6.2 簡單示例

用戶端首頁:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>01.html-aaa</title>
</head>

<body>
    <h1>test for websocket</h1>
</body>
<script>
    let socket = new WebSocket('ws://127.0.0.1:3000');
    socket.onopen = function () {
        //向伺服器發送肉麻消息
        socket.send('I LOVE YOU');
    } 
    socket.onmessage = function (e) {
        //接收傳回的資料
        console.log(e.data);
    }
</script>
</html>
           

服務端NODEJS檔案:在對應目錄下的cmd使用

node 檔案名.js

即可(需要安裝nodejs環境)

let express = require('express');
let app = express();
let websocket = require('ws');
let wss = new websocket.Server({ port: 3000 });

wss.on('connection',
    function (ws) {
        ws.on('message',
            function (data) {
                console.log(data);
                ws.send('I am here,and listen to you.')
            }
        );
    }
)

           

測試結果:

同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

轉換檢視服務端資訊:

D:\phpstudy_pro\WWW\bbb>node socket.js
<Buffer 49 20 4c 4f 56 45 20 59 4f 55>
           
同源跨域的概念與實作跨域的幾種方案1. 同源限制2.跨域的實作

2.7 CORS

CORS 是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是 W3C 标準,屬于跨源 AJAX 請求的根本解決方法。相比 JSONP 隻能發

GET

請求,CORS 允許任何類型的請求。

限于篇幅,這一塊的内容請移步後續的文章。

下面就來總結一下,首先為了用戶端的安全性,浏覽器支援了同源政策。在不同的網站之間嚴禁互相通路本地存儲檔案(cookie、localstorge、indexeddb)并且在很多的屬性方法上也做出了很大的限制。我們在開發中有時會有跨域的需求,故在漫長的歲月中演變出了很多的解決方案。其中有降域解決同二級域名網站間的跨域方案。也有利用片段辨別符、windows.name等中轉的漢克方案。這其中最友善的就是postmessage方法的使用。不僅可以結合iframe使用更是可以使用localstorge進行更加系統的跨域共享。

但是以上的方法都适用于靜态html頁面,涉及到動态頁面時我們就有三個可選的解決方案,首先是古老的JSONP方案由于傳輸量小、方法支援單一不推薦、那麼其次就是websocket通信,友善好用。最後還有最推薦的官方正統CROS跨域機制可謂是官方出品實屬精品了。

繼續閱讀