天天看点

同源跨域的概念与实现跨域的几种方案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跨域机制可谓是官方出品实属精品了。

继续阅读