毁人不倦-令人困惑的浏览器安全策略:同源策略
同源策略(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]
三者相同则视为同源
,或者叫同域
,通常称之为同域
,因为我们通常都是叫别人的小名二狗子
,而不会称呼其大名犬次郎
。
更多示例:
URL | Origin |
---|---|
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
响应报文
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
,表示对方同意
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/
DENY
: 无论请求者是谁,都不允许嵌入。SAMEORIGIN
: 只有同源(origin)
的页面才可以嵌入。ALLO-From https://www.othersite.com/
: 只有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'.
这些都是基本的网络安全规范,但是其重要性却不可忽略。面对红果果的互联网,时刻不能放松。