解决HttpServletRequest的输入流只能读取一次的问题
背景
通常对安全性有要求的接口都会对请求参数做一些签名验证,而我们一般会把验签的逻辑统一放到过滤器或拦截器里,这样就不用每个接口都去重复编写验签的逻辑。
在一个项目中会有很多的接口,而不同的接口可能接收不同类型的数据,例如表单数据和json数据,表单数据还好说,调用request的getParameterMap就能全部取出来。而json数据就有些麻烦了,因为json数据放在body中,我们需要通过request的输入流去读取。
但问题在于request的输入流只能读取一次不能重复读取,所以我们在过滤器或拦截器里读取了request的输入流之后,请求走到controller层时就会报错。而本文的目的就是介绍如何解决在这种场景下遇到HttpServletRequest的输入流只能读取一次的问题。
注:本文代码基于SpringBoot框架
HttpServletRequest的输入流只能读取一次的原因
我们先来看看为什么HttpServletRequest的输入流只能读一次,当我们调用getInputStream()
方法获取输入流时得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承于InputStream。
InputStream的read()
方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()
会返回-1,表示已经读取完了。如果想要重新读取则需要调用reset()
方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。调用reset()
方法的前提是已经重写了reset()
方法,当然能否reset也是有条件的,它取决于markSupported()
方法是否返回true。
InputStream默认不实现reset()
,并且markSupported()
默认也是返回false,这一点查看其源码便知:
我们再来看看ServletInputStream,可以看到该类没有重写mark()
,reset()
以及markSupported()
方法:
综上,InputStream默认不实现reset的相关方法,而ServletInputStream也没有重写reset的相关方法,这样就无法重复读取流,这就是我们从request对象中获取的输入流就只能读取一次的原因。
使用HttpServletRequestWrapper + Filter解决输入流不能重复读取问题
既然ServletInputStream不支持重新读写,那么为什么不把流读出来后用容器存储起来,后面就可以多次利用了。那么问题就来了,要如何存储这个流呢?
所幸JavaEE提供了一个 HttpServletRequestWrapper类,从类名也可以知道它是一个http请求包装器,其基于装饰者模式实现了HttpServletRequest界面,部分源码如下:
从上图中的部分源码可以看到,该类并没有真正去实现HttpServletRequest的方法,而只是在方法内又去调用HttpServletRequest的方法,所以我们可以通过继承该类并实现想要重新定义的方法以达到包装原生HttpServletRequest对象的目的。
首先我们要定义一个容器,将输入流里面的数据存储到这个容器里,这个容器可以是数组或集合。然后我们重写getInputStream方法,每次都从这个容器里读数据,这样我们的输入流就可以读取任意次了。
具体的实现代码如下:
package com.example.wrapperdemo.controller.wrapper; import lombok.extern.slf4j.Slf4j; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.Charset; /** * @author 01 * @program wrapper-demo * @description 包装HttpServletRequest,目的是让其输入流可重复读 * @create 2018-12-24 20:48 * @since 1.0 **/ @Slf4j public class RequestWrapper extends HttpServletRequestWrapper { /** * 存储body数据的容器 */ private final byte[] body; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); // 将body数据存储起来 String bodyStr = getBodyString(request); body = bodyStr.getBytes(Charset.defaultCharset()); } /** * 获取请求Body * * @param request request * @return String */ public String getBodyString(final ServletRequest request) { try { return inputStream2String(request.getInputStream()); } catch (IOException e) { log.error("", e); throw new RuntimeException(e); } } /** * 获取请求Body * * @return String */ public String getBodyString() { final InputStream inputStream = new ByteArrayInputStream(body); return inputStream2String(inputStream); } /** * 将inputStream里的数据读取出来并转换成字符串 * * @param inputStream inputStream * @return String */ private String inputStream2String(InputStream inputStream) { StringBuilder sb = new StringBuilder(); BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset())); String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { log.error("", e); throw new RuntimeException(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { log.error("", e); } } } return sb.toString(); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } }
除了要写一个包装器外,我们还需要在过滤器里将原生的HttpServletRequest对象替换成我们的RequestWrapper对象,代码如下:
package com.example.wrapperdemo.controller.filter; import com.example.wrapperdemo.controller.wrapper.RequestWrapper; import lombok.extern.slf4j.Slf4j; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author 01 * @program wrapper-demo * @description 替换HttpServletRequest * @create 2018-12-24 21:04 * @since 1.0 **/ @Slf4j public class ReplaceStreamFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { log.info("StreamFilter初始化..."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request); chain.doFilter(requestWrapper, response); } @Override public void destroy() { log.info("StreamFilter销毁..."); } }
然后我们就可以在拦截器中愉快的获取json数据也不慌controller层会报错了:
package com.example.wrapperdemo.controller.interceptor; import com.example.wrapperdemo.controller.wrapper.RequestWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author 01 * @program wrapper-demo * @description 签名拦截器 * @create 2018-12-24 21:08 * @since 1.0 **/ @Slf4j public class SignatureInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("[preHandle] executing... request uri is {}", request.getRequestURI()); if (isJson(request)) { // 获取json字符串 String jsonParam = new RequestWrapper(request).getBodyString(); log.info("[preHandle] json数据 : {}", jsonParam); // 验签逻辑...略... } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } /** * 判断本次请求的数据类型是否为json * * @param request request * @return boolean */ private boolean isJson(HttpServletRequest request) { if (request.getContentType() != null) { return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE); } return false; } }
编写完以上的代码后,还需要将过滤器和拦截器在配置类中进行注册才会生效,过滤器配置类代码如下:
package com.example.wrapperdemo.config; import com.example.wrapperdemo.controller.filter.ReplaceStreamFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; /** * @author 01 * @program wrapper-demo * @description 过滤器配置类 * @create 2018-12-24 21:06 * @since 1.0 **/ @Configuration public class FilterConfig { /** * 注册过滤器 * * @return FilterRegistrationBean */ @Bean public FilterRegistrationBean someFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(replaceStreamFilter()); registration.addUrlPatterns("/*"); registration.setName("streamFilter"); return registration; } /** * 实例化StreamFilter * * @return Filter */ @Bean(name = "replaceStreamFilter") public Filter replaceStreamFilter() { return new ReplaceStreamFilter(); } }
拦截器配置类代码如下:
package com.example.wrapperdemo.config; import com.example.wrapperdemo.controller.interceptor.SignatureInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author 01 * @program wrapper-demo * @description * @create 2018-12-24 21:16 * @since 1.0 **/ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Bean public SignatureInterceptor getSignatureInterceptor(){ return new SignatureInterceptor(); } /** * 注册拦截器 * * @param registry registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getSignatureInterceptor()) .addPathPatterns("/**"); } }
接下来我们就可以测试一下在拦截器中读取了输入流后在controller层是否还能正常接收数据,首先定义一个实体类,代码如下:
package com.example.wrapperdemo.param; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * @author 01 * @program wrapper-demo * @description * @create 2018-12-24 21:11 * @since 1.0 **/ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserParam { private String userName; private String phone; private String password; }
然后写一个简单的Controller,代码如下:
package com.example.wrapperdemo.controller; import com.example.wrapperdemo.param.UserParam; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 01 * @program wrapper-demo * @description * @create 2018-12-24 20:47 * @since 1.0 **/ @RestController @RequestMapping("/user") public class DemoController { @PostMapping("/register") public UserParam register(@RequestBody UserParam userParam){ return userParam; } }
启动项目,请求结果如下,可以看到controller正常接收到数据并返回了:
控制台输出如下:
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
精讲Spring Boot—— 入门+进阶+实例
Spring Boot到底是什么 在了解Spring Boot之前应该先了解下Spring,因为Spring Boot的核心是基于Spring构建的。Spring是由Rod Johnson在2002年创建的一个开源框架,Spring在创建到现在一直处于飞速发展之中。目前已经成为J2EE开发中的事实标准。但是随着IT技术的飞速发展,及IT项目变得越来越复杂。如果学习过Spring就知道Spring非常依赖xml文件进行配置,有时一个项目就有几十个xml配置文件,这些繁琐的配置、以及Spring整合第三方框架的配置问题。经常导致开发效率低下,一不小心就出现丢三拉四;有时一不留神就导致发版失败,给公司造成经济损失。 Spring Boot是由Pivotal团队打造的,并在2014年发布的一个全新框架,其设计目标就是用来简化Spring应用的搭建、开发及部署。Spring Boot采用了特定的方式进行配置,从而达到使开发人员不在需要配置繁琐的xml文件。简单的说其实Spring Boot并不是什么新的框架,就是摒弃了xml的配置方式,使用了一种新的配置方式;然后在maven中使用starter...
- 下一篇
微软特权访问管理
2018-2022是私有云混合云在中国最火热的时代,私有云将在中国从摸索走向成熟阶段,随着云技术的火热,下一个企业必须要思考的将是信息安全的问题,现在企业都在导入云计算技术,建置更多的信息应用系统以从中获取信息化带来的价值。那么随着带来的一个隐患就是,管理员要管理的基础架构和应用系统数量越来越多,这时候管理员账户就变的很重要了,如何保证管理员账户能够安全,如果保证管理员账户的管理操作可控,可记录,如果保证云资源管理员和租户虚机的隐私问题,将是企业信息化接下来必须要考虑的安全点 老王之前做企业咨询实施的时候发现国内企业信息化,发现有一些现象,企业IT部门的管理员,可以很轻而易举的获得多个高权限的管理账号,有些企业信息项目外包出去,外包人员需要的管理员账户,做完项目也不回收。导致了Domain Admins组有大量的用户,这其实是极大的一个安全隐患,越多的高权限账户就有越多的风险,Hacker或者内部恶意管理员只要随便破解找到一个管理员的账号密码,就可以利用这个账号访问所有的域内系统,如果没有虚拟机保护机制,还可以利用此账户去窥探任意租户虚拟机的内容。 针对于这些云技术时代产生的安全隐患,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装Docker,最新的服务器搭配容器使用
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7设置SWAP分区,小内存服务器的救世主