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

相关推荐
Bs_MoneyMagnet10 分钟前
基于springboot+vue的个人健康管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·vue3·springboot3·计算机毕业设计
Bs_MoneyMagnet3 小时前
基于springboot+vue的心理咨询预约与随访平台的设计与实现 源码+文档
vue.js·spring boot·后端·spring·毕业设计·旅游·计算机毕业设计
陌シ未央ゞ4 小时前
基于BM25算法和RRF实现的混合索引(java版)
人工智能·spring boot·后端·算法
user_admin_god7 小时前
第 01 篇:OpenAI 兼容 API 初探 —— 用 curl 跑通第一次对话
java·人工智能·spring boot·语言模型·devops
小辰爱喝汤9 小时前
基于智能定价的 校园二手交易平台-计算机毕业设计
spring boot·毕业设计·课程设计
RuoyiOffice10 小时前
SpringBoot3+Vue3 库存盘点:账面和实盘对不上,差异单怎么走审批再调账
spring boot·vue3·erp·spring boot 3·库存盘点·盘盈盘亏
行者-全栈开发12 小时前
Spring Boot 接入 MaxKey 单点登录:6 个内部系统,一次登录全通行
spring boot·iam·oauth2·统一认证·oidc·sso 单点登录·maxkey
user_admin_god13 小时前
第 09 篇:实践一 —— 文本摘要(Map-Reduce 分块)
java·人工智能·spring boot·语言模型
vx_BS8133013 小时前
【项目编号:project49759】Spring Boot 宠物综合服务平台:商城、预约、咨询、活动与健康档案的一体化实现
java·spring boot·eclipse·tomcat·intellij-idea
user_admin_god13 小时前
第 05 篇:结构化输出 —— 让模型稳定返回 JSON
java·人工智能·spring boot·语言模型