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

相关推荐
SuniaWang8 小时前
《Spring AI + 大模型全栈实战》学习手册系列 · 专题六:《Vue3 前端开发实战:打造企业级 RAG 问答界面》
java·前端·人工智能·spring boot·后端·spring·架构
韩立学长8 小时前
Springboot校园跑腿业务系统0b7amk02(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
sheji34168 小时前
【开题答辩全过程】以 基于springboot的扶贫系统为例,包含答辩的问题和答案
java·spring boot·后端
Meepo_haha10 小时前
Spring Boot 条件注解:@ConditionalOnProperty 完全解析
java·spring boot·后端
sheji341610 小时前
【开题答辩全过程】以 基于springboot的房屋租赁系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
xiaohe0712 小时前
Spring Boot 各种事务操作实战(自动回滚、手动回滚、部分回滚)
java·数据库·spring boot
gechunlian8812 小时前
Spring Boot中的404错误:原因、影响及处理策略
java·spring boot·后端
givemeacar13 小时前
Spring Boot中集成MyBatis操作数据库详细教程
数据库·spring boot·mybatis
Mr.456714 小时前
Spring Boot集成Redis:单机、哨兵、集群三种模式统一配置实战
spring boot·redis·bootstrap
lay_liu14 小时前
Spring Boot 自动配置
java·spring boot·后端