同源策略

概述

含义

所谓同源是值三个相同

  • 协议相同
  • 域名相同
  • 端口相同

目的

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

设想这样一种情况: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。

实验 1 cookie document.domain

w1.example.com/a.html

1
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>Title</title>
</head>
<body>

<script>
var cookieName = 'Cookie';
var cookieValue = 'Uknow';
var myDate = new Date();
myDate.setMonth(myDate.getMonth() + 12);
document.cookie = cookieName +"=" + cookieValue + ";expires=" + myDate
+ ";domain=.example.com;path=/";
alert(document.cookie);
</script>
</body>
</html>

w2.example.com/b.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
document.domain = 'example.com';
alert(document.cookie);
</script>
</body>
</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
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 lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<script>
document.domain = 'example.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://w2.example.com/b.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
// 这里操作DOM
var oUl = doc.getElementById('ul1');
alert(oUl.innerHTML);
ifr.onload = null;
};
</script>
</body>
</html>

子域 w2.example.com/b.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
document.domain = 'example.com';
</script>
<ul id="ul1">我是子域w2.example.com中的UL</ul>
</body>
</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.html

1
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.html

1
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
2
3
4
5
6
7
8
9
10
11
var onmessage = function (event) {
var data = event.data;
var origin = event.origin;
//do someing
};
if (typeof window.addEventListener != 'undefined') {
window.addEventListener('message', onmessage, false);
} else if (typeof window.attachEvent != 'undefined') {
//for ie
window.attachEvent('onmessage', onmessage);
}

回调函数第一个参数接收 event 对象,有三个常用属性:

  • data: 消息
  • origin: 消息来源地址
  • source: 源 DOMWindow 对象

实验 4 iframe window.postMessage

example.com/a.html

1
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.html

1
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
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<title>jsonp-test</title>
</head>
<body>
<script type="text/javascript">
function callback_data (data) {
console.log(data);
}
</script>
<script type="text/javascript" src="http://w2.example.com/jsonp.php?callback=callback_data"></script>
</body>
</html>

w2.example.com/jsonp.php

1
2
3
4
5
<?php
$callback = $_GET['callback']; // 获取回调函数名
$arr = array("name" => "Uknow", "blog" => "uknowsec.cn"); // 要请求的数据
echo $callback."(". json_encode($arr) .");"; // 输出
?>

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
2
3
$.getJSON("http://w2.example.com/jsonp.php?callback=?", function(data){
console.log(data);
});

原理是一样的,只不过我们不需要手动的插入script标签以及定义回掉函数。jQuery会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。

从请求的url和响应的数据就可以很明显的看出来了:

jQuery214040478116061364444_1522324096074 就是一个临时代理函数。

$.getJSON方法会自动判断是否跨域,不跨域的话,就调用普通的ajax方法;跨域的话,则会以异步加载js文件的形式来调用jsonp的回调函数。

另外jsonp是无法post数据的,尽管jQuery.getJSON(url, [data], [callback]); 提供data参数让你可以携带数据发起请求,但这样是以get方式传递的。比如:

1
2
3
4
//$.getJSON()方法
$.getJSON("http://127.0.0.1:9000/jsonp.php?callback=?", {u:'abc', p: '123'}, function(jsonData){
console.log(jsonData);
});

调用$.ajax()方法指定type为post,它还是会转成get方式请求

1
2
3
4
5
6
7
8
9
10
$.ajax({
type: 'post',
url: "http://w2.example.com/jsonp.php",
crossDomain: true,
data: {u: 'Uknow', age: 20},
dataType: "jsonp",
success: function(r){
console.log(r);
}
});

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
2
3
header('Access-Control-Allow-Origin: *'); // "*"号表示允许任何域向服务器端提交请求;也可以设置指定的域名,那么就允许来自这个域的请求:
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Max-Age: 1000');
  • Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
  • Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
  • Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

example.com/cors.html

1
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
2
3
4
5
6
7
8
9
10
11
<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Max-Age: 1000');
if($_POST){
$arr = array('name' => $_POST['u'], 'age' => $_POST['age']);
echo json_encode($arr);
} else {
echo json_encode([]);
}
?>

这样也是可以实现跨域post数据的。

兼容性。CORS是W3C中一项较新的方案,所以部分浏览器还没有对其进行支持或者完美支持,详情可移至 http://www.w3.org/TR/cors/。
安全问题。CORS提供了一种跨域请求方案,但没有为安全访问提供足够的保障机制,如果你需要信息的绝对安全,不要依赖CORS当中的权限制度,应当使用更多其它的措施来保障。

Reference

js实现跨域
跨域资源共享 CORS 详解
浏览器同源政策及其规避方法