定义
同源:两个站点同协议、同域名、同端口则认为是同源,只要有一项不同,就认为是非同源。
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。
同源策略是浏览器最核心、最基本的安全策略,现行的web安全策略很多以同源策略为基础。
为什么要有同源
主要是为了保护cookie,因为http协议无状态的特点,想要服务器端记住客户端,则可以通过cookie和session机制,这样,客户端请求报文cookie中携带的sessionid就是该客户端在服务器端的标识。JavaScript可以跨域读取到cookie,同源策略不允许跨域发送cookie,想象这样一个场景:A页面向A服务器发送请求,A页面携带有sessionId,服务器能认证A的请求,这时在同一个浏览器的另一个tab页面打开钓鱼网站B,其中嵌入了代码时对A服务器的请求A页面,如果这个时候A服务器也能收到这个sessionid,那钓鱼网站就能顺利认证进入A页面了。
IE同源策略的不同之处
在同源策略中,Internet Explorer有两点不同:
- 授信范围(Trust Zones):两个相互之间高度互信的域名,如公司域名 (corporate domains),不遵守同源策略的限制。
- 端口:未将端口号加入到同源策略的组成部分之中,因此 http://abc.com:81/index.html 和http://abc.com/index.html 属于同源并且不受任何限制。
浏览器的同源机制
(1)不受同源策略限制
- 页面中的链接、重定向以及表单提交
- 跨域资源的引入不受同源策略的限制,如
(2)同源策略主要的限制范围
- Cookie、LocalStorage、sessionStorage和 IndexDB 无法读取
localStorage、IndexedDB等数据存储会以源进行分割,每个源拥有自己独立的存储空间,一个源的js脚本不能对属于其他源的数据进行读写操作。
cookies同样只有同源网页才能共享,设置其domain、path、secure、HttpOnly属性可以来限定其访问性。 - DOM 节点无法读取和设置
- AJAX 请求不能发送
属性 | 作用 |
---|---|
domain | 指定cookies对哪个域有效,cookies只会发向该域,默认值是设置cookie的那个域 |
path | 表示相对于domain的路径,只有在该路径下才能拿到cookies,默认值为/ |
secure | 设置了该属性或者设置了’secure=true’表示只能在 HTTPS 连接中传递cookies |
HttpOnly | 设置了该属性或这设置了’HttpOnly=true’表示js脚本不能读取到cookie信息 |
实现跨域读取
跨域分为:
- XMLHttpRequest的跨域
- Cookie跨域
- 跨窗口的跨域通信
- 跨源数据存储
1. XMLHttpRequest的跨域
四种实现方式:
- JSONP
- CORS
- WebSocket
- 代理
(1)JSONP(JSON with Padding)
JSONP是 json 的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。利用的是src属性不受同源的限制机制,所以jsonp的实现方式其实就是和
在网页中创建一个script标签,src为请求的url,请求的查询字符串有一个callback参数,用来指定回调函数的名称,回调函数在js脚本中声明好。当服务器收到请求后,返回一句js脚本,内容是将json数据作为参数传入回调函数并调用该函数。
示例:
//前端:
var jsonp = {
exec: function() {
var script = document.getElementById('jsonp');
if(script) {
script.parentElement.removeChild(script);
}
//创建<script>标签
script = document.createElement('script');
script.id = 'jsonp';
//返回js脚本:
//jsonp.jsonpcallback({"code":1000,"data":{"username":"carl","userAge":20,"userSex":"男"}})
script.src = 'http://localhost:8080/getusermsg?callback=jsonp.jsonpcallback';
document.head.appendChild(script);
},
//返回js脚本时会调用该函数
jsonpcallback: function (userdata) {
alert('姓名:' + userdata.data.username);
alert('年龄:'+ userdata.data.userAge);
alert('性别:' + userdata.data.userSex);
}
}
$('#btn1').click(jsonp.exec);
//服务端:
function getusermsg(req, res, next) {
if(req.url.match(api.getusermsg)) {
var queryJson = queryParse(req.url.split('?')[1]);
var fb = {code: 1000, data: {username: 'carl', userAge: 20, userSex: '男'}};
//查询字符串中有callback参数
if(queryJson.callback) {
res.writeHead(200);
//返回调用回调函数的字符串,前端以js脚本来解析并执行
res.write(queryJson.callback + '(' + JSON.stringify(fb) + ')');
}else {
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200);
res.write(JSON.stringify(fb));
}
}
next();
}
(2)CORS(Cross-origin resource sharing)
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。它的通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息(如origin请求头),有时还会多出一次附加的请求(预检请求),但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)
- 浏览器发出CORS简单请求,只需要在头信息之中增加一个Origin字段。
- 浏览器发出CORS非简单请求,会在正式通信之前,增加一次HTTP查询请求(请求方式为OPTIONS),称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
只要同时满足以下两大条件,就属于简单请求
- 请求方法是以下三种方法之一:
HEAD
GET
POST- HTTP的头信息不超出以下几种字段:
AcceptAccept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
CORS需要服务器响应头中增加下面一种或几种:
//必须指定。表示允许任意源的访问,也可以指定特定的源
1.Access-Control-Allow-Origin:*
.
//可选。它的值是一个布尔值,表示是否允许发送Cookie.默认情况下,不发送Cookie,即:false。对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json,这个值只能设为true。如果服务器不要浏览器发送Cookie,删除该字段即可。设置为true需同时在ajax请求中设置withCredentials: true
2.Access-Control-Allow-Credentials: true
.
//可选。如果设置了该值,则预检请求响应头会返回该头信息,表示所有支持的方法,而不单是浏览器请求的那个方法,这是为了避免多次"预检"请求
3.Access-Control-Allow-Methods: GET, POST, PUT
.
//可选,允许携带的请求头部信息
4.Access-Control-Allow-Headers:Content-Type,User-Agent,Token
.
//可选。用来指定本次预检请求的有效期,单位为秒。在有效期间,不用发出另一条预检请求。
5.Access-Control-Max-Age: 1728000
.
//可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他头部信息,就必须在Access-Control-Expose-Headers里面指定。
6.Access-Control-Expose-Headers: Connection,Content-Encoding
CORS请求头会携带的头信息:
//必要请求头,表示当前源,相应的预检响应需要返回Access-Control-Allow-Origin
1.Origin
.
//预检时会带上的头,表示真正请求的方法,相应的预检响应需要返回Access-Control-Allow-Method
2.Access-Control-Request-Method
.
//预检时会带上的头,表示真正请求会额外发送的头信息,相应的预检响应需要返回>Access-Control-Allow-Headers
3.Access-Control-Request-Headers
示例:
//www7.html 存放在本机80端口上的资源
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script>
$.ajax({
url:"http://localhost:8081/www5.php",
type:"get", //请求方式
dataType:"JSON",
data:{id:1,abc:12}, //请求参数
withCredentials:true,
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader('Token', '123456789'); //自定义header头
},
contentType: 'application/json',
success: function(data){
console.log("成功");
console.log(data);
},
error : function(jqXHR) {
console.log("失败");
console.log(jqXHR.status);
}
})
</script>
</html>
//www5.php 存放在本机8081端口上的资源
<?php
if($_SERVER['REQUEST_METHOD'] == 'OPTIONS'){
//配置请求OPTIONS响应参数
header('Access-Control-Allow-Origin:http://localhost/www7.html');
header('Access-Control-Allow-Headers:Accept,Referer,Host,Keep-Alive,User-Agent,X-Requested-With,Cache-Control,Content-Type,Cookie,Token');
header('Access-Control-Allow-Credentials:true');
header('Access-Control-Allow-Methods:GET,POST,OPTIONS');
header('Access-Control-Max-Age:1728000');
header('Content-Type:text/plain charset=UTF-8');
header('Content-Length: 0', true);
header('status: 200');
header('HTTP/1.0 204 No Content');
exit;
}else{
header('Access-Control-Allow-Origin:http://localhost/www7.html');
header('Access-Control-Allow-Credentials: true');
header("Access-Control-Allow-Methods:GET, POST, PUT,DELETE,POSTIONS");
header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie,Token");
}
$headers = apache_request_headers();
echo json_encode(array("headers"=>$headers));
预检请求的请求头部:
真正请求的请求头部:
(3)WebSocket
WebSocket是一种新的通信协议,能够在一个持久连接上提供全双工、双向通信。使用url模式也略有不同。未加密连接使用ws://,加密连接使用wss://,最重要的一点是该协议不实行同源策略。服务器需要自己确定请求源是否在白名单内,从而过滤恶意的请求。
这里以PHP的swoole举例,swoole安装在linux的php上
//js代码
<script>
var wsServer = 'ws://linux远程服务器ip:8080';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
console.log("Connected to WebSocket server.");
};
websocket.onclose = function (evt) {
console.log("Disconnected");
};
websocket.onmessage = function (evt) {
console.log('Retrieved data from server: ' + evt.data);
};
websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
};
</script>
//PHP swoole代码
<?php
$ws = new swoole_websocket_server("0.0.0.0", 8080);
$ws->on('open', function ($ws, $request) {
var_dump($request->fd, $request->get, $request->server);
$ws->push($request->fd, "hello, welcome\n");
});
$ws->on('message', function ($ws, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
});
$ws->on('close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
});
$ws->start();
(4)代理
- 正向代理
需要借助同源的代理服务器,可以安装浏览器的代理服务器插件,浏览器先将请求发送给代理服务器,代理服务器接收请求并其转发给目标数据服务器,目标服务器接收到的源来自代理服务器认定为同源,然后将数据返回给代理服务器,再有代理服务器返回给浏览器。 - 反向代理
通常可以用Nginx配置反向代理,浏览器想访问的资源在不同源的服务器B上,可以向让浏览器访问A服务器,再由A服务器做反向代理,去B服务器上拿到资源后返回给A服务器,A服务器返回给浏览器。
2. Cookie跨域
同源的页面才可以共享cookie,但是如果两个源的一级域名相同,二级域名不同,浏览器可以通过设置document.domain来共享cookie,比如有:
domainA:http://gg.jjp.com/index.html
domainB:http://bb.jjp.com/index.html
现在想让domainA和domainB能互相访问对方的cookie,可以双方都设置document.domain为jjp.com,domainA则能够访问到domainB设置的cookie,domainB也能访问到domainA设置的cookie.
3. 跨窗口的跨域通信
iframe窗口和window.open打开的窗口若与父窗口不是同源的,都无法与创建它们的父窗口通信,无法互相访问对方的document对象。
如果两个窗口一级域名相同,二级域名不同,可以通过设置document.domain解决。
但是对于完全不同源的窗口,想要进行通信,可以通过下面的方法:
1.片段识别符
2.window.name
3.window.postMessage
方案1.片段识别符
地址栏中url的#后面的内容变化是不会引起页面的刷新的,这部分内容就是片段识别符,当片段识别符内容变化时,会触发hashchange事件。
因此发信息的窗口可以把信息写入接收信息窗口的片段标识符中,接收信息窗口监听hashchange事件来取得自己的片段标识符,从而来达成通信的目的。
方案2.window.name
window.name值在不同的页面(甚至不同域名)加载后依旧存在,并且值最大可以达到2MB。
示例:窗口A和窗口B不同源,现在A想拿到窗口B的消息,可以借助window.name 以及 iframe实现跨域通信
步骤:
1.窗口A在页面中动态添加一个iframe,将其src置为窗口B页面地址
2.iframe加载了窗口B的页面,窗口B将要发送的消息写入window.name中
3.由于窗口A与iframe处于不同域,因为同源策略,窗口A不能访问iframe的window.name
4.此时再让iframe加载一个与窗口A同源的页面,使窗口A与iframe属于同域
5.窗口A读取iframe的window.name,至此接收到窗口B发送的消息,完成通信
方案3.window.post
Messagewindow.postMessage是HTML5引入的一个新的api,允许两个窗口通信,不论是否两个窗口是否同源.
示例:
//发送信息的窗口:http://jjp.com
var sonWin = window.open('https://www.baidu.com','百度');
//参数:要发送的信息、接受信息的窗口的源
sonWin.postMessage('你好,百度', 'https://www.baidu.com');
//接收信息的窗口:https://www.baidu.com
//监听postMessage事件
window.addEventListener('message', function(event) {
//event.source:发送消息的窗口
//event.origin: 发送消息的网址
//event.data: 消息内容
if(event.origin === 'http://jjp.com') {
event.source.postMessage('Got it', event.origin);
console.log(event.data)
}
});
4. 跨源数据存储
通过window.postMessage,能够实现读写其他窗口的localStorage和IndexDB。
在用window.postMessage实现窗口间的通信的基础上进行
- 写:
接收其他窗口的消息时,将消息作为值其存入 - 读:
接受其他窗口的消息时,将消息作为键值取出值,并将值通过postMessage发送给其他窗口