關(guān)于后端如何解決跨域的問題說明
對于跨域,相信同學們都有所了解。前端的跨域的若干種方式,大家也都知道,什么 JSONP,iframe+domain 等等。但是我們今天的主題,不是前端跨域,而是后端跨域。
一旦提及到跨域,就會想到同源策略,那我們就先來回顧跨域和同源策略。
什么是跨域請求
首先,我們要了解什么是跨域請求。簡單來說,當一臺服務器資源從另一臺服務器(不同 的域名或者端口)請求一個資源或者接口,就會發(fā)起一個跨域 HTTP 請求。
舉個簡單的例子:
從http://aaa.com/index.html,發(fā)送一個 Ajax 請求,請求地址是 http://bbb.com/下面的一個接口,這就是發(fā)起了一個跨域請求。在不做任何處理的情況下,這個跨域請求是無法被成功請求的,因為瀏覽器基于同源策略 會對跨域請求做一定的限制。那什么又是同源策略呢?
什么是同源策略
首先大家要知道同源策略發(fā)生的場景——瀏覽器中,什么意思呢?如果不是瀏覽器的話, 就不會受到同源策略的影響。也就是說,兩個服務器直接進行跨域請求是可以進行數(shù)據(jù)請求的。這也就為我們接下來的后端跨域埋下一下小伏筆。 同源策略的目的是什么呢?同源策略限制了從同一個源加載的文檔或者腳本如何與來自另 一個源的資源進行交互。這是一個用于隔離潛在惡意文件的重要安全機制。
同源策略限制內(nèi)容有
- Cookie、LocalStorage、IndexedDB 等存儲性內(nèi)容
- DOM 節(jié)點
- AJAX 請求不能發(fā)送
那什么又是同源?
大家都知道,一個域名請求地址的組成是:協(xié)議+域名+端口號+請求資源地址 , 當協(xié)議、域名、端口號中任意一個不相同時 , 都算作不同源(必須是域名完全相同,比如說 a.example.com 和 b.example.com 這兩個域名。
雖然它們的頂級域名和二級域名(均為 example.com)都相同,但是三級域名(a 和 b)不相同,所以也不能算作域名相同)。
如果不同時滿足這上面三個條件,那就不符合瀏覽器的同源策略。 需要注意的是,不是所有的交互都會被同源策略攔截下來,下面兩種交互就不會觸發(fā)同源策略:
跨域?qū)懖僮鳎–ross-origin writes)
- 例如超鏈接、重定向以及表單的提交操 作,特定少數(shù)的 HTTP 請求需要添加預檢請求(preflight)
跨域資源嵌入(Cross-origin embedding)
- <script> 標簽嵌入的跨域腳本; o <link> 標簽嵌入的 CSS 文件; o <img> 標簽嵌入圖片;
- <video> 和 <audio> 標簽嵌入多媒體資源; o <object>, <embed>, <applet> 的插件;
- @font-face 引入的字體,一些瀏覽器允許跨域字體(cross-origin fonts),一些需要同源字體(same-origin fonts);
- <frame> 和 <iframe> 載入的任何資源,站點可以使用 X-FrameOptions 消息頭來組織這種形式的跨域交互。
這里你或許有個疑問:請求跨域了,那么請求到底發(fā)出去沒有?
跨域并不是請求發(fā)不出去,請求能發(fā)出去,服務端能收到請求并正常返回結(jié)果,只是結(jié)果被瀏覽器攔截了。
你可能會疑問明明通過表單的方式可以發(fā)起跨域請求,為什么 Ajax 就不會?因為歸根結(jié)底,跨域是為了阻止用戶讀取到另一個域名下的內(nèi)容,Ajax 可以獲取響應,瀏覽器認為這不安全,所以攔截了響應。但是表單并不會獲取新的內(nèi)容,所以可以發(fā)起跨域請求。
同時也說明了跨域并不能完全阻止 CSRF,因為請求畢竟是發(fā)出去了。
跨域解決方案
跨域就是通過某些手段來繞過同源策略限制,實現(xiàn)不同服務器之間通信的效果。方法有很多 ,大致分為兩類:
- 服務端進行設置默認允許某些域名跨域訪問
- 從客戶端入手想辦法繞開同源安全策略
常見的解決方案有:
1.jsonp
利用 <script> 標簽沒有跨域限制的漏洞,網(wǎng)頁可以得到從其他來源動態(tài)產(chǎn)生的 JSON 數(shù)據(jù)。JSONP請求一定需要對方的服務器做支持才可以。
JSONP和AJAX相同,都是客戶端向服務器端發(fā)送請求,從服務器端獲取數(shù)據(jù)的方式。但AJAX屬于同源策略,JSONP屬于非同源策略(跨域請求)
JSONP優(yōu)點是簡單兼容性好,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題。缺點是僅支持get方法具有局限性,不安全可能會遭受XSS攻擊。
2.cors
CORS(Cross-origin resource sharing),跨域資源共享。CORS 其實是瀏覽器制定的一個規(guī)范,瀏覽器會自動進行 CORS 通信,它的實現(xiàn)則主要在服務端,它通過一些 HTTP Header 來限制可以訪問的域,例如頁面 A 需要訪問 B 服務器上的數(shù)據(jù),如果 B 服務器 上聲明了允許 A 的域名訪問,那么從 A 到 B 的跨域請求就可以完成。對于那些會對服務器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請求,瀏覽器會使用 OPTIONS 方法發(fā)起 一個預檢請求(preflight request),從而可以獲知服務器端是否允許該跨域請求,服 務器端確認允許后,才會發(fā)起實際的請求。在預檢請求的返回中,服務器端也可以告知客 戶端是否需要身份認證信息。我們只需要設置響應頭,即可進行跨域請求。
雖然設置 CORS 和前端沒什么關(guān)系,但是通過這種方式解決跨域問題的話,會在發(fā)送請求時出現(xiàn)兩種情況,分別為簡單請求和復雜請求。
簡單請求:
只要同時滿足以下兩大條件,就屬于簡單請求:
1)使用GET、HEAD、POST方法之一;
2)Content-Type 的值僅限于:text/plain、multipart/form-data、application/x-www-form-urlencoded,請求中的任意 XMLHttpRequestUpload 對象均沒有注冊任何事件監(jiān)聽器; XMLHttpRequestUpload 對象可以使用 XMLHttpRequest.upload 屬性訪問;
復雜請求:
不符合以上條件的請求就肯定是復雜請求了。 復雜請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求,該請求是 option 方法的,通過該請求來知道服務端是否允許跨域請求。
我們用PUT向后臺請求時,屬于復雜請求,后臺需被請求的Servlet中添加Header設置,Access-Control-Allow-Origin這個Header在W3C標準里用來檢查該跨域請求是否可以被通過,如果值為*則表明當前頁面可以跨域訪問。默認的情況下是不允許的。
一般我們可以寫一個過濾器:
@WebFilter(filterName = "corsFilter", urlPatterns = "/*", initParams = {@WebInitParam(name = "allowOrigin", value = "*"), @WebInitParam(name = "allowMethods", value = "GET,POST,PUT,DELETE,OPTIONS"), @WebInitParam(name = "allowCredentials", value = "true"), @WebInitParam(name = "allowHeaders", value = "Content-Type,X-Token")}) public class CorsFilter implements Filter { private String allowOrigin; private String allowMethods; private String allowCredentials; private String allowHeaders; private String exposeHeaders; @Override public void init(FilterConfig filterConfig) throws ServletException { allowOrigin = filterConfig.getInitParameter("allowOrigin"); allowMethods = filterConfig.getInitParameter("allowMethods"); allowCredentials = filterConfig.getInitParameter("allowCredentials"); allowHeaders = filterConfig.getInitParameter("allowHeaders"); exposeHeaders = filterConfig.getInitParameter("exposeHeaders"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (!StringUtils.isEmpty(allowOrigin)) { if(allowOrigin.equals("*")){ // 設置哪個源可以訪問 response.setHeader("Access-Control-Allow-Origin", allowOrigin); }else{ List<String> allowOriginList = Arrays.asList(allowOrigin.split(",")); if (allowOriginList != null && allowOriginList.size() > 0) { String currentOrigin = request.getHeader("Origin"); if (allowOriginList.contains(currentOrigin)) { response.setHeader("Access-Control-Allow-Origin", currentOrigin); } } } } if (!StringUtils.isEmpty(allowMethods)) { //設置哪個方法可以訪問 response.setHeader("Access-Control-Allow-Methods", allowMethods); } if (!StringUtils.isEmpty(allowCredentials)) { // 允許攜帶cookie response.setHeader("Access-Control-Allow-Credentials", allowCredentials); } if (!StringUtils.isEmpty(allowHeaders)) { // 允許攜帶哪個頭 response.setHeader("Access-Control-Allow-Headers", allowHeaders); } if (!StringUtils.isEmpty(exposeHeaders)) { // 允許攜帶哪個頭 response.setHeader("Access-Control-Expose-Headers", exposeHeaders); } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }
大功告成,現(xiàn)在前端就可以跨域獲取后臺的數(shù)據(jù)了,正如我們上面所說的,后端是實現(xiàn) CORS 通信的關(guān)鍵。
如果你的SpringBoot版本在2.0以上,以下代碼配置即可完美解決你的前后端跨域請求問題:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); final CorsConfiguration corsConfiguration = new CorsConfiguration(); /*是否允許請求帶有驗證信息*/ corsConfiguration.setAllowCredentials(true); /*允許訪問的客戶端域名*/ corsConfiguration.addAllowedOrigin("*"); /*允許服務端訪問的客戶端請求頭*/ corsConfiguration.addAllowedHeader("*"); /*允許訪問的方法名,GET POST等*/ corsConfiguration.addAllowedMethod("*"); urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); return new CorsFilter(urlBasedCorsConfigurationSource); } }
或者使用WebMvcConfigurationSupport,實現(xiàn)方式有很多,感興趣的可以自行研究。
3.@CrossOrigin注解
這個方法僅對Java有用。springboot中,在Controller類上添加一個 @CrossOrigin(origins ="*") 注解就可以實現(xiàn)對當前controller 的跨域訪問了,當然這個標簽也可以加到方法上,或者直接加到入口類上對所有接口進行跨域處理,注意這個注解只在JDK1.8版本以上才起作用。
4.使用SpringCloud網(wǎng)關(guān)
服務網(wǎng)關(guān)(zuul)又稱路由中心,用來統(tǒng)一訪問所有api接口,維護服務。
Spring Cloud Zuul通過與Spring Cloud Eureka的整合,實現(xiàn)了對服務實例的自動化維護,所以在使用服務路由配置的時候,我們不需要向傳統(tǒng)路由配置方式那樣去指定具體的服務實例地址,只需要通過Ant模式配置文件參數(shù)即可
5.Node中間件代理(兩次跨域)
實現(xiàn)原理:同源策略是瀏覽器需要遵循的標準,而如果是服務器向服務器請求就無需遵循同源策略。這樣的話,我們可以讓服務器替我們發(fā)送一個請求,請求其他服務器下面的數(shù)據(jù)。然后我們的頁面訪問當前服務器下的接口就沒有跨域問題了。
代理服務器,需要做以下幾個步驟:
- 接受客戶端請求 。
- 將請求 轉(zhuǎn)發(fā)給服務器。
- 拿到服務器 響應 數(shù)據(jù)。
- 將 響應 轉(zhuǎn)發(fā)給客戶端。
6.nginx反向代理
實現(xiàn)原理類似于Node中間件代理,需要你搭建一個中轉(zhuǎn)nginx服務器,用于轉(zhuǎn)發(fā)請求。
使用nginx反向代理實現(xiàn)跨域,是最簡單的跨域方式。只需要修改nginx的配置即可解決跨域問題,支持所有瀏覽器,支持session,不需要修改任何代碼,并且不會影響服務器性能。
實現(xiàn)思路:通過nginx配置一個代理服務器做跳板機,反向代理訪問domain2接口,并且可以順便修改cookie中domain信息,方便當前域cookie寫入,實現(xiàn)跨域登錄。
將nginx目錄下的nginx.conf修改如下:
// proxy服務器 server { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 index index.html index.htm; # 當用webpack-dev-server等中間件代理接口訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啟用 add_header Access-Control-Allow-Origin http://www.domain1.com; #當前端只跨域不帶cookie時,可為* add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; add_header Access-Control-Allow-Headers *; } }
這樣我們的前端代理只要訪問 http:www.domain1.com:81/*就可以了。
總結(jié)
- CORS支持所有類型的HTTP請求,是跨域HTTP請求的根本解決方案
- JSONP只支持GET請求,JSONP的優(yōu)勢在于支持老式瀏覽器,以及可以向不支持CORS的網(wǎng)站請求數(shù)據(jù)。
- 不管是Node中間件代理還是nginx反向代理,主要是通過同源策略對服務器不加限制。
- 日常工作中,用得比較多的跨域方案是cors和nginx反向代理
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解SpringBoot2.0的@Cacheable(Redis)緩存失效時間解決方案
這篇文章主要介紹了詳解SpringBoot2.0的@Cacheable(Redis)緩存失效時間解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04如何解決java.lang.ClassNotFoundException: com.mysql.jdbc.Dr
這篇文章主要介紹了如何解決java.lang.ClassNotFoundException: com.mysql.jdbc.Driver問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12Java詳細分析String類與StringBuffer和StringBuilder的使用方法
當對字符串進行修改的時候,需要使用 StringBuffer 和 StringBuilder類,和String類不同的是,StringBuffer和 StringBuilder類的對象能夠被多次的修改,并且不產(chǎn)生新的未使用對象2022-04-04Java對數(shù)組實現(xiàn)選擇排序算法的實例詳解
這篇文章主要介紹了Java對數(shù)組實現(xiàn)選擇排序算法的實例,選擇排序的比較次數(shù)為 O(N^2)而交換數(shù)為O(N),需要的朋友可以參考下2016-04-04springboot+kafka中@KafkaListener動態(tài)指定多個topic問題
這篇文章主要介紹了springboot+kafka中@KafkaListener動態(tài)指定多個topic問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12