一:什麼是跨域問題
源於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的報錯。
三:報錯原因
- 首先明確,這個頁面發起的ajax請求是被我的後端請求給接受到了的,進入了Controller裏面處理了邏輯業務;而且最後服務器是把Response響應給返回了!
- 那麼爲什麼還會報錯呢?!原因在於cors policy同域策略,通俗地講就是瀏覽器在接受到這個Response的時候才反應過來,這個Response如果瀏覽器他接收的話他就是違規的,所以瀏覽器選擇遵守規定放棄了這個Response體,並彈出cors policy錯誤警告。
四:解決跨域問題(springboot後端解決)
-
要解決這個問題也很簡單,只需要告訴瀏覽器“這個報文你接受吧,我同意你不遵守cors規則”就行了。這樣瀏覽器就不會扔掉數據報錯了。
-
如何告訴瀏覽器呢?!這時候我們需要在Response報文中加一個響應頭Header信息,也就是Access-Control-Allow-Origin:【被允許跨域訪問的源】;
- 在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用方法二解決:
-
假設,當我們用方法二重寫addCorsMappings(CorsRegistry registry)後,能夠跨域請求了。
會出現一個問題。那就是ajax的請求是不會自動攜帶cookie的,意味着後端無法憑藉前端cookie記錄的JSessionId來獲取後端存儲的Session。
通俗講就是每一次ajax請求後端都會視作爲不同的用戶,每次都會給這個ajax請求的Response消息頭裏設置一個新的“Set-Cookie:JSESSIONID=44AA8F3EFF388BD7ADE0551BC33FADB”。
-
如何解決?很簡單,讓前端頁面發起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用方法三解決:
同方法二介紹的一樣
- 用Filter進行實現跨域並維護session,前端同樣要在發送請求的時候添加{withCredentials: true}參數,
- 後端的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請求導致異常。但是異常出現的原因是不一致的。
- 用addCorsMappings(CorsRegistry registry)解決跨域和session問題,然後配置了攔截器阻攔了option請求。這個時候,是因爲OPTION請求沒加上跨域頭部信息,所以你會發現第二次的請求根本不允許發送。這次是瀏覽器根本不允許發出去,不同於最開始討論的等到服務器返回了瀏覽器才扔掉。
檢查option請求返回的Response體,發現添加的允許跨域的幾個頭部信息沒有被添加上。
- 用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後端處理跨域問題有兩個主要方法:
- 重寫addCorsMappings(CorsRegistry registry)
- 實現Filter接口重寫doFilter方法
- 如果還需要用攔截器來實現一些業務邏輯,比如我在攔截器裏面用session判斷是否登陸,而且還要手動拋出異常的話。那麼最好用實現Filter接口重寫doFilter()的方法。
- 如果你的請求中包含還複雜請求,那麼最好還要針對option請求進行放行操作。原因已經在上面記錄了。
- 全劇終,希望對你我有幫助。