场景:
对于新版本(版本信息在请求header中)接口,使用AES算法对返回结果进行全局加密。
原始接口都是RESTful类型,使用@ResponseBody
标注,返回json数据。
考虑实现方案:
- web容器过滤器
Filter
- Spring拦截器
Interceptor
- Spring的
HttpMessageConverter
- Spring的
ResponseBodyAdvice
Spring拦截器Interceptor
开始想到的是使用Spring的拦截器实现,在postHandle
中对返回结果进行加密。
但是发现拦截器中并不能修改response内容。
想想拦截器的使用场景:
- 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等;
- 权限检查:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面;
- 性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);
- 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现。
我们平时用的最多的是在preHandle
方法中进行参数校验、权限校验等,并不适用于修改response的场景。
并且Spring文档中也指出:
Note that postHandle is less useful with
@ResponseBody
andResponseEntity
methods for which the response is written and committed within the HandlerAdapter and before postHandle. That means it is 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 anController Advice
bean or configure it directly onRequestMappingHandlerAdapter
.
返回值response已经在HandlerAdapter
中提交了,postHandle
中去修改是没有用的。
Spring的ResponseBodyAdvice
ResponseBodyAdvice
这个接口可以在Controller
处理完请求后,交给HttpMessageConverter
之前,对返回值进行处理。
|
|
注意,接口返回的是json数据,那么会使用MappingJackson2HttpMessageConverter
写到respon body中。
这个方法是在写之前调用的,我们可以将body进行加密,但是,最终还是会以json的形式写入,不符合场景需求。
Spring的HttpMessageConverter
上面说了,接口返回的是json数据,那么会使用MappingJackson2HttpMessageConverter
写到respon body中。我们可以重写MappingJackson2HttpMessageConverter
,在写方法中对数据进行加密:
这样确实能搞定加密。但是这个方法中并不能拿到request,不能获取header中的版本信息。因此,也不符合场景需求。
过滤器Filter(选取方案)
其实最开始想到的也是使用过滤器Filter,但是遇到了一些麻烦。
第一件事是,如何在Filter中修改response?
搜索找到答案,实现HttpServletResponseWrapper
,然后在其中保存response的流数据:
对应的Filter代码:
web.xml中的配置:
这样,仅配置一个过滤器,没有问题,加密成功。
但是,业务层还用到了etag过滤器,配合使用时,就出现了奇怪的现象。
第一种配合方式,先配置aes加密过滤器,然后配置etag过滤器:
我们知道,对于相同的url-pattern,过滤器按照声明顺序执行,返回时,先处理etag,然后处理aes加密。
此时,发现加密成功,日志打印正常,但是response中拿到的数据被截断了,长度与未加密前的数据长度相等。
也就是说aes过滤器处理后,content-length没有更新。
换一种配合方式,先声明etag过滤器,在声明aes过滤器,数据没有被截断。但是,对response添加header操作失败。
debug一下,发现问题出现在ShallowEtagHeaderFilter
中,它内部使用ContentCachingResponseWrapper
包装response,对返回结果进行处理。
我们也要对response进行处理,但是content-length没有更新,一定是BufferedServletResponseWrapper
写法不正确,少实现了一些方法。
阅读ContentCachingResponseWrapper
代码,发现了一些痕迹,它不光实现了getOutputStream()
和getWriter()
两个重要的方法,还是实现了如下两个方法:
从注释可以看出,重写flushBuffer()
方法是为了防止在我们的wrapper类复制数据之前,底层的response输出数据到客户端。这样,我们才能在外层修改response,例如添加头信息等。
第二个方法setContentLength()
是为了更新数据存储区域的,例如,我们加密数据之后,长度变化了,content-length也要变化。
仿照ContentCachingResponseWrapper
,将重要的方法重写:
这样就可以完美修改response了。
有人可能会说,为什么不直接继承ContentCachingResponseWrapper
?
主要是因为ShallowEtagHeaderFilter
中会用一下方法寻找response wrapper,如果我们也用这个类,那么会产生冲突,造成加密无效。
最终的方案是将ContentCachingResponseWrapper
中的代码考过来,放到一个新的类中,然后在web.xml中注册时,还是先声明etag过滤器,再声明aes加密过滤器。
对于相同内容,AES加密后的结果是相同的,因此不会影响Etag比对。