解决HttpServletRequest输入流只能读一次的问题

简介: 在日常的开发中 出于对接口的安全性考虑会要求对接口的请求参数等一起进行签名 加签验签的处理。不同的接口中可能接受不同的数据类型。例如表单数据和json数据,表单数据还好说,调用request的getParameterMap就能全部取出来。而json数据就有些麻烦了,因为json数据放在body中,我们需要通过request的输入流去读取。但问题在于request的输入流只能读取一次不能重复读取,所以我们在过滤器或拦截器里读取了request的输入流之后,请求走到controller层时就会报错。而本文的目的就是介绍如何解决在这种场景下遇到HttpServlet

为什么HttpServletRequest的输入流只能读一次呢?

当我们调用getInputStream()方法获取输入流时得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承于InputStream。

InputStream的read()方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了。如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。调用reset()方法的前提是已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true。

InputStream默认不实现reset的相关方法,而ServletInputStream也没有重写reset的相关方法,这样就无法重复读取流,这就是我们从request对象中获取的输入流就只能读取一次的原因。


解决办法:重写HttpServletRequestWrapper方法

通过重写HttpServletRequestWrapper把request的保存下来,然后通过过滤器保存下来的request在填充进去,这样就可以多次读取request了。

重写HttpServletRequestWrapper

package com.aliyun.icc.core.config.httpHelp;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Enumeration;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class BodyReaderHttpServletRequestWrapper extends
        HttpServletRequestWrapper {
    private final byte[] body;
    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        Enumeration e = request.getHeaderNames()   ;
        while(e.hasMoreElements()){
            String name = (String) e.nextElement();
            String value = request.getHeader(name);
            System.out.println(name+" = "+value);
        }
        body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {
            }
            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }
    @Override
    public String getHeader(String name) {
        return super.getHeader(name);
    }
    @Override
    public Enumeration<String> getHeaderNames() {
        return super.getHeaderNames();
    }
    @Override
    public Enumeration<String> getHeaders(String name) {
        return super.getHeaders(name);
    }
}

再编写过滤器

package com.aliyun.core.config.filter; 
import com.aliyun.core.config.httpHelp.BodyReaderHttpServletRequestWrapper; 
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class HttpServletRequestReplacedFilter implements Filter {
    @Override
    public void destroy() {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest &&
                (null == request.getParameterMap() || request.getParameterMap().isEmpty())) {
            requestWrapper = new RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);
        }
        //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。
        // 在chain.doFiler方法中传递新的request对象
        if(requestWrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }
    @Override
    public void init(FilterConfig arg0) throws ServletException {
    }
}
相关文章
解决springboot添加拦截器之后只能获取一次流,并且@requestbody注解和表单方式都可以接到参
解决springboot添加拦截器之后只能获取一次流,并且@requestbody注解和表单方式都可以接到参
|
存储 JSON 数据格式
解决HttpServletRequest中的流无法重复读取的问题
解决HttpServletRequest中的流无法重复读取的问题
932 0
|
JSON 前端开发 Java
前端请求SpringBoot接口出现Required request body is missing
前端请求SpringBoot接口出现Required request body is missing
983 2
|
Java Maven Spring
Spring Boot - 手把手教小师妹自定义Spring Boot Starter
Spring Boot - 手把手教小师妹自定义Spring Boot Starter
557 0
|
JSON Java 应用服务中间件
HttpServletRequest核心方法以及获取请求参数
HttpServletRequest核心方法以及获取请求参数
3468 0
|
缓存 Ubuntu Linux
在Linux中,如何检查系统的CPU和内存使用情况?
在Linux中,如何检查系统的CPU和内存使用情况?
|
SpringCloudAlibaba Java Nacos
SpringBoot使用spring.config.import多种方式导入配置文件
`SpringBoot`从2.4.x版本开始支持了导入文件的方式来加载配置参数,与`spring.config.additional-location`不同的是不用提前设置而且支持导入的文件类型相对来说要丰富很多。
springboot使用文件流下载
springboot使用文件流下载
556 0
|
7月前
|
缓存 NoSQL Java
Java 防重放攻击实战:从原理到落地
重放攻击(Replay Attack)是一种常见的网络攻击手段,攻击者通过截取网络中传输的合法请求数据(如API调用参数、令牌等),然后在未授权的情况下重复发送该请求,以达到欺骗服务器、获取非法利益的目的。在Java开发中,重放攻击多发生在HTTP接口(尤其是RESTful API)、RPC调用、分布式系统通信等场景。要防御重放攻击,核心思路是让每个合法请求都具备“唯一性”和“时效性”,使攻击者截取的旧请求无法被服务器正常处理。
1256 1
|
存储 Java 中间件
SpringBoot+MDC链路追踪trace_id丢失
前言 trace_id是用来标识同一个请求的唯一标识,不管请求经过多少服务,都可以通过tracid排查对应模块的日志信息找到对应请求的一些细节,是排查问题的一个重要线索。
1365 0

热门文章

最新文章