spring 两次进入拦截器_SpringBoot 拦截器获取http请求参数

本文详细介绍了在SpringBoot中使用拦截器获取HTTP请求参数的需求,以及遇到的拦截器执行两次和ServletInputStream无法重复读取的问题。通过自定义HttpServletRequestWrapper和DispatcherServlet子类,解决了输入流重复读取的难题,为服务端接口签名校验、防重复请求等提供了实现思路。
摘要由CSDN通过智能技术生成

本文作者:上帝爱吃苹果
本文链接:https://www.cnblogs.com/keeya/p/13634015.html

获取http请求参数是一种刚需

我想有的小伙伴肯定有过获取http请求的需要,比如想

  1. 前置获取参数,统计请求数据
  2. 做服务的接口签名校验
  3. 敏感接口监控日志
  4. 敏感接口防重复提交

等等各式各样的场景,这时你就需要获取 HTTP 请求的参数或者请求body,一般思路有两种,一种就是自定义个AOP去拦截目标方法,第二种就是使用拦截器。整体比较来说,使用拦截器更灵活些,因为每个接口的请求参数定义不同,使用AOP很难细粒度的获取到变量参数,本文主线是采用拦截器来获取HTTP请求。

定义拦截器获取请求

基于 spring-boot-starter-parent 2.1.9.RELEASE

看起来这个很简单,这里就直接上code,定义个拦截器

/** * @author axin * @summary HTTP请求拦截器 */@Slf4jpublic class RequestInterceptor implements HandlerInterceptor {    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        //获取请求参数        String queryString = request.getQueryString();        log.info("请求参数:{}", queryString);        //获取请求body        byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());        String body = new String(bodyBytes, request.getCharacterEncoding());        log.info("请求体:{}", body);        return true;    }}

然后把这个拦截器配置一下中:

/** * WebMVC配置,你可以集中在这里配置拦截器、过滤器、静态资源缓存等 */@Configurationpublic class WebMvcConfig implements WebMvcConfigurer {    @Override    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");    }}

定义个接口测试一下

/** * @author axin * @summary 提交测试接口 */@Slf4j@RestControllerpublic class MyHTTPController {    @GetMapping("/v1/get")    public void get(@RequestParam("one") String one,                    @RequestParam("two") BigDecimal number) {        log.info("参数:{},{}", one, number);    }    @PostMapping("/v1/post")    public void check(@RequestBody User user) {        log.info("{}", JSON.toJSONString(user));    }}

GET请求获取请求参数示例:

e3556659554f28041006300b788cf4a0.png

POST请求获取请求Body示例:

7b282dd5ec5800d85a14dbfa0c07d5f5.png

我们发现拦截器在获取HTTP请求的body时出现了 400 (Required request body is missing: public void com.axin.world.controller.MyHTTPController.check(com.axin.world.domain.User));同时也发现拦截器竟然走了两遍,这又是咋回事呢?

73678c7f589e38e1b11f8de0ae3a4b44.png

为什么拦截器会重复调两遍呢?

其实是因为 tomcat截取到异常后就转发到/error页面,就在这个转发的过程中导致了springmvc重新开始DispatcherServlet的整个流程,所以拦截器执行了两次,我们可以看下第二次调用时的url路径:

34a06ad014a5ba95c9928e3684ed0b4c.png

ServletInputStream(CoyoteInputStream) 输入流无法重复调用

而之前出现的 Required request body is missing 错误 其实是ServletInputStream被读取后无法第二次再读取了,所以我们要把读取过的内容存下来,然后需要的时候对外提供可被重复读取的ByteArrayInputStream。

对于MVC的过滤器来说,我们就需要重写 ServletInputStream 的 getInputStream()方法。

自定义 HttpServletRequestWrapper

为了 重写 ServletInputStream 的 getInputStream()方法,我们需要自定义一个 HttpServletRequestWrapper :

/*** @author Axin* @summary 自定义 HttpServletRequestWrapper 来包装输入流*/public class AxinHttpServletRequestWrapper extends HttpServletRequestWrapper {    /**     * 缓存下来的HTTP body     */    private byte[] body;    public AxinHttpServletRequestWrapper(HttpServletRequest request) throws IOException {        super(request);        body = StreamUtils.copyToByteArray(request.getInputStream());    }    /**     * 重新包装输入流     * @return     * @throws IOException     */    @Override    public ServletInputStream getInputStream() throws IOException {        InputStream bodyStream = new ByteArrayInputStream(body);        return new ServletInputStream() {            @Override            public int read() throws IOException {                return bodyStream.read();            }            /**             * 下面的方法一般情况下不会被使用,如果你引入了一些需要使用ServletInputStream的外部组件,可以重点关注一下。             * @return             */            @Override            public boolean isFinished() {                return false;            }            @Override            public boolean isReady() {                return true;            }            @Override            public void setReadListener(ReadListener readListener) {            }        };    }        @Override    public BufferedReader getReader() throws IOException {        InputStream bodyStream = new ByteArrayInputStream(body);        return new BufferedReader(new InputStreamReader(getInputStream()));    }}

然后定义一个 DispatcherServlet子类来分派 上面自定义的 AxinHttpServletRequestWrapper :

/*** @author Axin* @summary 自定义 DispatcherServlet 来分派 AxinHttpServletRequestWrapper*/public class AxinDispatcherServlet extends DispatcherServlet {    /**     * 包装成我们自定义的request     * @param request     * @param response     * @throws Exception     */    @Override    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {        super.doDispatch(new AxinHttpServletRequestWrapper(request), response);    }}

然后配置一下:

/** * WebMVC配置,你可以集中在这里配置拦截器、过滤器、静态资源缓存等 */@Configurationpublic class WebMvcConfig implements WebMvcConfigurer {    @Override    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");    }    @Bean    @Qualifier(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)    public DispatcherServlet dispatcherServlet() {        return new AxinDispatcherServlet();    }}

再调用一下 POST请求:

7d6e8c275481d72a74e881122ac230b9.png

请求成功!

总结一下 展望一下

如果你想对HTTP请求做些骚操作,那么前置获取HTTP请求参数是前提,为此文本给出了使用MVC拦截器获取参数的样例。

在获取HTTP Body 的时候,出现了 Required request body is missing 的错误,同时拦截器还出现执行了两遍的问题,这是因为 ServletInputStream被读取了两遍导致的,tomcat截取到异常后就转发到 /error 页面 被拦截器拦截到了,拦截器也就执行了两遍。

为此我们通过自定义 HttpServletRequestWrapper 来包装一个可被重读读取的输入流,来达到期望的拦截效果。

在获取到HTTP的请求参数后,我们可以前置做很多操作,比如常用的服务端接口签名验证,敏感接口防重复请求等等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值