同源策略(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'.
這些都是基本的網絡安全規范,但是其重要性卻不可忽略。面對紅果果的互聯網,時刻不能放松。