spring boot專案實戰:跨域問題解決
背景
前後端分離架構,前端anglerjs,後端spring boot,使用shiro作為許可權控制,已配置通用跨域請求支援。
前端呼叫介面時部分情況正常,部分情況出現跨域請求不支援情況,錯誤資訊如下:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'xxxx' is therefore not allowed access.
配置錯了?
首先,想到的就是對跨域請求支援的配置是錯誤的,嘗試著替換不同的跨域支援配置,有以下幾種:
1、繼承WebMvcConfigurerAdapter
@Configuration
public class AppConfig extends WebMvcConfigurerAdapter {
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowCredentials(true).allowedHeaders("Origin, X-Requested-With, Content-Type, Accept") .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS") .maxAge(3600); } }
2、配置WebMvcConfigurer
@Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedOrigins("*") .allowedMethods("*").allowedHeaders("*") .allowCredentials(true) .exposedHeaders(HttpHeaders.SET_COOKIE).maxAge(3600L); } }; }
…
If you are using Spring Security, make sure to enable CORS at Spring Security level as well to allow it to leverage the configuration defined at Spring MVC level.
大概意思就是使用Spring Security要進行特殊的配置來支援CORS。而當前專案內使用的是shiro,是不是許可權控制導致的問題?檢查shiro相關程式碼,果然找到了問題,在loginFilter內會判斷如果未登入,就通過response寫回未登入提示,程式碼如下:
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
HttpServletResponse resp = (HttpServletResponse) response;
String contentType = "application/json";
resp.setContentType(contentType);
resp.setCharacterEncoding("UTF-8");
Map<String, String> map = Maps.newLinkedHashMap();
map.put("code", "xxx");
map.put("msg", "xxx");
String result = JacksonHelper.toJson(map);
PrintWriter out = resp.getWriter();
try{
out.print(result);
out.flush();
} finally {
out.close();
}
return;
}
那就新增上跨域支援
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
本來以為ok了,但是前端是不報錯了,但並不能獲得對應介面期望的結果,而是一直收到
{"code":"xxx","msg":"xxx"}
顯然是被登入攔截了,但是明明已經登入,而且有的介面可以正常通過登入攔截,為什麼部分介面會出現不能登入的情況呢?
明明登入了,為什麼被loginFilter攔截?
遇到了問題就要想辦法解決,首先就是懷疑客戶端sessionId未被正常儲存,在loginFilter內新增日誌列印sessionID,發現每次的sessionID都不一樣,問題清晰了一些,前端並未正確的保持登入狀態,對比前端兩個呼叫介面的程式碼,發現正常的是get請求,post請求不正常,通過在網上搜索,發現ajax post跨域請求時,預設是不攜帶瀏覽器的cookie的,也就是每次請求都會生成一個新的session,因此post請求都被登入攔截。解決辦法如下:
$.ajax({
type:"POST",
url:"",
data:{},
crossDomain:true,
xhrFields: { withCredentials: true },
success:function(data){
},
error:function(data){
}
})
配置crossDomain:true 和 xhrFields: { withCredentials: true }就可以讓請求正常攜帶cookie。
一個完整可用方案
1、配置支援跨域請求(多種方式自由選擇,推薦使用下面的方式)
@Configuration
public class WebConfig {
/**
* 跨域請求支援
*/
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*")
.allowedMethods("*").allowedHeaders("*")
.allowCredentials(true)
.exposedHeaders(HttpHeaders.SET_COOKIE).maxAge(3600L);
}
};
}
}
2、前端ajax post請求時新增xhrFields: { withCredentials: true }
$.ajax({
type:"POST",
url:"",
data:{},
crossDomain:true,
xhrFields: { withCredentials: true },
success:function(data){
},
error:function(data){
}
})
3、檢查許可權控制程式碼,看是否有特殊處理的地方,未新增跨域支援。如上文所提,登入攔截直接通過response寫回未登入提示;
使用spring-security框架時也要新增特殊配置,如下:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()...
}
}
解決跨域的本質就是在返回頭內新增Access-Control-Allow-Origin,實現方式有多種,spring體系內解決跨域可參考CORS support in Spring Framework,很全面的介紹了各種場景。使用許可權框架時,要注意許可權框架本身的CORS支援。