Spring Boot的request输入流读取一次即关闭问题解决方案

  • 问题背景 :HTTP 请求的 InputStream 是单向的,默认只能被读取一次。一旦调用 request.getInputStream() 或 request.getReader() 读取数据后,后续再尝试读取会抛出 IllegalStateException。
  • 场景需求:在微信支付回调场景中,需要先验证签名(读取请求体),再解密数据(需要再次读取请求体)。若直接使用原始请求对象,第二次读取会失败。
  • 解决方案:自定义一个包装器,缓存request中输入流的数据,后续可以从包装器中直接获取输入流的数据,而不是直接读取原始流,避免产生二次读取导致抛出异常。
java 复制代码
import org.apache.commons.io.IOUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

public class VerdureRequestWrapper extends HttpServletRequestWrapper {
    HttpServletRequest orgRequest = null;
    private byte[] bytes;
    private WrappedServletInputStream wrappedServletInputStream;

    public VerdureRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.orgRequest = request;
        //读取输入流的请求参数,保存到bytes中
        bytes = IOUtils.toByteArray(request.getInputStream());
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        this.wrappedServletInputStream = new WrappedServletInputStream(byteArrayInputStream);

        //把post参数重新写入请求流
        reWriteInputStream();
    }

    public void setRequestParams(String json) {
        wrappedServletInputStream.setStream(new ByteArrayInputStream(json.getBytes()));
    }

    /**
     * 把参数重新写进请求里
     */
    public void reWriteInputStream() {
        wrappedServletInputStream.setStream(new ByteArrayInputStream(bytes != null ? bytes : new byte[0]));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return wrappedServletInputStream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(wrappedServletInputStream));
    }

    /**
     * 获取post参数
     */
    public String getRequestParams() throws IOException {
        return new String(bytes, this.getCharacterEncoding());
    }

    //清洗参数,防止xss注入
    public String[] getParameterValues(String parameter) {
        String[] values = super.getParameterValues(parameter);
        if (values == null) {
            return null;
        }
        int count = values.length;
        String[] encodedValues = new String[count];
        for (int i = 0; i < count; i++) {
            encodedValues[i] = xssEncode(values[i]);
        }
        return encodedValues;
    }

    public String getParameter(String name) {
        String value = super.getParameter(xssEncode(name));
        if (value != null) {
            value = xssEncode(value);
        }
        return value;
    }

    public String getHeader(String name) {
        String value = super.getHeader(xssEncode(name));
        if (value != null) {
            value = xssEncode(value);
        }
        return value;
    }

    private static String xssEncode(String s) {
        return s;
    }

    public HttpServletRequest getOrgRequest() {
        return this.orgRequest;
    }

    private class WrappedServletInputStream extends ServletInputStream {
        public void setStream(InputStream stream) {
            this.stream = stream;
        }

        private InputStream stream;

        public WrappedServletInputStream(InputStream stream) {
            this.stream = stream;
        }

        public int read() throws IOException {
            return stream.read();
        }

        public boolean isFinished() {
            return true;
        }

        public boolean isReady() {
            return true;
        }

        public void setReadListener(ReadListener readListener) {
        }
    }
}
  • 使用示例:
java 复制代码
    private JSONObject wxCallbackDecrypt(HttpServletRequest request) throws IOException {
        // 微信支付配置信息
        String apiV3Key = wxPayConfig.getApiV3Key();
        log.info("apiV3Key:=======" + apiV3Key);
        AesUtil.checkKey(apiV3Key.getBytes());
        VerdureRequestWrapper requestWrapper = new VerdureRequestWrapper(request);
        log.info(requestWrapper.getRequestParams());
        PayCallbackWx callbackWx = JSONObject.parseObject(requestWrapper.getRequestParams(), PayCallbackWx.class);
        try {
            if (verifySignature(request, requestWrapper)) {
                PayCallbackWxResource resource = callbackWx.getResource();
                String associatedData = resource.getAssociated_data();
                String nonce = resource.getNonce();
                String ciphertext = resource.getCiphertext();
                String decrypt = AesUtil.decryptToString(apiV3Key.getBytes(), associatedData.getBytes(), nonce.getBytes(), ciphertext);
                JSONObject result = JSONObject.parseObject(decrypt);
                return result;
            }
        } catch (Exception e) {
            log.info("微信回调信息解密失败:{}", e.getMessage());
        }
        return null;
    }

如此便可有效解决Spring Boot框架中的HttpServeletRequest的原始输入流只能读取一次问题。

相关推荐
马剑威(威哥爱编程)3 分钟前
【AI全栈后端12-08】Spring Boot 用 MCP 统一接入内部系统:让 AI 接一次,全公司复用
java·人工智能·spring boot
顽疲9 分钟前
从零用 Java 实现小红书 SpringBoot Vue UniApp(23)笔记话题多对多:一条笔记进多个话题池
java·vue.js·spring boot
做个文艺程序员22 分钟前
MQ第02篇:RabbitMQ快速上手教程:AMQP模型详解+Spring Boot整合实战(附完整代码)
spring boot·消息队列·rabbitmq·java-rabbitmq·amqp
RuoyiOffice23 分钟前
SpringBoot3 企业定时结算架构:考勤日结、合同到期、库存预警与财务结转如何可靠执行
spring boot·xxl-job·定时任务·分布式任务·spring boot 3·幂等设计·ruoyi office
天空鸟_时光不老1 小时前
13-亮点提炼:把技术说辞翻译成业务价值
java·spring boot·spring·spring cloud·mybatis
小蒜学长1 小时前
基于SpringBoot+Vue的租房管理系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·租房管理
洋就在江州1 小时前
gitlab-cicd 离线集成——springboot-cicd (非docker形式,shell形式)
java·spring boot·后端·ci/cd·gitlab·gitlab-runner
令狐少侠20111 小时前
centos7 下,使用docker 部署IOT 调试平台,ThingsBoard 并完美启动
java·spring boot·iot
省钱兄--zs3 小时前
24小时自助健身房系统软件开发实战:从架构设计到部署全指南
java·数据仓库·spring boot·系统架构·需求分析
IT研究室4 小时前
最新计算机毕业设计选题推荐-基于spring boot的智慧教学资源管理与互动交流平台-网站-文档指导-Java-springboot
java·spring boot·课程设计