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文档

时间: 2024-07-28 17:53:16

spring拦截器中修改响应消息头的相关文章

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

最近有一个需要从拦截器中获取post请求的参数的需求,这里记录一下处理过程中出现的问题. 首先想到的就是request.getParameter(String )方法,但是这个方法只能在get请求中取到参数,post是不行的,后来想到了使用流的方式,调用request.getInputStream()获取流,然后从流中读取参数,如下代码所示: String body = ""; StringBuilder stringBuilder = new StringBuilder(); Buf

HTTP响应消息头泄露信息的处理

一些Web服务器存在版本漏洞,就是说有些版本有漏洞,而有些版本没有. 在访问Web服务器时,返回的HTTP响应消息头(Response Header)中通常包含Server版本以及其他一些信息.这些头信息可用于网站统计分析,比如某些爬虫类搜索引擎,当然也包括攻击者进行社会工程信息收集.    事实上,有些头信息完全可以去掉或隐藏,而不影响系统正常访问,同时也节省了少许传输字节. 隐藏服务器在HTTP响应消息头中的不必要信息,是为了防止服务器的版本信息泄露,可做为提高站点安全的一项初步防护措施.

Spring 拦截器实现事物

Spring+Hibernate的实质:就是把Hibernate用到的数据源Datasource,Hibernate的SessionFactory实例,事务管理器HibernateTransactionManager,都交给Spring管理.那么再没整合之前Hibernate是如何实现事务管理的呢?通过ServletFilter实现数据库事务的管理,这样就避免了在数据库操作中每次都要进行数据库事务处理.一.事务的4个特性: 原子性:一个事务中所有对数据库的操作是一个不可分割的操作序列,要么全做,

spring 拦截器简介

spring 拦截器简介 常见应用场景 1.日志记录:记录请求信息的日志,以便进行信息监控.信息统计.计算PV(Page View)等.2.权限检查:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面:3.性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录):4.通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提

spring 拦截器拦截点的配置

实用正则org.springframework.aop.support.RegexpMethodPointcutAdvisor 然后 <property name="advice"> <ref local="methodCacheInterceptor"/> </property> <property name="patterns"> <list> <value>.*_cac

Spring拦截器

Spring的拦截器具备在web的前置和后置来处理各种请求. 拦截器接口-HandlerInterceptor 自定义的拦截器,需要继承HandlerInterceptor接口,并且实现HandlerInterceptor中提供的三个方法: 1. preHandle 方法会在请求处理前被调用.这个方法返回boolean值,如果返回true则继续往下执行,如果返回false则中断. 2. postHandle 方法会在请求处理后,继续调用. 3. afterCompletion 方法会在视图渲染之

【MARK】拦截器中自动注入失败问题

我在拦截器中想自动注入一个对象的时候发现无法注入,获取到的一直是null public class RestInterceptor implements HandlerInterceptor { @Autowired private EscUserMapper escUserMapper; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object hand

SpringMVC拦截器中通过反射得到Controller方法注解时ClassCastException解决方案

错误应用场 在Controller中,我们自定义了一个@Auth注解来实现权限控制功能,如: @Auth(verifyLogin=false,verifyURL=false) @RequestMapping("/login") public ModelAndView login(HttpServletRequest request,HttpServletResponse response) throws Exception{ Map<String,Object> conte

SpringBoot拦截器中service或者redis注入为空的问题

原文:https://my.oschina.net/u/1790105/blog/1490098 这两天遇到SpringBoot拦截器中Bean无法注入问题.下面介绍我的思考过程和解决过程: 1.由于其他bean在service,controller层注入一点问题也没有,开始根本没意识到Bean无法注入是在拦截器中无效的问题,一直在查找注解指定的包在哪里配置的,然而却找不到配置,Springboot是用java类的形式加载配置的.在网络的某个角落看到这样的说法: SpringBoot项目的Bea