生產(chǎn)上上線了展示能力輸出功能,此功能將把內(nèi)部的H5頁面,在其他APP可以嵌入,涉及到跨域訪問的問題
進行總結(jié)
參考文檔:
跨域資源共享 CORS 詳解
跨域的那些事兒
1.什么是跨域
別笑,之前我還真不知道
什么是跨域?
跨域,指的是瀏覽器不能執(zhí)行其他網(wǎng)站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器施加的安全限制。
所謂同源是指,域名,協(xié)議,端口均相同,不明白沒關系,舉個栗子:
http://www.123.com/index.html 調(diào)用 http://www.123.com/server.php (非跨域)
http://www.123.com/index.html 調(diào)用 http://www.456.com/server.php (主域名不同:123/456,跨域)
http://abc.123.com/index.html 調(diào)用 http://def.123.com/server.php (子域名不同:abc/def,跨域)
http://www.123.com:8080/index.html 調(diào)用 http://www.123.com:8081/server.php (端口不同:8080/8081,跨域)
http://www.123.com/index.html 調(diào)用 https://www.123.com/server.php (協(xié)議不同:http/https,跨域)
請注意:localhost和127.0.0.1雖然都指向本機,但也屬于跨域。
瀏覽器執(zhí)行javascript腳本時,會檢查這個腳本屬于哪個頁面,如果不是同源頁面,就不會被執(zhí)行。
2.瀏覽器的同源策略
同源策略又分為以下兩種
- DOM同源策略:禁止對不同源頁面DOM進行操作。這里主要場景是iframe跨域的情況,不同域名的iframe是限制互相訪問的。
XmlHttpRequest同源策略:禁止使用XHR對象向不同源的服務器地址發(fā)起HTTP請求。 - 只要協(xié)議、域名、端口有任何一個不同,都被當作是不同的域,之間的請求就是跨域操作。
3.為什么我們需要跨域限制
主要是出于安全的考慮
AJAX同源策略主要用來防止CSRF攻擊。如果沒有AJAX同源策略,相當危險,我們發(fā)起的每一次HTTP請求都會帶上請求地址對應的cookie,那么可以做如下攻擊:
- 用戶登錄了自己的銀行頁面 http://mybank.com,http://mybank.com向用戶的cookie中添加用戶標識。
- 用戶瀏覽了惡意頁面 http://evil.com。執(zhí)行了頁面中的惡意AJAX請求代碼。
- http://evil.com向http://mybank.com發(fā)起AJAX HTTP請求,請求會默認把http://mybank.com對應cookie也同時發(fā)送過去。
- 銀行頁面從發(fā)送的cookie中提取用戶標識,驗證用戶無誤,response中返回請求數(shù)據(jù)。此時數(shù)據(jù)就泄露了。
- 而且由于Ajax在后臺執(zhí)行,用戶無法感知這一過程。
DOM同源策略也一樣,如果iframe之間可以跨域訪問,可以這樣攻擊:
- 做一個假網(wǎng)站,里面用iframe嵌套一個銀行網(wǎng)站 http://mybank.com。
- 把iframe寬高啥的調(diào)整到頁面全部,這樣用戶進來除了域名,別的部分和銀行的網(wǎng)站沒有任何差別。
- 這時如果用戶輸入賬號密碼,我們的主網(wǎng)站可以跨域訪問到http://mybank.com的dom節(jié)點,就可以拿到用戶的輸入了,那么就完成了一次攻擊。
4.如何做到合理的跨域訪問---CORS
理論基礎:CORS:”跨域資源共享”(Cross-origin resource sharing),這是一個W3C標準
CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10。
整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對于開發(fā)者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn)AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
因此,實現(xiàn)CORS通信的關鍵是服務器。只要服務器實現(xiàn)了CORS接口,就可以跨源通信
- CORS 機制
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
具體可參考開頭的阮一峰的參考文獻
簡要來講,非簡單請求多了一個預檢的操作
4.1 預檢會是一個OPTIONS的請求,例如
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...`
4.2 服務器需要對預檢請求進行回應
服務器收到"預檢"請求以后,檢查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,確認允許跨源請求,就可以做出回應。
> HTTP/1.1 200 OK
> Date: Mon, 01 Dec 2008 01:15:39 GMT
> Server: Apache/2.0.61 (Unix)
> Access-Control-Allow-Origin: http://api.bob.com
> Access-Control-Allow-Methods: GET, POST, PUT
> Access-Control-Allow-Headers: X-Custom-Header
> Content-Type: text/html; charset=utf-8
> Content-Encoding: gzip
> Content-Length: 0
> Keep-Alive: timeout=2, max=100
> Connection: Keep-Alive
> Content-Type: text/plain
如果瀏覽器否定了"預檢"請求,會返回一個正常的HTTP回應,但是沒有任何CORS相關的頭信息字段。這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發(fā)一個錯誤,被XMLHttpRequest對象的onerror回調(diào)函數(shù)捕獲。控制臺會打印出如下的報錯信息。
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
(1)Access-Control-Allow-Methods
該字段必需,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是為了避免多次"預檢"請求。
(2)Access-Control-Allow-Headers
如果瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限于瀏覽器在"預檢"中請求的字段。
(3)Access-Control-Allow-Credentials
該字段與簡單請求時的含義相同。
(4)Access-Control-Max-Age
該字段可選,用來指定本次預檢請求的有效期,單位為秒。上面結(jié)果中,有效期是20天(1728000秒),即允許緩存該條回應1728000秒(即20天),在此期間,不用發(fā)出另一條預檢請求。
4.3 瀏覽器的正常請求和回應
一旦服務器通過了"預檢"請求,以后每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭信息字段。服務器的回應,也都會有一個Access-Control-Allow-Origin頭信息字段。
下面是"預檢"請求之后,瀏覽器的正常CORS請求。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面頭信息的Origin
字段是瀏覽器自動添加的。
下面是服務器正常的回應。
> Access-Control-Allow-Origin: http://api.bob.com
> Content-Type: text/html; charset=utf-8
特別注意!!!
上面頭信息中,Access-Control-Allow-Origin
字段是每次回應都必定包含的。
5 作為服務器開發(fā)者,我們到底怎么做的
5.1 我們需要做什么
可以從上文的流程中看到,服務器的開發(fā)者,需要做的時候分2步
1.在預檢請求中,添加信息,并返回200/204
204是一個沒有響應體的成功響應2.在后續(xù)的請求中,繼續(xù)添加Access-Control的信息
5.2 實際操作
生產(chǎn)我們用Nginx作為反向代理,所以我們需要再Nginx進行處理
- 一個普通的Nginx配置
server {
listen 80; #監(jiān)聽80端口,可以改成其他端口
server_name localhost; # 當前服務的域名
location ~ *.json {
proxy_pass 你的服務器;
}
我們需要處理邏輯的條件是2個,域名+是否為opation請求
所以我們Nginx需要對這2個條件進行判斷,本來是很簡單的事情,但
!!!nginx不支持多重判斷
所以,配置成了這樣
server {
listen 80; #監(jiān)聽80端口,可以改成其他端口
server_name localhost; # 當前服務的域名
location ~ *.json {
proxy_pass 你的服務器;
set $flag 0;
if ($http_origin ~ (域名A| 域名B)){
set $flag "${flag}1";
}
if ($request_method = 'OPTIONS'){
set $flag "${flag}2";
}
if ( $flag = "012" ){
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'X-PINGOTHER,Content-Type,Accept,Origin,User-Agent,Cache-Control,isOutput';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
return 204;
}
if ( $flag = "01" ){
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'X-PINGOTHER,Content-Type,Accept,Origin,User-Agent,Cache-Control,isOutput';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
}
}
千萬別忘了,opation預檢請求后,仍然要添加信息,只是不需要直接返回204而已
6.跨域和CDN的坑
當一切在開發(fā)環(huán)境和測試環(huán)境驗證無誤后,上生產(chǎn)發(fā)現(xiàn)了新的問題。
以A域名為例,已經(jīng)在Nginx上配置了A域名可以跨域
問題的發(fā)現(xiàn):
生產(chǎn)驗證環(huán)節(jié),發(fā)現(xiàn)A域名下部分網(wǎng)址可以正常響應,部分網(wǎng)址無法正常相應。
正常跨域請求的Options請求和Get請求都可以獲得服務器配置的請求頭,有Access-Control各個字段,而不正常的請求,只有Options請求有,Get請求沒有,懷疑是Get請求沒有達到服務器6.1 第一步
觀察后發(fā)現(xiàn),不能正常響應的請求,都是.sjson結(jié)尾,此結(jié)尾代表著和CDN廠商規(guī)定的靜態(tài)接口。因此懷疑是CDN的問題。
將不好的請求,改變Url的某個時間戳參數(shù),請求正常執(zhí)行。因此CDN是肯定是原因之一
- 6.2第二步
如果問題都在CDN上,則是因為請求到CDN層直接擊中緩存的原因,沒有達到服務器層獲得最新的跨域配置
繼續(xù)觀察:在 .sjson結(jié)尾的請求中,也有部分參數(shù)請求是好的,部分請求參數(shù)是不好的
那么第二個問題來了,請求要么全是好的,要么全是不好的,為何會有概率的發(fā)生這種情況
因此懷疑是部分Nginx配置有誤,進行排查,發(fā)現(xiàn)所有服務器都配置都已正常替換,服務器重新時間都在版本當晚
(這里因為CDN的緩存機制不了解,因此思考出現(xiàn)了問題,就卡住了)
- 6.3 第三步
聯(lián)系CDN服務廠商,了解了CDN的推送策略有2種,公司的默認是第二種
一是物理上直接將目錄下資源刪除
二是將CDN目錄下的資源置為過期,CDN會回源,比較新的資源是否變化,如果沒變化,則繼續(xù)使用,有變化,則更新
當時立馬讓廠商進行了第一種方式的刪除,而沒有經(jīng)過分析,第二次錯過了發(fā)現(xiàn)問題的機會。
廠商刪除目錄下資源后,再次訪問,發(fā)現(xiàn).sjson 的請求,還是部分正常 部分不正常
6.4第四步
陷入僵局后,再次搜尋資料,病急亂投醫(yī),包括懷疑CDN節(jié)點不同步等等。
這里經(jīng)CDN廠商提醒,跨域請求的網(wǎng)址,正常不跨域的請求也會訪問,2份緩存是同一份,可能存在這個問題,這樣就可以解釋為什么部分請求成功,部分不成功的問題。因為清楚緩存后,哪個請求先到,就會緩存哪份6.5五.如何驗證
廠商同事,用有問題的請求,分別發(fā)送了帶Origin字段和不帶Origin的字段,用md5計算返回報文,比較發(fā)現(xiàn)一致。確認是同一份緩存。6.6六.如何解決
一是前端特殊處理,跨域的請求,加特殊的字段參數(shù),但這需要修改代碼
二是CDN廠商提供的,根據(jù)http請求頭里的Origin字段,為每個值維護一份緩存
最終選擇了第二個方案6.7繼續(xù)測試
為了不影響生產(chǎn),對第二個方案進行測試
廠商提供了一個配置了新的規(guī)則的測試CDN節(jié)點
比較請求:
curl -vo ~/tmp 'https://36.250.240.133/*****.sjson?*****&updTs=20180706004105' -H "Host:訪問的域名" -k -H "Origin: 跨域的域名"
curl -vo ~/tmp 'https://36.250.240.133/*****.sjson?*****&updTs=20180706004105' -H "Host:訪問的域名" -k -H "Origin: 不跨域的域名"
這樣就是2份緩存了,可以比較返回的報文,最終解決了問題。
- 6.8反思點
這里有對CDN緩存機制不了解的原因,雖然知道CDN回源策略有定時,也有Url改變,但沒有想到,這個請求頭里的參數(shù)是不比較的。同樣,以后CDN的接口如果有cookie等請求頭信息,都要注意。不過靜態(tài)請求也不應該包含那些變化的東西。
在了解了CDN推送的策略時,其實就應該想到過期后,CDN回源比較沒更新,說明了請求的返回報文就是和跨域配置改變前是一樣的,這就是同一份緩存