為什麼給你設置重重障礙?講一講Web開發中的跨域
來自專欄做後端開發,除了寫代碼,你還需要知道這些27 人贊了文章
零
最近知乎上居然連續有兩個關於跨域的問題邀請我回答。
雖然網上寫跨域的文章已經有很多很多,且跨域本來是一個來自瀏覽器端(前端)的「問題」。但是跨域問題是一定要後端配合來解決的,加之網上文章大多只是介紹跨域如今已經定下的規則,很少提到規則的由來,其實容易讓人迷惑。
假如你是一個後端工程師,和你配合的前端經驗不夠豐富,而你又對跨域一無所知。當你們遇到跨域帶來的瀏覽器端問題,bug可能根本無從查起。
所以還是寫了這一篇。講講跨域。
一、跨域是個什麼「問題」?
最常見到的跨域「問題」是這樣:
- 我有一個域名
a.com
和一個域名b.com
- 我在
a.com
上有一個介面a.com/api
,會返回一些數據 - 我想在
b.com
域名下的一個頁面上訪問a.com/api
得到數據 - 瀏覽器阻止了我
直覺來講這是一件挺奇怪的事情,我把上面的例子換成一個更實際的:
- 這篇知乎專欄文章,所在域名是
zhuanlan.zhihu.com
- 知乎主站域名是
www.zhihu.com
,用戶數據的api就在這個域名下 - 這個頁面被載入出來時,它還要非同步載入我的用戶數據然後展示出來,訪問了
www.zhihu.com
下的api - 這個操作被瀏覽器阻止了,於是我的用戶數據顯示不出來
(假如知乎後端沒有做跨域的配置)
二、為什麼不讓我跨域?
因為在web交互的環境中,只能保證請求發自某個用戶的瀏覽器,卻不能保證請求本身是用戶自願發出的。
想像這樣一個場景,如果世界上沒有跨域限制,這時假如:
- 支付寶的轉賬操作是一個post請求,大概是
https://alipay.com/api/withdraw/?to_user=kindJeff&amout=1000
- 我寫了一段ajax的post請求代碼,請求連接是上面的url。然後我把這段代碼嵌入我的網站
a.com
- 你不久前登陸過支付寶,瀏覽器里保存了
alipay.com
域名的cookie - 我讓你訪問
a.com
,打開頁面,於是在你不知情的情況下發出了post請求,你的錢就被轉到我的賬號里了
這就是跨站請求偽造(CSRF)。這是一個非常嚴重的後果,其利用的就是網站(上例的支付寶)對瀏覽器的充分信任。
所以瀏覽器一定會設置跨域限制,避免在用戶和網站不知情的情況下發出請求。
三、JSONP——最常用的繞過辦法
回憶一下上面zhuanlan.zhihu.com
和www.zhihu.com
的例子,我只是想要GET
一些數據而已。
當遇到這種跨域問題不知怎麼解決的時候,查詢一下,會發現有兩種解決辦法:
- 如果是子域名下的頁面想訪問父域的api,如
zhuanlan.zhihu.com
想訪問zhihu.com
的api,那可以在發請求前設置一下document.domain
的值為zhihu.com
。畢竟是子域,瀏覽器幾乎沒有做什麼限制。(如我在一個油猴腳本里就這樣用過https://greasyfork.org/zh-CN/scripts/27195) - 在非父子域關係的情況下,如
zhuanlan.zhihu.com
和www.zhihu.com
(或者a.com
和b.com
),就是被瀏覽器當作兩個不同的域名的,一般就會使用JSONP了
JSONP本質上就是讓數據變成js代碼,使用script標籤來載入數據。
如我的用戶數據api本來是https://www.zhihu.com/api/v4/members/kindjeff
,返回值為{"name": "kindJeff", "gender": 1}
想要通過JSONP實現在zhuanlan.zhihu.com
下跨域拿到這個數據,需要做的是:
- 改造這個api,讓他返回一段js代碼而不是json數據。如改造成,你訪問
https://www.zhihu.com/api/v4/members/kindjeff?callback=render
,得到的響應會是render({"name": "kindJeff", "gender": 1})
- 在專欄文章頁面,不使用ajax去拿取數據,而是嵌入一個script標籤:
<script src="https://www.zhihu.com/api/v4/members/kindjeff?callback=render"></script>
- 在專欄文章頁面寫好一個叫render的函數,用來渲染用戶數據
因為瀏覽器並不限制script標籤的src是要從哪裡載入腳本,跨域問題似乎就被JSONP「繞過」了。
四、為什麼JSONP可以?
再想一想,瀏覽器不做script來源的跨域限制,而且大家都喜歡用JSONP並且改造了大量的api響應,問題不是回到了原點嗎?我有一個網站a.com
,在裡面嵌入了支付寶某個api的JSONP版本(也就是個script);我騙你訪問a.com
;瀏覽器自動去載入script,也就去訪問了這個api。
其實問題並沒有回到原點,因為JSONP實際上受限很大。作為一個script標籤,一是瀏覽器只會使用GET
方法去請求它,二是請求它的時候不會攜帶cookie,三是能被改造成JSONP形式的api一定是純粹用來GET
數據的。
就算其他網站用這些JSONP改造過的介面,也不會對網站造成影響。
(所以後端開發者最好不要在GET
操作里做非冪等的事,因為別人在他的網站里嵌入script或者img標籤放你網站的url,瀏覽器就會發出一個不帶cookie的GET請求)
那更複雜的跨域需求應該怎麼辦呢?比如我就是需要在zhuanlan.zhihu.com
頁面下,發post請求到www.zhihu.com
域名下的api,而且還是要帶cookie的。這時JSONP就完全用不上了。
五、跨域資源共享(CORS)
歡迎來到沒有JSONP的世界。
我個人不喜歡用JSONP:一是因為JSONP是一種HACK,一種非標準行為,利用了script來做數據的事;二是它使得別人能直接在他的網頁上使用你的數據(雖然還是阻止不了別人用一些後端代理的手段來獲取數據,但至少能增加對方的成本)。
對於跨域的訪問控制,是有HTTP標準的。這也是網上很多講跨域的文章的主要內容,我就只簡單介紹,跨域資源共享(CORS)把跨域行為分三類:
簡單請求
如簡單的GET
和POST
。
還是以zhuanlan.zhihu.com
頁面請求www.zhihu.com
的api為例。
- 瀏覽器發出請求時,request里會帶上
Origin
頭,值為zhuanlan.zhihu.com
- 這時只需要api響應header裡帶的
Access-Control-Allow-Origin
欄位包含(匹配)了發送請求的頁面所在的域名(zhuanlan.zhihu.com
),瀏覽器就會認為合法,把數據接著使用。 - 否則,瀏覽器會攔截掉這段數據:沒錯,響應的數據已經放body里到達了客戶端,而瀏覽器會阻止掉,讓專欄頁面里負責發ajax的那段js代碼拿不到響應值。
這樣的好處很明顯:我只需要在伺服器端(通常是網關這一層)配置好Access-Control-Allow-Origin
,而我的代碼邏輯不需要對來源站點區別對待,就阻止其他人純前端的手段使用我的數據,做到HTTP訪問控制。
預檢請求
略微複雜一定的請求,如PUT
和DELETE
等,或者請求時添加了CORS安全的header之外的header(如自定義的)。
這時,正式發送跨域請求前,瀏覽器會先對目標api發出一個OPTIONS
預檢請求,這個請求里會帶三個和跨域相關的header,其值為預檢之後,正式發送api請求時將會使用的來源/方法/請求頭。這三個header是:
- Origin
- Access-Control-Request-Method
- Access-Control-Request-Headers
看名稱應該能大概知道對應什麼了。預檢請求的響應需要帶著與它們對應匹配的header和值,這樣瀏覽器才會去請求跨域api。
預檢請求的出現,是因為PUT
等複雜操作通常是非冪等的。如果像簡單請求一樣直接請求,發現響應不合理才去攔截響應值,這個時候後端的PUT
操作里該執行的事情已經被執行過了。
(至於為什麼POST
這個非冪等語義的方法會是簡單請求,我覺得應該是歷史包袱。畢竟在CORS出現前,form表單里POST
就是能跨域使用的。而早期的js很弱小,提交form之後頁面會刷新跳轉到目標地址,源地址是拿不到POST
響應的數據的)
帶cookie的請求
這種跨域請求才是最危險的,最嚴重情況下能實現上面舉的支付寶轉賬例子。
所以這種請求要求響應頭裡Access-Control-Allow-Credentials
為true,且Access-Control-Allow-Origin
不能是通配符,防止後端開發者犯錯。
關於CORS更具體的規則,可以在MDN查閱到詳細的資料。
六、不讓跨域請求?還可以直接跨網頁
按照上面的規則,支付寶把CORS設置的非常詳細和安全,在自己同公司的業務能訪問支付寶介面的同時,讓a.com
這種網站再無可乘之機,沒有辦法跨域訪問。
這時,你登上a.com
,點了a.com
網站上的一個按鈕。你的錢還是消失了。
這就是點擊劫持(clickjacking)。
實現原理可以如下:
- 假如支付寶有一個頁面,頁面上的按鈕點擊是轉賬1000元給kindJeff
- 我把這個頁面作為一個
iframe
放在a.com
的網頁上 - 我把這個
iframe
設置為透明,在它的按鈕位置下面放置一個可以看見的「下一頁」按鈕 - 你看見我的網頁,毫無防備地點擊了下一頁,實際上點擊的位置是轉賬按鈕
這種「跨域」也有類似CORS的控制方式,即X-Frame-Options
響應頭。它的值有三種:
- DENY。表示該頁面不允許在 frame 中展示,即便是在相同域名的頁面中嵌套也不允許。
- SAMEORIGIN。表示該頁面可以在相同域名頁面的 frame 中展示。
- ALLOW-FROM uri。表示該頁面可以在指定來源(uri)的 frame 中展示。
發現網頁在iframe里,且X-Frame-Options
響應頭的值不符合要求,瀏覽器不會載入這個iframe。
推薦閱讀:
※跨域特戰,尖兵如何選
※vue項目,配置proxyTable跨域訪問伺服器數據
※跨域post 及 使用token防止csrf 攻擊
※如何配置解決跨域的問題