本文将结合周老师的讲义对同源与跨域这一前端经典问题进行系统的总结、整理。一起来坐牢,快!
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.配置网页目录
这些目录都需要创建,并在其内部写入首页文件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进行测试
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页面的降域设置:
浏览器不允许进行跨域的数据交换,此时我们打开降域设置在此测试:
可以看到,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域里面尝试直接修改该父页面:
这是因为在现在的安全内环境下,这种子页面修改父页面的hash标识符的行为在跨域情况下是被禁止的。我们的解决方案就是在下面添加进入一个中转页面,利用中转页面和父页面同源的特性,让中转页面实现数据的传递。
大概原理如下图:
说白了,bbb
找了中间人帮忙给修改他爷爷的hash片段识别符。
修改代码之后在此进行测试:
跨域成功,但是这样操作的缺点显而易见。
缺点:
- 数据直接暴露在了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"
可以看到,跳转后也被保存了下来
示例: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:
不允许,因为很显然aaa包含进了bbb,进行了跨域,并且window.name属性并未在限制范围之外。故一定会被同源策略禁止掉,我们可以采用二次修改src的方式,将bbb的window.name数值转移到本地同源的中转网页上,再传输进来。使用修改好的代码再次测试:
跨域成功。
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>
测试效果:
可以看到其在新的窗口内进行了跨域通信。但是正常使用的时候弹出一个新页面并不常见。更多的还是使用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>
效果测试:
示例3:结合LocalStorage实现跨域
通过window.postMessage
,读写其他窗口的 LocalStorage 也成为了可能。
大概的思路就是让子页面通过postmessage给父页面提供操作子页面LocalStorage小小数据库的接口。从而在子页面LocalStorage内实现和子页面的共享数据以进行跨域通信。