理解跨域問題,前後端分離中,在springboot後端解決跨域問題

一:什麼是跨域問題

源於JavaScript的同源策略。即只有 協議+主機名+端口號全部相同,才允許相互訪問。如果其中有一個不同,正常情況下瀏覽器就會把收到的報文丟棄,然後報一個cors policy的錯誤。

二:出現情況(前後端分離開發vue+springboot)

我在本地用nginx服務器掛了一個端口127.0.0.1:10086 用來提供 靜態頁面;
而靜態頁面中需要用ajax請求127.0.0.1:8080的後端接口獲取數據。
  • 前端請求
 axios.post("http://127.0.0.1:8080/test/hello").then(function(response){
		console.log(response);
	},function(error){
        console.log(error);
    })
  • 後端響應
@PostMapping("/test/hello")
public  AjaxResponse testhello(){
    log.info("hello");
    return "hello";
}

  • 當前端頁面按鈕觸發ajax請求後,會有cors policy的報錯。

三:報錯原因

  1. 首先明確,這個頁面發起的ajax請求是被我的後端請求給接受到了的,進入了Controller裏面處理了邏輯業務;而且最後服務器是把Response響應給返回了!
  2. 那麼爲什麼還會報錯呢?!原因在於cors policy同域策略,通俗地講就是瀏覽器在接受到這個Response的時候才反應過來,這個Response如果瀏覽器他接收的話他就是違規的,所以瀏覽器選擇遵守規定放棄了這個Response體,並彈出cors policy錯誤警告。

四:解決跨域問題(springboot後端解決)

  1. 要解決這個問題也很簡單,只需要告訴瀏覽器“這個報文你接受吧,我同意你不遵守cors規則”就行了。這樣瀏覽器就不會扔掉數據報錯了。

  2. 如何告訴瀏覽器呢?!這時候我們需要在Response報文中加一個響應頭Header信息,也就是Access-Control-Allow-Origin:【被允許跨域訪問的源】;

在這裏插入圖片描述

  1. 在springboot有幾種處理的方法,但要知道所有方法的最後其實都是給Response加上了一個Header信息,告訴瀏覽器不要扔掉信息,都是這個原理。
  • 方法一: 用httpServletResponse封裝好的類直接給返回頭加上這個信息(此方法用於理解…)。

    @PostMapping("/hello")
        public  AjaxResponse testhello(HttpServletResponse response){
            response.setHeader("Access-Control-Allow-Origin","http://127.0.0.1:10086");
            log.info("hello");
            return AjaxResponse.success("hello");
    }
    
  • 方法二:實現WebMvcConfigurer接口,然後重寫addCorsMappings(CorsRegistry registry)方法。

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
       @Override
        public void addCorsMappings(CorsRegistry registry) {
            String mapping = "/**"; // 所有請求,也可配置成特定請求,如/api/**
            String origins = "http://localhost:10086"; // * 表示所有來源,也可以配置成特定的來源才允許跨域,如http://www.xxxx.com
            String methods = "*"; // 所有方法,GET、POST、PUT等
            Boolean allowCredentials = true; // 表示是否允許攜帶cookie  //解決Session問題
            long maxAge =30 * 1000; //表示探測請求通過後,保持認證的時間。 //這個探測請求是針對複雜請求設計的,最後面說明
            registry
                .addMapping(mapping)
                .allowedOrigins(origins)
                .allowedMethods(methods)
    //          .allowCredentials(true)
    //          .maxAge(maxAge)
            ;
        }  
    }
    

    實際上只是針對跨域問題,只需要配置好mapping origins 和methods三個參數就好了,配置好了以後再次用前端向後端發起跨域請求,就會發現不再報錯了。

    這個方法二有個弊端,就是如果你還配置了攔截器,那麼就會產生衝突。最後面記錄我的填坑過程。

  • 方法三:

    實現Filter接口重寫doFilter方法,過濾器中添加頭部

@WebFilter(filterName = "MyFilter",urlPatterns = "/*")
public class CorsFilterConfig implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
        //指定允許其他域名訪問
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
		
		//前端攜帶cookie時,也就是寫了 {withCredentials: true},後端不能用通配符,
        //得用"Access-Control-Allow-Origin",httpServletRequest.getHeader("origin")
        
            //響應頭設置
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");

        //允許攜帶cookie
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");

        //響應類型
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");

        //option驗證後時間
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

五:解決Session維護問題:

5.1用方法二解決:

  1. 假設,當我們用方法二重寫addCorsMappings(CorsRegistry registry)後,能夠跨域請求了。

    會出現一個問題。那就是ajax的請求是不會自動攜帶cookie的,意味着後端無法憑藉前端cookie記錄的JSessionId來獲取後端存儲的Session

    通俗講就是每一次ajax請求後端都會視作爲不同的用戶,每次都會給這個ajax請求的Response消息頭裏設置一個新的“Set-Cookie:JSESSIONID=44AA8F3EFF388BD7ADE0551BC33FADB”。

  2. 如何解決?很簡單,讓前端頁面發起ajax請求的時候讓它帶上所有cookie不就好了!同時我們返回的Response裏面需要加上一個頭信息"Access-Control-Allow-Credentials:true"。

在這裏插入圖片描述

  • 具體步驟如下

  • 第一步讓前端的ajax部分加上withCredentials: true這個參數,保證前端會帶上cookie。

    比如我這裏用的axios就這樣寫

   axios.post("http://127.0.0.1:8080/test/hello", {withCredentials: true}).then(function(response){
   		console.log(response);
   	},function(error){
           console.log(error);
       })

或者用Jquery封裝的ajax這樣加

   $.ajax({
               url: "http://localhost:8080/orders",
               type: "GET",
               xhrFields: {
                   withCredentials: true
               },
               success: function (data) {
                   render(data);
               }
    })
  • 第二步,在後端addCorsMappings(CorsRegistry registry)方法裏添加 allowCredentials(true)。

    (注意,前端授權了cookie發送,也就是配置了withCredentials: true,那麼後端allowedOrigins(origins)這個參數origins就不能寫 * 匹配所有,而且allowCredentials(allowCredentials)必須是true。)

    我理解意思就是前端把cookie全帶過來了,後端得指定要求需要的一個域裏面的cookie,畢竟不能全要。

     @Configuration
     public class WebMvcConfig implements WebMvcConfigurer {
        @Override
         public void addCorsMappings(CorsRegistry registry) {
             String mapping = "/**"; 
             String origins = "http://localhost:10086"; //寫成String origins = "*"; 錯誤,這時候需要指定明確的域
             String methods = "*"; 
             Boolean allowCredentials = true; // 表示是否允許攜帶cookie  //解決Session問題
             registry
                 .addMapping(mapping)
                 .allowedOrigins(origins)
                 .allowedMethods(methods)
                 .allowCredentials(allowCredentials)
             ;
         }  
     }

這樣你會發現session就被同步了,每次Ajax的請求的Response都不會被重新賦值新的JSessionId了。用Session記錄登陸信息也可以實現了。

5.2用方法三解決:

同方法二介紹的一樣

  1. 用Filter進行實現跨域並維護session,前端同樣要在發送請求的時候添加{withCredentials: true}參數,
  2. 後端的filter中也要在response中添加頭部信息 httpServletResponse.setHeader(“Access-Control-Allow-Credentials”, “true”)。

六:所謂的addCorsMappings與攔截器衝突問題

  • 如果用第二種方法解決跨域和session維護問題,而且你後端還要配值攔截器,那麼就有可能出現問題,就是很多網友說的衝突問題。
  • 如果在攔截器中拋出異常或者攔截器return了false,這個時候你會發現第二次的請求根本不允許發送。
  • 原因:重寫addCorsMappings()來給Response添加Access-Control-Allow-Origin頭部信息的操作也是在攔截器的某一步操作裏做的,這就導致Option請求在被攔截後由於不被放行,所以連同導致後面給添加Access-Control-Allow-Origin等頭部信息的操作也沒能執行。所以瀏覽器還是會遵守規則不接受你的報文。
  • 不被放行的原因可能有是人爲或非人爲地在攔截器拋出異常;或者攔截器那裏你的邏輯返回了false導致不放行(比如說下面介紹的,你的請求用的是複雜請求,而又沒對複雜請求中的option方法設計好邏輯,導致不被放行)。
  • 解決方法:換方法三,重寫filter方法。filter優先於攔截器,這樣添加跨域的頭部和攔截器就不會攪和在一起了。就算你攔截器邏輯中不被放行,但我回復的報文已經提前加上相關頭部信息了。(這種情況仍然解決不了option請求的問題,仍需要額外處理)

七:攔截器中對option請求的處理。

7.1 首先要了解option請求。

  • options請求又叫做預檢請求(我選用json格式導致請求變成了需要先發送預檢請求的複雜請求),在真正的請求發送出去之前,瀏覽器會先發送一個options請求向服務詢問此接口是否允許我訪問。也就是說,你的數據請求實際上瀏覽器發送了兩個請求。第一次是preflight,也就是options請求,用於請求驗證, 第二次纔是我真正需要發送的Post請求。只有第一個OPTION正常返回了,第二次請求才會正常發送。

  • 預檢請求的頭信息參數含義。

    在還沒有配置攔截器的情況下用方法二或者方法三解決跨域問題和session維護問題後,預檢請求有下列正常參數。

  • 預檢請求發出Request頭部信息中有兩個參數:

    Access-Control-Request-Headers: content-type  //告知服務器,實際請求攜帶自定義請求首部字段Content-Type
    Access-Control-Request-Method: POST  //告知服務器,實際請求將使用 POST 方法
    
  • 預檢請求返回的Response頭信息中的重要參數:

    Access-Control-Allow-Origin: http://localhost:10086  //表明服務器允許跨域,允許的域是http://localhost:10086
    Access-Control-Allow-Methods: POST, GET, OPTIONS  // 表明服務器允許客戶端使用 POST,GET 和 OPTIONS 方法發起請求
    Access-Control-Allow-Headers: Content-Type  // 表明服務器允許請求中攜帶字段Content-Type
    Access-Control-Max-Age: 1800  //這個預檢請求認證通過的有效期爲1800秒,在有效時間內,瀏覽器無須爲同一請求再次發起預檢請求,請注意,瀏覽器自身維護了一個最大有效時間
    
  • 在沒有配置攔截器的情況下,一切都正常有序。

7.2 你的攔截器或filter的邏輯極大概率不會放行OPTION請求,導致異常。

  • 配置了攔截器就意味着必然會攔截到第一次的option請求。因爲option請求是不會攜帶參數信息,例如cookie,token那些個,數據處理可能就出現問題。所以這個時候你的邏輯沒有對option進行特殊處理的話,就有極大可能不放行此次option請求。
  • 這裏無論是用addCorsMappings(CorsRegistry registry) 還是用filter來處理上述的session維護問題和跨域問題,都可能會因爲option請求導致異常。但是異常出現的原因是不一致的。
  1. 用addCorsMappings(CorsRegistry registry)解決跨域和session問題,然後配置了攔截器阻攔了option請求。這個時候,是因爲OPTION請求沒加上跨域頭部信息,所以你會發現第二次的請求根本不允許發送。這次是瀏覽器根本不允許發出去,不同於最開始討論的等到服務器返回了瀏覽器才扔掉。
    檢查option請求返回的Response體,發現添加的允許跨域的幾個頭部信息沒有被添加上。
    在這裏插入圖片描述
  2. 用filter處理session和跨域問了後,攔截OPTION請求,同樣第二次的正真請求同樣也不被允許發送出去。原因是OPTION沒有正確的狀態碼,本來也是這樣,OPTION不被放行,而第二次請求必然不會發送(不同的是,這裏跨域問題是解決的了)。
    前端報錯如下,沒有正確的status。在這裏插入圖片描述
  • 通用的解決方法:無論怎麼樣攔截器或者filter都放行option請求。
    比如我配置的攔截器:
public class Login_Interceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
      if(request.getMethod().equals("OPTIONS")){
          log.info("OPTIONS直接放行");
          return true;
      }
      HttpSession session = request.getSession();
      Object userInfo = session.getAttribute("userInfo");
      if (userInfo==null) {
          log.info("被攔截++,:session.getId()ID爲:"+session.getId());
          return false;
      }else {
          log.info("被放行++,:session.getId()ID爲:"+session.getId());
          return true;
      }
  }
}

七:總結:

  • springboot後端處理跨域問題有兩個主要方法:
  1. 重寫addCorsMappings(CorsRegistry registry)
  2. 實現Filter接口重寫doFilter方法
  • 如果還需要用攔截器來實現一些業務邏輯,比如我在攔截器裏面用session判斷是否登陸,而且還要手動拋出異常的話。那麼最好用實現Filter接口重寫doFilter()的方法
  • 如果你的請求中包含還複雜請求,那麼最好還要針對option請求進行放行操作。原因已經在上面記錄了。
  • 全劇終,希望對你我有幫助。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章