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的原始输入流只能读取一次问题。

相关推荐
嘵奇4 小时前
基于Spring Boot实现文件秒传的完整方案
java·spring boot·后端
caihuayuan54 小时前
JavaScript数据结构与算法实战: 探秘Leetcode经典题目
java·大数据·spring boot·后端·课程设计
LUCIAZZZ5 小时前
KRaft面试思路引导
java·spring boot·算法·面试·kafka·操作系统·raft
码起来呗5 小时前
基于Spring Boot+微信小程序的智慧农蔬微团购平台-项目分享
spring boot·后端·微信小程序
云只上5 小时前
PDF转excel+json ,vue3+SpringBoot在线演示+附带源码
前端·javascript·spring boot·后端·pdf·json·excel
工业互联网专业7 小时前
基于springboot+vue的校园二手物品交易平台
java·vue.js·spring boot·毕业设计·源码·课程设计·校园二手物品交易平台
常年游走在bug的边缘7 小时前
基于spring boot 集成 deepseek 流式输出 的vue3使用指南
java·spring boot·后端·ai
李菠菜8 小时前
SpringBoot+Shiro同服务器多项目Cookie冲突解决方案
spring boot·后端·shiro
旅行的狮子9 小时前
二、在springboot 中使用 AIService
java·spring boot·langchain4j