同源策略和跨域访问学习笔记
同源策略
概述
含义
所谓同源是值三个相同
- 协议相同
- 域名相同
- 端口相同
目的
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,”同源政策”是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
跨域
只要协议、域名、端口有任何一个不同,都被当作是不同的域,跨域就是访问非本域的资源。
跨域实现方式
- document.domain
- window.name
- window.postMessage
- JSONP
- iframe
- CORS
document.domain
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie。
举例来说,A网页是http://w1.example.com/a.html B网页是 http://w2.example.com/b.html 那么只要设置相同的document.domain,两个网页就可以共享Cookie。
1 | document.domain = 'example.com'; |
现在,A网页通过脚本设置一个 Cookie。
1 | document.cookie = "test1=hello"; |
B网页就可以读到这个 Cookie。
1 | var allCookie = document.cookie; |
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策。
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com。
1 | Set-Cookie: key=value; domain=.example.com; path=/ |
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
w1.example.com/a.html
1 | <!DOCTYPE html> |
w2.example.com/b.html
1 | <!DOCTYPE html> |
在PHPstudy下配置虚拟主机 w1.example.com和w2.example.com,他们的html代码分别如上
访问 w2.example.com/b.html 看得到的cookie是否和 w1.example.com/a.html 一样
由此可见document.domain可以实现cookie的跨域。
同样,两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。
这里也可以通过document.domain来进行子域与父域之间的传值。
实验 2 iframe document.domain
父域 example.com/a.html
1 | <!DOCTYPE html> |
子域 w2.example.com/b.html
1 | <!DOCTYPE html> |
这里可以看到我们在子域 w2.example.com/b.html中写了一个w2.example.com/b.html,写了一个UL标签。同时设置了
1 | document.domain = 'example.com'; |
然后在父域 example.com/a.html 定义一个iframe去读取子域的UL,实现异域的DOM读取。
window.name
浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。
在w2.example.com/b.html,该网页将信息写入window.name属性。
1 | window.name = data; |
接着,example.com/a.html跳回一个与它同域的网址。
1 | src = "http://example.com/Proxy.html |
然后在example.com/a.html 读取w2.example.com/b.html的window.name
1 | document.getElementById("iframe").contentWindow.name |
实验 3 iframe window.name
example.com/a.html1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>example.com/a.html</title>
</head>
<body>
<h1>example.com/a.html</h1>
<script>
function test(){
var obj = document.getElementById("iframe");
obj.onload = function(){
var message = obj.contentWindow.name;
alert(message);
}
obj.src = "http://example.com/Proxy.html";
}
</script>
<iframe id="iframe" src="http://w2.example.com/b.html" onload="test()"></iframe>
</body>
</html>
w2.example.com/b.html1
2
3
4
5
6
7
8
9
10
11
12
13
14<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>window.name</title>
</head>
<body>
<h1>w2.example.com/b.html</h1>
<script>
//todo
window.name = "This is message!";
</script>
</body>
</html>
整个跨域的流程就是:
w2.example.com/b.html 设置了window.name = “This is message!”
而example.com/a.html想要获取到window.name的值就需要依靠iframe作为中间代理
首先把iframe的src设置成http://w2.example.com/b.html 这样就相当于要获取iframe的window.name,
而要想获取到iframe中的window.name,就需要把iframe的src设置成当前域的一个页面地址”http://example.com/Proxy.html“
不然根据前面讲的同源策略,window.name.html是不能访问到iframe里的window.name属性的。
window.postMessage
HTML5引入了一个全新的API:跨文档通信 API(Cross-document messaging)。
这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。
向外界窗口发送消息
postMessage方法:
1 | otherWindow.postMessage(message, targetOrigin); |
otherWindow: 指目标窗口,也就是给哪个window发消息。
message: 要发送的消息,类型为 String、Object (IE8、9 不支持)
targetOrigin: 是限定消息接收范围,不限制请使用 ‘*’
接受信息的”message”事件
1 | var onmessage = function (event) { |
回调函数第一个参数接收 event 对象,有三个常用属性:
- data: 消息
- origin: 消息来源地址
- source: 源 DOMWindow 对象
实验 4 iframe window.postMessage
example.com/a.html1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>postmessage</title>
</head>
<body>
<h1>example.com/a.html</h1>
<script>
window.onload = function () {
if (typeof window.postMessage === undefined) {
alert("浏览器不支持postMessage!");
} else {
window.top.postMessage("Uknow", "http://w2.example.com");
}
}
</script>
</body>
</html>
w2.example.com/b.html1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>w2.example.com -- iframe-postmessage</title>
</head>
<body>
<h1>w2.example.com/b.html</h1>
<script>
function test(){
if (typeof window.postMessage === undefined) {
alert("浏览器不支持postMessage!");
} else {
window.addEventListener("message", function(e){
if (e.origin == "http://example.com") { //只接收指定的源发来的消息
alert(e.data);
};
}, false);
}
}
</script>
<iframe id="iframe" src="http://example.com/a.html" onload="test()"></iframe>
</body>
</html>
JSONP
JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写。
JSONP实现跨域请求的原理简单的说,就是动态创建script标签,然后利用script的src 不受同源策略约束来跨域获取数据。
JSONP 由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的 JSON 数据。
实验 5 JSONP
example.com/a.html
1 | <!DOCTYPE html> |
w2.example.com/jsonp.php
1 | <?php |
w2.example.com目录下的jsonp.php文件获取数据时,地址后面跟了一个callback参数(一般的就是用callback这个参数名,你也可以用其他的参数名代替)。
如果你要获取数据的页面是你不能控制的,那你只能根据它所提供的接口格式进行获取。
因为我们的type规定是当成是一个javascript文件来引入的,所以php文件返回的应该是一个可执行的js文件。
访问w2.example.com/jsonp.php 控制台打印出:
通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,同时把我们需要的json数据作为参数传入。所以jsonp是需要服务器端和客户端相互配合的。
知道jsonp跨域的原理后我们就可以用js动态生成script标签来进行跨域操作了,而不用特意的手动的书写那些script标签。比如jQuery封装的方法就能很方便的来进行jsonp操作了。
我们在example.com中引用JQuery.js用它封装好的方法进行jsonp操作
1 | $.getJSON("http://w2.example.com/jsonp.php?callback=?", function(data){ |
原理是一样的,只不过我们不需要手动的插入script标签以及定义回掉函数。jQuery会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。
从请求的url和响应的数据就可以很明显的看出来了:
jQuery214040478116061364444_1522324096074 就是一个临时代理函数。
$.getJSON方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式来调用jsonp的回调函数。
另外jsonp是无法post数据的,尽管jQuery.getJSON(url, [data], [callback]); 提供data参数让你可以携带数据发起请求,但这样是以get方式传递的。比如:
1 | //$.getJSON()方法 |
调用$.ajax()方法指定type为post,它还是会转成get方式请求
1 | $.ajax({ |
CORS
CORS定义一种跨域访问的机制,全称是”跨域资源共享”(Cross-origin resource sharing),可以让AJAX实现跨域访问。CORS 允许一个域上的网络应用向另一个域提交跨域 AJAX 请求。实现此功能非常简单,只需由服务器发送一个响应标头即可。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
即服务器响应头设置
1 | header('Access-Control-Allow-Origin: *'); // "*"号表示允许任何域向服务器端提交请求;也可以设置指定的域名,那么就允许来自这个域的请求: |
- Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
- Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
- Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
example.com/cors.html1
2
3
4
5
6
7
8
9
10$.ajax({
type: 'post',
url: "http://w2.example.com/cors.php",
crossDomain: true,
data: {u: 'Uknow', age: 20},
dataType: "json",
success: function(r){
console.log(r);
}
});
w2.example.com/cors.php
1 | <?php |
这样也是可以实现跨域post数据的。
兼容性。CORS是W3C中一项较新的方案,所以部分浏览器还没有对其进行支持或者完美支持,详情可移至 http://www.w3.org/TR/cors/。
安全问题。CORS提供了一种跨域请求方案,但没有为安全访问提供足够的保障机制,如果你需要信息的绝对安全,不要依赖CORS当中的权限制度,应当使用更多其它的措施来保障。