问题描述

前后端分离的项目,前端使用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. Educational Codeforces Round 33 (Rated for Div. 2) F. Subtree Minimum Query(主席树合并)

    题意 给定一棵 \(n\) 个点的带点权树,以 \(1\) 为根, \(m\) 次询问,每次询问给出两个值 \(p, k\) ,求以下值: \(p\) 的子树中距离 \(p \le k\) 的所有点权 ...

  2. Hdoj 1102.Constructing Roads 题解

    Problem Description There are N villages, which are numbered from 1 to N, and you should build some ...

  3. [WC2011]最大XOR和路径(贪心+线性基)

    题目大意:给一张无向图,求一条1-n的路径,是路径边权的异或和最小. 题解 这道题的思路很妙,首先我们可以随便找出一条从1到n的路径来,然后我们可以选一些环. 其实不管这个环和这条路径有怎样的关系,我 ...

  4. 生命不息,折腾不止 ~ 旧PC改造之家庭影音

    前言引入 之前把在校园陪伴多年的旧电脑由Win装成了Linux,的确不卡了,基本上日常办公也够了(大项目还是吃不消,日常捣鼓倒是够了),然后把真正的工作游戏本也改成了Linux,那么旧电脑又变成闲置机 ...

  5. nodejs的某些api~(四)udp&dns

    今天记udp/数据报套接字和dns. udp UDP/数据报套接字 => require('dgram');dgram.createServer([type],[cb]);type:可以是'ud ...

  6. Home School Books美国家庭学校教育小学初中高中全套美语教材

    加州的资料总共买过三次: ①优妈妈儿童教育,买过美国加州小学一.二年级的语文及相应的练习册,并买了纸版资料. (这是自己学习用的) ②美国加州原版小学教材Reading Wonders 2014新版语 ...

  7. CF670C cinema

    想必是个半水题,div2的C嘛 仔细观察,发现排序可做. 怎么排序呢?排啥呢?拿啥离散化,拿啥结构体呢? 仔细思考热静分析,便可得出结论: 以每个人会的语言离散化,把每个电影建结构体后不排序,而是枚举 ...

  8. 跟我一起学习vue2(熟悉vue.js)[一]

    由于有angularjs的基础,所以我第一步是在官网:https://cn.vuejs.org/ 上面看了三遍他的理论知识,还有实例. 现在做完了第二步,就是在菜鸟教程:http://www.runo ...

  9. Codeforce 867 C. Ordering Pizza (思维题)

    C. Ordering Pizza It's another Start[c]up finals, and that means there is pizza to order for the ons ...

  10. POJ 3678 Katu Puzzle (2-SAT)

                                                                         Katu Puzzle Time Limit: 1000MS ...