问题描述

前后端分离的项目,前端使用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接口,允许在这种场景下实现对响应消息头的控制。

@ControllerAdvice
public 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拦截器中修改响应消息头的更多相关文章

  1. Spring拦截器中通过request获取到该请求对应Controller中的method对象

    背景:项目使用Spring 3.1.0.RELEASE,从dao到Controller层全部是基于注解配置.我的需求是想在自定义的Spring拦截器中通过request获取到该请求对应于Control ...

  2. spring拦截器中使用spring的自动注入

    需要在spring的拦截器中使用自定义的服务,这要就设计到将服务注入到拦截器中.网上看的情况有两种: 1. @Configuration public class OptPermissionHandl ...

  3. 在spring拦截器中response输出html标签到页面

    @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object ...

  4. Spring 拦截器postHandle无法修改Response的原因

    如果controller跳转至页面,postHandle是没问题的. 如果@ResponseBody注释 或者返回 ResponseEntity,在postHandle拦截器中修改请求头,是无效的. ...

  5. 【跨域】SpringBoot跨域,拦截器中,第一次获取的请求头为NULL,发送两次请求的处理方式

    背景: 在做前后端分离时,牵扯到跨域,但是已经设置了跨域 前端设置了允许携带Cookie axios.defaults.withCredentials = true; 后端也配置了跨域 浏览器端查看发 ...

  6. spring boot拦截器中获取request post请求中的参数

    最近有一个需要从拦截器中获取post请求的参数的需求,这里记录一下处理过程中出现的问题. 首先想到的就是request.getParameter(String )方法,但是这个方法只能在get请求中取 ...

  7. 【spring boot】在自定义拦截器中从request中获取json字符串

    又这样的需求,需要在自定义的拦截器中获取request中的数据,想获取到的是JSON字符串 那需要在拦截器中写这样一个方法 public static String getOpenApiRequest ...

  8. Spring拦截器总结

    本文是对慕课网上"搞定SSM开发"路径的系列课程的总结,详细的项目文档和课程总结放在github上了.点击查看 Spring过滤器WebFilter可以配置中文过滤 拦截器实现步骤 ...

  9. Spring 拦截器——HandlerInterceptor

    采用Spring拦截器的方式进行业务处理.HandlerInterceptor拦截器常见的用途有: 1.日志记录:记录请求信息的日志,以便进行信息监控.信息统计.计算PV(Page View)等. 2 ...

随机推荐

  1. 缺省源和 Vim 配置

    缺省源 #include <bits/stdc++.h> #define x first #define y second #define pb push_back #define mp ...

  2. CS Academy Sliding Product Sum(组合数)

    题意 有一个长为 \(N\) 的序列 \(A = [1, 2, 3, \dots, N]\) ,求所有长度 \(\le K\) 的子串权值积的和,对于 \(M\) 取模. \(N \le 10^{18 ...

  3. 自学华为IoT物联网之路

    自学华为IoT物联网之路 01 自学华为IoT物联网_01 物联网概述 02 自学华为IoT物联网_02 常见物联网通信技术 03 自学华为IoT物联网_03 公共事业物联网常见问题及解决方案 04  ...

  4. NOIP2013火柴排队

    Solution 恕我直言,这题是真的坑. 对于这道题,一个很显然的思路是对于A B两个序列,他们交换完后相对的两个数在原序列中的相对大小是相同的,于是我们就把序列按照A排序,在把B离散化,求逆序对, ...

  5. 按奇偶排序数组 II

    题目描述 给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数. 对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数:当 A[i] 为偶数时, i 也是偶数. 你可以返回任何满足上述 ...

  6. Shiro中的Rememberme后出现浏览器500错误

    问题详述:在Shiro中添加Remember me功能后,只要勾选Remember me选项为true的时候,浏览器就会跳转到一个不可达页面,并且在Chrome中显示HTTP 500错误. 问题追踪: ...

  7. (一)flask-sqlalchemy的安装和配置

    在使用flask-sqlalchemy之前要先了解ORM模型,什么叫做ORM模型 一.什么是ORM ORM 全拼Object-Relation Mapping. 称为对象-关系映射 主要实现模型对象到 ...

  8. MySQL -- 单行函数

    大小写控制函数 SELECT LOWER('HelloWrold'), UPPER('HelloWorld'); 字符控制函数 SELECT REPLACE('abcdababab','p','m') ...

  9. [luogu1503][鬼子进村]

    题目链接 思路 将哪些村庄已经被摧毁了放到treap里.查询的时候如果当前村庄已经被毁了,那么就可以直接输出0.不然就输出这个村庄的后继-前驱-1.原因显然 代码 #include<cstdio ...

  10. MD5加密解密类(asp.net)&使用MD5过时处理

    加密类 #region ========加密======== /// <summary> /// 加密 /// </summary> /// <param name=&q ...