问题描述

前后端分离的项目,前端使用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. Android 一些关于 Activity 的技巧

    锁定 Activity 运行时的屏幕方向 Android 内置了方向感应器的支持.在 G1 中,Android 会根据 G1 所处的方向自动在竖屏和横屏间切换.但是有时我们的应用程序仅能在横屏 / 竖 ...

  2. rt-thread中软件定时器组件超时界限的一点理解

    @2019-01-15 [小记] 对 rt-thread 中的软件定时器组件中超时界限的一点理解 rt_thread_timer_entry(void *parameter)函数中if ((next_ ...

  3. 【dfs】LETTERS

    1212:LETTERS [题目描述] 给出一个roe×colroe×col的大写字母矩阵,一开始的位置为左上角,你可以向上下左右四个方向移动,并且不能移向曾经经过的字母.问最多可以经过几个字母. [ ...

  4. luogu5010 HMR的LIS III (dp+线段树)

    这个东西和最长上升子序列很像 考虑如果已经知道每个位置为开头的LIS长度和个数 f[i],我可以扫一遍 判断这个个数和K的大小,找到第一个长度=len而且个数<K的,这个位置就是要选的 然后K- ...

  5. 小电阻之大作用——CAN终端电阻

    CAN总线终端电阻,顾名思义就是加在总线末端的电阻.此电阻虽小,但在CAN总线通信中却有十分重要的作用. 终端电阻的作用 CAN总线终端电阻的作用有两个: 1.提高抗干扰能力,确保总线快速进入隐性状态 ...

  6. BZOJ2288 生日礼物

    本题是数据备份的进阶版. 首先去掉所有0,把连续的正数/负数连起来. 计算所有正数段的个数与总和. 然后考虑数据备份,有一点区别: 如果我们在数列中选出一个负数,相当于把它左右连起来. 选出一个正数, ...

  7. 洛谷P3195 玩具装箱

    P3195 [HNOI2008]玩具装箱TOY 第一道斜率优化题. 首先一个基本的状态转移方程是 要使f[i]最小,即b最小. 对于每个j,可以表示为一个点. 然后我们取固定斜率时截距最小的即可,高中 ...

  8. 记一次 HTTP信息头管理器使用 的重要性

    今天在测试中遇到了一个问题 使用JMeter时请求相关地址参数及方法都填写正确,但是相应数据返回始终不对,例如 查看取样器结果显示 200 正常,但响应数据不符合正常的结果. 经反复检查发现问题如下: ...

  9. 苹果电脑利用wget总是会出现无法建立 SSL 连接的问题

    在做迁徙学习的过程中,需要下载已经训练好的Inception_v3模型,首先我们为了将下载的模型保存到指定的地方,我们需要利用 wget -P 想要保存的目录 模型的网址,例如 wget -P /Vo ...

  10. tomcat部署-手动启动tomcat部署,添加网页,

    公司的内网什么都不能往外传,于是自己用公司的网络搭了一个网页,在网上抄了一堆upload,用来来回传输数据.... 但是每次用ideaJ启动服务器太费时. 研究了一下怎么手动启动tomcat,部署网页 ...