為什麼給你設置重重障礙?講一講Web開發中的跨域

為什麼給你設置重重障礙?講一講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.comwww.zhihu.com的例子,我只是想要GET一些數據而已。

當遇到這種跨域問題不知怎麼解決的時候,查詢一下,會發現有兩種解決辦法:

  • 如果是子域名下的頁面想訪問父域的api,如zhuanlan.zhihu.com想訪問zhihu.com的api,那可以在發請求前設置一下document.domain的值為zhihu.com。畢竟是子域,瀏覽器幾乎沒有做什麼限制。(如我在一個油猴腳本里就這樣用過greasyfork.org/zh-CN/sc
  • 在非父子域關係的情況下,如zhuanlan.zhihu.comwww.zhihu.com(或者a.comb.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)把跨域行為分三類:

簡單請求

如簡單的GETPOST

還是以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訪問控制。

預檢請求

略微複雜一定的請求,如PUTDELETE等,或者請求時添加了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 攻擊
如何配置解決跨域的問題

TAG:Ajax | 跨域 | 後端技術 |