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

相关推荐
昵称为空C8 小时前
SpringBoot3 http接口调用新方式RestClient + @HttpExchange像使用Feign一样调用
spring boot·后端
麦兜*17 小时前
MongoDB Atlas 云数据库实战:从零搭建全球多节点集群
java·数据库·spring boot·mongodb·spring·spring cloud
麦兜*17 小时前
MongoDB 在物联网(IoT)中的应用:海量时序数据处理方案
java·数据库·spring boot·物联网·mongodb·spring
汤姆yu17 小时前
基于springboot的毕业旅游一站式定制系统
spring boot·后端·旅游
计算机毕业设计木哥18 小时前
计算机毕设选题推荐:基于Java+SpringBoot物品租赁管理系统【源码+文档+调试】
java·vue.js·spring boot·mysql·spark·毕业设计·课程设计
hdsoft_huge1 天前
Java & Spring Boot常见异常全解析:原因、危害、处理与防范
java·开发语言·spring boot
AD钙奶-lalala1 天前
SpringBoot实现WebSocket服务端
spring boot·后端·websocket
毕设源码-朱学姐1 天前
【开题答辩全过程】以 4S店汽车维修保养管理系统为例,包含答辩的问题和答案
java·spring boot·汽车
BXCQ_xuan1 天前
软件工程实践二:Spring Boot 知识回顾
java·spring boot·后端
wuxuanok1 天前
SpringBoot -原理篇
java·spring boot·spring