spring拦截器中修改响应消息头
问题描述
前后端分离的项目,前端使用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拦截器中修改响应消息头的更多相关文章
- Spring拦截器中通过request获取到该请求对应Controller中的method对象
背景:项目使用Spring 3.1.0.RELEASE,从dao到Controller层全部是基于注解配置.我的需求是想在自定义的Spring拦截器中通过request获取到该请求对应于Control ...
- spring拦截器中使用spring的自动注入
需要在spring的拦截器中使用自定义的服务,这要就设计到将服务注入到拦截器中.网上看的情况有两种: 1. @Configuration public class OptPermissionHandl ...
- 在spring拦截器中response输出html标签到页面
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object ...
- Spring 拦截器postHandle无法修改Response的原因
如果controller跳转至页面,postHandle是没问题的. 如果@ResponseBody注释 或者返回 ResponseEntity,在postHandle拦截器中修改请求头,是无效的. ...
- 【跨域】SpringBoot跨域,拦截器中,第一次获取的请求头为NULL,发送两次请求的处理方式
背景: 在做前后端分离时,牵扯到跨域,但是已经设置了跨域 前端设置了允许携带Cookie axios.defaults.withCredentials = true; 后端也配置了跨域 浏览器端查看发 ...
- spring boot拦截器中获取request post请求中的参数
最近有一个需要从拦截器中获取post请求的参数的需求,这里记录一下处理过程中出现的问题. 首先想到的就是request.getParameter(String )方法,但是这个方法只能在get请求中取 ...
- 【spring boot】在自定义拦截器中从request中获取json字符串
又这样的需求,需要在自定义的拦截器中获取request中的数据,想获取到的是JSON字符串 那需要在拦截器中写这样一个方法 public static String getOpenApiRequest ...
- Spring拦截器总结
本文是对慕课网上"搞定SSM开发"路径的系列课程的总结,详细的项目文档和课程总结放在github上了.点击查看 Spring过滤器WebFilter可以配置中文过滤 拦截器实现步骤 ...
- Spring 拦截器——HandlerInterceptor
采用Spring拦截器的方式进行业务处理.HandlerInterceptor拦截器常见的用途有: 1.日志记录:记录请求信息的日志,以便进行信息监控.信息统计.计算PV(Page View)等. 2 ...
随机推荐
- centos install redmine (项目管理工具)
安装环境:Centos.mysql.Ruby.Apache.Redmineyum updateyum -y groupinstall "Development Tools"yum ...
- Apache 安装及常用参数设置
禁用 selinux setenforce 0 sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/confi ...
- <数据结构基础学习>(一)数组
一.数组基础 1.数组,即把数据码成一排存放. 数组优点:快速查询. 数组最好应用于“索引有语意”的情况,但并非所有有语意的索引都适用于数组,数组也可以处理“索引没有语意”的情况. 2.增.删.改.查 ...
- 逆向并查集 HYSBZ1015星球大战starwar
星球大战starwar HYSBZ - 1015 很久以前,在一个遥远的星系,一个黑暗的帝国靠着它的超级武器统治者整个星系.某一天,凭着一个偶然的机遇,一支反抗军摧毁了帝国的超级武器,并攻下了星系 ...
- 「SDOI2014」向量集 解题报告
「SDOI2014」向量集 维护一个向量集合,在线支持以下操作: A x y :加入向量 \((x, y)\): Q x y l r:询问第 \(L\) 个到第 \(R\) 个加入的向量与向量 \(( ...
- Nginx+Tomcat-cluster构建
-----------ReProxy-------------------------Client-----------192.168.56.202 nginx 192.168.56.200 Tomc ...
- [SCOI2014]方伯伯的OJ(线段树)
方伯伯正在做他的Oj.现在他在处理Oj上的用户排名问题.Oj上注册了n个用户,编号为1-n“,一开始他们按照编号排名. 方伯伯会按照心情对这些用户做以下四种操作,修改用户的排名和编号: 1.操作格式为 ...
- 跟我一起用node-express搭建一个小项目[一]
我一直以为你要学nodejs你就要掌握很多东西啊!比如js最少得看10本书吧,nodejs书籍得看吧!其实呢,到最后,你会发现,因为工作,或者找工作的需要,什么不学,那些js不懂 也没有很大的关系.所 ...
- struts2 数据转换器
四.数据类型的转换(明白原理,实际开发中几乎不用) 1.开发中的情况: 实际开发中用户通过浏览器输入的数据都是String或者String[]. String/String[]————填充模型(set ...
- glob.glob 匹配文件
glob.glob以列表形式返回匹配的文件路径 只有一个参数:文件的匹配规则 e.g. >>>res_home = '/vip_data_center/test_envs/train ...