您的位置:首页 > 其它

毁人不倦-令人困惑的浏览器安全策略:同源策略

2017-03-05 14:43 501 查看

同源策略(The same-origin policy)

这是浏览器的一个基本却又非常重要的安全策略,浏览器会限制对
异源(异域)
(我们常称之为别人家的站点)的资源操作。打个比方,你不会让老王来你家,也不允许他在你家墙上打个洞,装个监控啥的。通过这个比喻你就知道
同源策略
的重要性了。

同源策略
主要针对
脚本(script)
的行为进行限制,而
<script>
,
<link>
,
<img>
,
<iframe>
,
<object>
等带有
src
属性的
dom元素
一般不受影响,这也很好理解,你可以禁止老王进你家,但是无法限制他在自己家装个雷达对你家进行监控。因此,在没有得到授权的情况下,用
javascript
脚本操作
异域
的资源,那是不允许的。科学的说法应该是:浏览器允许发起请求,但如果响应中没有包含对方的许可的话,浏览器就会屏蔽响应结果,不给你用。经常会抛出这样的异常:
XMLHttpRequest cannot load http://www.othersite.com/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://www.mysite.com' is therefore not allowed access.


源(Origin)

唠叨了半天
异源(异域)
,科学的解释一下什么是
源(Origin)


公式:
Origin = [protocol]://[domain]:[port]


Origin
是你的页面所在的位置。例如我有个站点
www.mysite.com
,那么它的
就是
http://www.mysite.com:80
,其中
[protocol]
的缺省值是
http
[port]
的缺省值是
80


不管我的站点在哪个页面
www.mysite.com/p/1.html
,只要
[protocol]
,
[domain]
,
[port]
三者相同则视为
同源
,或者叫
同域
,通常称之为
同域
,因为我们通常都是叫别人的小名
二狗子
,而不会称呼其大名
犬次郎


更多示例:

URLOrigin
http://www.mysite.com/p/1.html
http://www.mysite.com
https://wwww.mysite.com/p/1.html
https://www.mysite.com
http://app.mysite.com/p/1.thml
http://app.mysite.com
http://www.mysite.net/p/1.html
http://www.mysite.net
http://www.mysite.com:9000/p/1.html
http:www.mysite.com:9000
http://www.mysite.com/news/fresh.html
http://www.mysite.com

解决访问跨域资源的问题

虽然是安全了,但是如果想从我的
www.mysite.com
去我的分站
son.mysite.com
获取点东西也会被
同源策略
禁止,这就不是我们想要的了,那怎么办呢?可以利用
<script>
等不受
同源策略
限制的
dom元素
绕过去,这种方式称之为
jsonp
,这是很多年前就提出来的方法,很巧妙不过很繁琐,渐渐地不怎么再使用了,有兴趣的自行
google


还有现代化的解决方案:
CORS(Cross-Origin Resource Sharing)
。还记得
同源策略
的规定吗?通过屏蔽响应结果的方式保证信息安全,也就是说浏览器并没有阻止发起跨域请求。浏览器如果在跨域请求的响应(
Http Response Headers
)中发现了对方的许可就会认为是安全的。这套标准称之为CORS。

这套标准规定一系列的
Http Headers
,让服务器申明哪些资源是可以被谁访问,浏览器通过解析响应头部就能知道是否得到了许可。

举两个简单的🌰栗子说明这一系列的
Http Headers
:

www.mysite.com
中有如下脚本,要去访问
www.othersite.com
的资源。

<script>
var request = new XMLHttpRequest();
var url = 'www.othersite.com/post/001/';

function callOtherDomain() {
if(request) {
request.open('GET', url, true);
request.onreadystatechange = handler;
request.send();
}
}
</script>

通过浏览器的控制台,查看到该请求,请求和响应报文的重要内容如下:

请求报文

GET /post/001 HTTP/1.1
Host: www.othersite.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: www.mysite.com
Origin: http://www.mysite.com[/code] 响应报文

HTTP/1.1 200 OK
Date: Sat, 04 Mar 2017 14:23:53 GMT
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked

这是一个简单的
Get
请求,在请求报文中有一个值得注意的
Origin
,这个属性就表示了当前所处的
,
Origin
是由浏览器自动控制的,不允许用户干预。如果
同源策略
判定请求是跨域请求,那么就会自动把
Origin
加入请求头部中。

在响应报文中,注意
Access-Control-Allow-Origin
属性,如果对方允许你跨域访问,那么它会在响应中加入你的请求头部中的
Origin
,此处的响应报文中的
*
表示允许任何请求的跨域访问。

https://www.mysite.com
要去修改
https://posts.mysite.com
中的一篇文章。

var request = new XMLHttpRequest();
var url = 'posts.mysite.com/?pid=1024';
var body = 'new post';

function callOtherDomain(){
if(request)
{
request.open('PUT', url, true);
request.setRequestHeader('userid', 'keke');
request.onreadystatechange = handler;
request.send(body);
}
}

这次是发送了
PUT
类型的请求,要去修改
pid=1024
的文章,同时还携带了我的身份信息
userid=keke
在请求头部中。继续在浏览器的控制台中观察请求信息,发现有两次请求,第一个如下:

OPTIONS /?pid=1024
Host: https://posts.mysite.com Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: https://www.mysite.com Access-Control-Request-Method: PUT
Access-Control-Request-Headers: userid

HTTP/1.1 200 OK
Date: Sat, 04 Mar 2017 14:35:39 GMT
Access-Control-Allow-Origin: https://www.mysite.com Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT
Access-Control-Allow-Headers: userid
Access-Control-Max-Age: 1728000
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

这是因为这次发送的不是简单请求,CORS规范要求先发个
预检请求(Preflight)
,一般会采用
OPTIONS
类型,该请求不包含请求体,会携带一些用于探测的信息,除了
Origin
,还有

* `Access-Control-Request-Method`
* `Access-Control-Request-Headers`

前者用来携带真正的请求的类型,后者携带真实请求自定义的头。

在响应报文中还有
Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT
表示对方许可的请求类型。

对方会根据
预检请求
中的信息判断是否可以接受真正的请求,如果
预检请求
通过了,浏览器才会发起真正的请求。

PUT /?pid=1024/ HTTP/1.1
Host: https://posts.mysite.com Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
userid: keke
Content-Type: text/xml; charset=UTF-8
Referer: https://www.mysite.com Content-Length: 8
Origin: https://www.mysite.com Pragma: no-cache
Cache-Control: no-cache
……

HTTP/1.1 200 OK
Date: Sat, 04 Mar 2017 14:35:39 GMT
Access-Control-Allow-Origin: https://www.mysite.com Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
……

响应报文报文头部中带有
Access-Control-Allow-Origin: https://www.mysite.com[/code],表示对方同意 
https://www.mysite.com
的请求,浏览器就不会屏蔽响应结果。

上述栗子中,提到了
CORS
规定了对于不简单的请求类型,要先发一个
预检请求(Preflight)
,那么简单与否的判定条件是什么呢?在如下范围内的请求都是被视为简单请求

请求类型的范围限制:
GET
,
HEAD
,
POST


自定义的请求头部限制范围:
Accept
,
Accept-Language
,
Content-Language


媒体类型(Content-Type)的限制范围的:
application/x-www-form-urlencoded
,
multipart/form-data
,
text/plain


除来上述栗子中提到的几个头部,还有哪些呢?如下明细:

HTTP请求头部

Origin
: 表示发送请求者的源(域),浏览器控制的。

Access-Control-Request-Method
: 这是
预检请求(Preflight)
中表示真实请求的请求方式。

Access-Control-Request-Headers
: 这是
预检请求(Preflight)
中表示真实请求的自定义的头部,可以有多个(
Access-Control-Request-Headers: userid, pwd, location
:表示真实请求会携带3个自定义头部(
userid
,
pwd
,
location
))。

HTTP响应头部

Access-Control-Allow-Origin: <origin> | *
: origin参数表示对方允许访问的URI.对于一个不带有credentials的请求,可以指定为'*',表示允许来自所有域的请求。

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
: 设置允许的请求头部。

Access-Control-Max-Age: <delta-seconds>
: 这个头告诉我们这次
预检请求
的结果的有效期是多久,delta-seconds 参数表示,允许这个
预检请求
的参数缓存的秒数,在此期间,不用发出另一条
预检请求


Access-Control-Allow-Credentials: true | false
: 告知客户端,当请求的credientials属性是true的时候,响应是否可以被得到.当它作为
预检请求
的响应的一部分时,它用来告知实际的请求是否使用了credentials.注意,简单的GET请求不会预检,所以如果一个请求是为了得到一个带有credentials的资源,而响应里又没有Access-Control-Allow-Credentials头信息,那么说明这个响应被忽略了。

Access-Control-Allow-Methods: <method>[, <method>]*
: 这个响应头信息在客户端发出
预检请求
的时候会被返回,表示被允许的请求方式。

Access-Control-Allow-Headers:<field-name>[, <field-name>]*
: 也是在响应
预检请求
的时候使用。用来指明在实际的请求中,可以使用哪些自定义HTTP请求头。

对于
Access-Control-Allow-Credentials
这个头部只会出现在请求头部中包含了
凭证(HttpCookie)
信息。一般而言,对于跨站请求,浏览器是不会发送
凭证
信息的。但如果将XMLHttpRequest的一个特殊标志位设置为true,浏览器就将允许该请求的发送。

var invocation = new XMLHttpRequest();
var url = 'http://www.othersite.com';

function callOtherDomain(){
if(invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}

再例如使用
jQuery
发起
ajax
请求时:

$.ajax({
//...
xhrFields: {
withCredentials: true
},
//...
});


针对
脚本(script)
的跨域操作是安全了,可是如果通过
<iframe>
这种
dom元素
来嵌入资源的话,
同源策略
就无法保护我们了。

针对
<iframe>
的安全策略

我们肯定不希望自己的页面被不法分子用
<iframe src="www.mysite.com"></frame>
等方式嵌入,然后被利用。浏览器们早已考虑到这个漏洞,并提出了解决方案。类似于CORS标准,解析响应头部中的
X-Frame-Options
,用这个头部信息来表达是否可以被对方嵌入。

X-Frame-Options
有三种值:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
X-Frame-Options: ALLOW-FROM https://www.othersite.com/[/code] 
DENY
: 无论请求者是谁,都不允许嵌入。

SAMEORIGIN
: 只有
同源(origin)
的页面才可以嵌入。

ALLO-From https://www.othersite.com/[/code]: 只有
https://www.othersite.com/
才可以嵌入咱们的页面。

例如,
www.othersite.com
要用如下代码嵌入我的页面
www.mysite.com


...
<iframe src="www.mysite.com"></iframe>
...

我给
www.mysite.com
的响应头部中加入
X-Frame-Options: SAMEORIGIN
,浏览器会屏蔽响应结果,并报错
Refused to display 'http://www.mysite.com/' in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'.


这些都是基本的网络安全规范,但是其重要性却不可忽略。面对红果果的互联网,时刻不能放松。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: