老被跨域問題煩?看看都有哪些處理方法
前面寫的《IT技術人員的自我修養》,沒想到幾天內收到了不少良好的反饋,在此感謝大家的關注。往後會不定時分享一些技術、管理領域的工作經驗總結與感悟,歡迎大家持續關注、交流。最近被問及一個跨域的問題,包括之前面試時發現很多面試者對跨域及其處理也是一知半解,故本文對該問題進行了梳理總結,以供參考。
1. 什麼是跨域
理解什麼是跨域,就要先了解一個叫“同源策略”的東西,什麼是“同源策略”?這是瀏覽器為了網站訪問安全,對來自不同源的請求做一些必要的訪問限制的一種策略。那什麼叫“同源”呢?我們知道,一個http請求地址一般包含四部分:協議://域名:埠/路徑
http://blog.jboost.cn/docker-1.html
, 來看以下地址是否與它同源
地址 | 是否同源 | 說明 |
---|---|---|
https://blog.jboost.cn/docker-1.html | 不同源 | 協議不同,一個http,一個https |
http://www.jboost.cn/docker-1.html | 不同源 | 域名不同 |
http://blog.jboost.cn:8080/docker-1.html | 不同源 | 埠不同,一個是預設埠80,一個是8080 |
http://blog.jboost.cn/docker-2.html | 同源 | 雖然路徑不同,但協議、域名、埠(預設80)都相同 |
那麼瀏覽器對不同源的請求做了哪些訪問限制呢?共有三種限制
-
對Cookie、LocalStorage,以及IndexDB(瀏覽器提供的類NoSQL的一個本地資料庫)的訪問
-
對DOM的訪問
-
AJAX請求
而跨域就是要打破這種訪問限制,對不同源的資源請求也能順利進行,最常見的就是AJAX請求,比如前後端分離架構中,兩者服務域名不同,前端通過AJAX直接訪問服務端介面,就會存在跨域問題。
2. 為什麼會存在跨域
前面說“同源策略”時已經提到,瀏覽器是為了網站的訪問安全,才設定了跨域這道屏障。那麼前面所說的三種限制,分別都是如何來保障網站安全的。
-
對本地儲存Cookie、LocalStorage、IndexDB的訪問限制
我們系統的登入憑證一般是通過在Cookie中設定 SESSIONID(如針對瀏覽器表單請求)或直接返回 token(如針對REST請求)的形式返回給客戶端的,比如Tomcat是通過在Cookie中設定名為 JSESSIONID 的屬性來儲存的,而一般REST請求的token前端會儲存於 LocalStorage 中,如果不存在訪問限制,則你訪問的其它網站可能就會獲取到這些憑證,然後偽造你的身份來發起非法請求,這就太不安全了。 -
對DOM的訪問限制
如果不對DOM進行訪問限制,那麼其它網站,尤其一些釣魚網站,就可以通過<iframe>
的形式拿到你訪問網站的DOM,進而獲取到你輸入的一些敏感資訊,比如使用者名稱、密碼… -
對AJAX請求的限制
同源策略規定,AJAX請求只能發給同源的網址,否則就會報錯。至於為什麼要限制,一方面是避免1中所提到偽造非法請求,另一方面我理解是AJAX過於靈活,如果不做限制,可能網站的介面資源就會被其它網站隨意使用,就像你的私有物品被別人招呼都不打任意拿去用一樣。
總之,同源策略是瀏覽器提供的最基本的一種安全保障機制或約定。
3. 怎麼實現跨域訪問
我們平常遇到的跨域問題基本都出現在AJAX請求的場景,一般而言,可以通過代理、CORS、JSONP等方式來解決跨域問題。
3.1 代理
既然“同源策略”是瀏覽器端的機制,那我們就可以繞開瀏覽器,最常見的做法就是使用代理,如 Nginx,比如我們前端專案的域名是 http://blog.jboost.cn,服務端介面域名是 http://api.jboost.cn,我們在 Nginx 中提供如下配置
server{ # 埠 listen 80; # 域名 server_name blog.jboost.cn; # 所有 http://blog.jboost.cn/api/xxx 請求都會被轉發到 http://api.jboost.cn/api/xxx location ^~ /api { proxy_pass http://api.jboost.cn; } }
則前端通過AJAX請求服務端介面 http://api.jboost.cn/api/xxx 都可以改為通過 http://blog.jboost.cn/api/xxx 來訪問,從而避免不同源的跨域問題。
3.2 CORS
CORS是Cross-Origin Resource Sharing的簡寫,即跨域資源共享,CORS需要服務端與瀏覽器同時支援,目前所有瀏覽器(除IE10以下)都支援CORS,因此,實現CORS,主要就是服務端的工作了。例如在Spring Boot中,我們可通過如下配置註冊一個CorsFilter的過濾器來實現跨域支援。
1 @Configuration 2 @ConditionalOnClass({Servlet.class, CorsFilter.class}) 3 public class CORSAutoConfiguration { 4 5 @Bean 6 @ConditionalOnMissingBean(name = "corsFilterRegistrationBean") 7 public FilterRegistrationBean corsFilterRegistrationBean() { 8 UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource(); 9 10 CorsConfiguration corsConfiguration = new CorsConfiguration(); 11 corsConfiguration.applyPermitDefaultValues(); 12 corsConfiguration.setAllowedMethods(Arrays.asList(CorsConfiguration.ALL)); 13 corsConfiguration.addExposedHeader(HttpHeaders.DATE); 14 15 corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); 16 17 CorsFilter corsFilter = new CorsFilter(corsConfigurationSource); 18 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); 19 filterRegistrationBean.setFilter(corsFilter); 20 filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); 21 filterRegistrationBean.addUrlPatterns("/*"); 22 23 return filterRegistrationBean; 24 } 25 }
其實質就是在響應訊息的Header中新增幾個屬性,主要有
-
Access-Control-Allow-Origin 必需,表示允許跨域的請求源,可以是具體的域名,也可以是 * ,表示任意域名
-
Access-Control-Allow-Methods 必需,表示允許跨域訪問的HTTP方法,如GET、POST、PUT、DELETE等,可以是 * ,表示所有
-
Access-Control-Allow-Headers 如果請求包括 Access-Control-Request-Headers 頭資訊,則必需,表示伺服器支援的所有頭資訊欄位
3.3 JSONP
JSONP是利用瀏覽器對HTML一些標籤(如 <script>
, <img>
等)的 src 屬性不具有同源策略限制的特性實現的,如前端新增
<script type="text/javascript" src="http://api.jboost.cn/hello?name=jboost&callback=jsonpCallback"/>
並且定義JS方法 jsonpCallback
。服務端介面返回內容需要是JS方法jsonpCallback
的呼叫格式,如jsonpCallback({"name":"jboost"})
,這樣在jsonpCallback
方法中就可以獲取服務端實際返回的結果資料{"name":"jboost"}
了。
JSONP方式的侷限性也很明顯,一是隻支援GET請求——你沒見過哪些<script>
, <img>
標籤是POST請求吧,二是需要對服務端返回資料格式做處理。
4. 總結
三種跨域支援的實現,代理方式最簡單,對客戶端、服務端都不具有侵入性,但如果需要支援的請求源比較多,或者是與第三方對接的話,代理方式就不太適用了。CORS相對來說是一種標準的處理方式,並且通過過濾器的方式對業務程式碼也沒有任何侵入性。而JSONP方式侷限性較大,只支援GET,並且需要服務端做返回資料格式的支援。可針對具體情況選擇適用的方式。