標籤:doc iat code frame 允許 dap therefore dev before
問題描述
前後端分離的項目,前端使用Vue,後端使用Spring MVC。
顯然,需要解決瀏覽器跨域訪問資料限制的問題,在此使用CROS協議解決。
由於該項目我在中期加入的,主要負責整合shiro架構到項目中作為許可權管理組件,之前別的同事已經寫好了部分介面,我負責寫一部分新的介面。
之前同事解決跨域問題使用Spring提供的@CrossOrigin註解:
@RequestMapping(value = "/list.do", method = RequestMethod.GET)@ResponseBody@CrossOrigin(origins="*")@RequiresPermissions({"edge:manage"})public JSONObject deviceList(HttpServletRequest request, HttpServletResponse response) throws Exception { // do something return new Object();}
我進入項目的時候覺得這種方式太繁瑣了,需要在每一個Controller方法中都明確使用@CrossOrigin註解。
於是,我就使用Filter的方式解決我新寫的這部分介面,如下:
public class CROSFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest)request; HttpServletResponse resp = (HttpServletResponse)response; String origin = req.getHeader("Origin"); if(origin == null) { String referer = req.getHeader("Referer"); if(referer != null) { origin = referer.substring(0, referer.indexOf("/", 7)); } } resp.setHeader("Access-Control-Allow-Origin", origin); // 允許指定域訪問跨域資源 resp.setHeader("Access-Control-Allow-Credentials", "true"); if(RequestMethod.OPTIONS.toString().equals(req.getMethod())) { String allowMethod = req.getHeader("Access-Control-Request-Method"); String allowHeaders = req.getHeader("Access-Control-Request-Headers"); resp.setHeader("Access-Control-Max-Age", "86400"); // 瀏覽器緩衝預檢請求結果時間,單位:秒 resp.setHeader("Access-Control-Allow-Methods", allowMethod); // 允許瀏覽器在預檢請求成功之後發送的實際要求方法名 resp.setHeader("Access-Control-Allow-Headers", allowHeaders); // 允許瀏覽器發送的請求訊息頭 return; } chain.doFilter(request, response); }}
OK,到目前為止,訪問我新寫的介面沒任何問題,但是訪問同事之前寫好的介面,在瀏覽器console中報錯:
Failed to load http://10.100.157.34:8080/devicemanager/device/list.do: The ‘Access-Control-Allow-Origin‘ header contains multiple values ‘http://192.168.252.138:8000, http://192.168.252.138:8000‘, but only one is allowed. Origin ‘http://192.168.252.138:8000‘ is therefore not allowed access.main.js:162 Error: Network Error at FtD3.t.exports (createError.js:16) at XMLHttpRequest.f.onerror (xhr.js:87)
根據日誌描述,用戶端報錯是因為服務端返回的響應訊息頭Access-Control-Allow-Origin包含了2個值。
錯誤原因
項目中涉及跨域訪問資料的問題,同時還需要跨域傳遞Cookie,根據CROS協議的規定,響應訊息頭Access-Control-Allow-Origin值只能為指定單一網域名稱(註:不能為萬用字元“*”)。
但是,現在服務端返回的響應訊息頭Access-Control-Allow-Origin包含了多個值,用戶端認為不符合CROS協議,所以報錯。
那為什麼會返回多個值呢?是因為請求在我寫的Filter中已經設定了一次,而到Controller方法時又通過Spring的@CrossOrigin註解添加了一次。
解決辦法
既然是同一個訊息頭返回了多個值不合法,那麼就需要控制服務端只能返回一個值,這是解決問題的思路和方向。
顯然,在Filter中是不能達到這個目的的。
1.使用Spring攔截器修改響應訊息頭
第一個想法是通過自訂攔截器實現在Controller方法執行完畢之後修改響應訊息頭值,其他不做任何修改。
public class CrossFilter extends HandlerInterceptorAdapter { public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 如果已經設定了訊息頭,確保只設定一個值 String originHeader = "Access-Control-Allow-Origin"; if(response.containsHeader(originHeader)) { String origin = request.getHeader("Origin"); if(origin == null) { String referer = request.getHeader("Referer"); if(referer != null) { origin = referer.substring(0, referer.indexOf("/", 7)); } } response.setHeader("Access-Control-Allow-Origin", origin); } String credentialHeader = "Access-Control-Allow-Credentials"; if(response.containsHeader(credentialHeader)) { response.setHeader("Access-Control-Allow-Credentials", "true"); } }}
在Spring中添加攔截器配置:
<!-- 攔截器:對特定路徑進行攔截 --><mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**" /> <bean class="org.chench.test.filter.CrossFilter" /> </mvc:interceptor></mvc:interceptors>
但是,調試時發現:雖然在postHandle方法中已經明確設定了訊息頭為一個值,但是返回到瀏覽器用戶端的依然是2個值!
百思不得解!
於是開始Google相關問題,終於找到了一篇博文:https://mtyurt.net/2015/07/20/spring-modify-response-headers-after-processing/。
博主也是想在Controller方法執行之後添加響應訊息頭,但是採用Spring攔截器的方式也是不生效。
真正的原因是SpringMVC架構的限制,詳見:https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc。
在Spring的文檔中搜尋索引鍵:postHandle,看到如下聲明:
Note that postHandle is less useful with @ResponseBody and ResponseEntity methods for which a the response is written and committed within the HandlerAdapter and before postHandle. That means its too late to make any changes to the response such as adding an extra header. For such scenarios you can implement ResponseBodyAdvice and either declare it as an Controller Advice bean or configure it directly on RequestMappingHandlerAdapter.
What?原來是因為@ResponseBody註解的原因,導致無法通過攔截器的方式實現修改響應訊息頭的目的。
2.在ResponseBodyAdvice中修改響應訊息頭
由於Controller方法中已經使用了@ResponseBody註解返回json資料,故不能通過Spring攔截器修改響應訊息頭。
但是Spring同時還提供了一個ResponseBodyAdvice介面,允許在這種情境下實現對響應訊息頭的控制。
@ControllerAdvicepublic class HeaderModifierAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { ServletServerHttpRequest ssReq = (ServletServerHttpRequest)request; ServletServerHttpResponse ssResp = (ServletServerHttpResponse)response; if(ssReq == null || ssResp == null || ssReq.getServletRequest() == null || ssResp.getServletResponse() == null) { return body; } // 對於未添加跨域訊息頭的響應進行處理 HttpServletRequest req = ssReq.getServletRequest(); HttpServletResponse resp = ssResp.getServletResponse(); String originHeader = "Access-Control-Allow-Origin"; if(!resp.containsHeader(originHeader)) { String origin = req.getHeader("Origin"); if(origin == null) { String referer = req.getHeader("Referer"); if(referer != null) { origin = referer.substring(0, referer.indexOf("/", 7)); } } resp.setHeader("Access-Control-Allow-Origin", origin); } String credentialHeader = "Access-Control-Allow-Credentials"; if(!resp.containsHeader(credentialHeader)) { resp.setHeader(credentialHeader, "true"); } return body; }}
OK,完美解決!
當然,對應我寫的Filter還需要對應調整一下:
public class CROSFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if(logger.isDebugEnabled()) { logger.debug(String.format("CORS filter do filter")); } // 不再對所有請求都添加跨域訊息頭 // 在Filter中只對OPTIONS請求進行處理,跨域訊息頭放在ResponseBodyAdvice中解決 if(RequestMethod.OPTIONS.toString().equals(req.getMethod())) { HttpServletRequest req = (HttpServletRequest)request; HttpServletResponse resp = (HttpServletResponse)response; String origin = req.getHeader("Origin"); resp.setHeader("Access-Control-Allow-Origin", origin); // 允許指定域訪問跨域資源 resp.setHeader("Access-Control-Allow-Credentials", "true"); String allowMethod = req.getHeader("Access-Control-Request-Method"); String allowHeaders = req.getHeader("Access-Control-Request-Headers"); resp.setHeader("Access-Control-Max-Age", "86400"); // 瀏覽器緩衝預檢請求結果時間,單位:秒 resp.setHeader("Access-Control-Allow-Methods", allowMethod); // 允許瀏覽器在預檢請求成功之後發送的實際要求方法名 resp.setHeader("Access-Control-Allow-Headers", allowHeaders); // 允許瀏覽器發送的請求訊息頭 return; } chain.doFilter(request, response); }}
總結
1.對於項目中需要解決瀏覽器跨域問題的方案應該統一,要麼使用Filter方式,要麼使用@CrossOrigin註解,這個必須一開始就全域統一規劃好。
而我不得不使用上述方式解決問題,是因為前期已經寫好了很多代碼,不希望再去修改,不得已而為之。
2.對於使用了@ResponseBody註解的情境,如果需要統一調整響應訊息頭,只能通過自訂ResponseBodyAdvice實現來完成。
3.建議通過Filter方式解決跨域問題,而不要直接使用Spring的註解@CrossOrigin,太繁瑣。
【參考】
http://www.cnblogs.com/nuccch/p/7875189.html 跨域請求傳遞Cookie問題
https://www.w3.org/TR/cors/ Cross-Origin Resource Sharing
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc SpringMVC文檔
spring攔截器中修改響應訊息頭