spring boot3 HttpServletRequest inputStream 只能读取一次问题,及重新构造请求 并将RSA加密的参数解密 验证 签名 最后再将解密后的参数重新放入请求

该博客介绍了如何通过继承HttpServletRequestWrapper来重构请求,使输入流可重复读取。同时展示了一个过滤器的实现,用于拦截特定请求,解密并验证参数,防止重复提交。在过滤器中,首先获取请求的body,然后使用RSA解密并验证签名,确保请求的安全性。

在 Spring Boot 中,HttpServletRequest 的 InputStream 默认只能读取一次。原因是底层的流在读取后,内容不会被缓存,导致流在后续处理时无法再次读取。

要解决该问题,同时满足 RSA 解密参数、验证签名,并重新构造请求 的需求,可以按照以下步骤实现:

实现方案

1. 核心逻辑步骤

拦截请求:
使用 Filter 或 HandlerInterceptor 捕获请求。

读取请求内容并缓存:
将 HttpServletRequest 包装为可重复读取的请求(例如,ContentCachingRequestWrapper 或自定义包装类)。

处理解密和签名验证:
使用 RSA 私钥解密参数。
验签逻辑验证请求数据的完整性。

重构请求并传递:
创建一个新的 HttpServletRequestWrapper,将解密后的参数重新注入。
将新构造的请求传递给后续的处理逻辑。

2. 代码实现

自定义过滤器

import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.stream.Collectors;

@Component
public class DecryptFilter implements Filter {
   
   

    // 配置 RSA 私钥
    private final PrivateKey privateKey = YourPrivateKeyProvider.getPrivateKey();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
   
   
        if (request instanceof HttpServletRequest) {
   
   
            CachedBodyHttpServletRequest wrappedRequest = new CachedBodyHttpServletRequest((HttpServletRequest) request);

            // 读取请求体
            String body = wrappedRequest.getCachedBody();
            System.out.println("Original Request Body: " + body);

            // 解密请求参数
            String decryptedBody = decryptRSA(body);
            
<think>我们正在讨论网关层处理请求时的一个关键问题HttpServletRequest的输入流只能读取一次,而网关需要解密请求体(意味着必须读取输入流),然后转发修改后的请求。这看起来是矛盾的,因为一旦读取了输入流,就无法再次读取以用于转发。下面我将详细解释这个问题以及解决方案。 首先,回顾一下HttpServletRequest输入流只能读取一次的原因: 1. 底层协议:HTTP请求体是通过TCP流传输的,数据按顺序到达,读取后指针向前移动,无法回退。 2. Servlet容器实现:输入流绑定到操作系统的网络缓冲区,读取后缓冲区内容会被释放,无法再次读取。 在网关层,我们通常需要做以下操作: - 读取请求体(例如,进行解密) - 修改请求体解密后的数据) - 将修改后的请求体转发到下游服务 矛盾点:当我们第一次读取请求体进行解密时,输入流已经被消耗,无法再次读取,那么转发时就没有数据可发送了。 解决方案的核心思想:在第一次读取请求体后,将请求体内容缓存起来,然后提供一个可以多次读取的缓存流。这样,在网关层我们可以先读取解密,然后用解密后的内容重新构建一个新的请求体,再转发。 具体实现方案(以Spring Cloud Gateway为例,因为它是基于WebFlux的响应式编程模型,处理方式与传统Servlet容器不同): 1. 在Spring Cloud Gateway中,请求体被封装成Flux<DataBuffer>,我们可以订阅一次,将数据缓存到内存或磁盘,然后基于缓存的数据进行解密。 2. 由于WebFlux是非阻塞的,我们需要小心处理:避免阻塞,且注意内存使用(大文件可能导致内存溢出)。 而对于传统的基于Servlet的网关(如Zuul1),则需要使用包装类(如HttpServletRequestWrapper)来缓存请求体。 步骤详解(传统Servlet网关): 1. 创建一个过滤器(Filter)来拦截请求。 2. 在过滤器中,使用HttpServletRequestWrapper包装原始的HttpServletRequest3. 在包装类中,覆盖getInputStream()和getReader()方法,使其返回一个缓存了请求体数据的流。 4. 这样,在过滤器中,我们可以先读取包装后的请求流(即缓存的数据),进行解密,然后修改请求体。 5. 将修改后的请求体设置到新的请求对象中,然后转发。 注意:缓存请求体可能会带来内存消耗,特别是大文件上传时。因此,需要考虑设置缓存大小限制,或者使用磁盘缓存。 在Spring Cloud Gateway(WebFlux)中的处理: 由于WebFlux的请求体是反应式流(Reactive Stream),我们可以通过缓存和修改数据缓冲(DataBuffer)来实现。例如,使用`ModifyRequestBodyGatewayFilterFactory`来修改请求体。 示例代码(传统Servlet过滤器): ```java public class DecryptFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 包装原始请求 RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) request); // 从包装后的请求读取请求体(此时已经被缓存) String requestBody = requestWrapper.getRequestBody(); // 解密 String decryptedBody = decrypt(requestBody); // 创建新请求对象(这里可以用自定义的包装类,将解密后的内容放入新的流中) HttpServletRequest newRequest = new DecryptedRequestWrapper((HttpServletRequest) request, decryptedBody); chain.doFilter(newRequest, response); } } // 请求包装类,缓存请求体 class RequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); // 读取原始请求体缓存 this.body = IOUtils.toByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() { return new CachedServletInputStream(this.body); } // 提供一个获取请求体字符串的方法 public String getRequestBody() throws UnsupportedEncodingException { return new String(body, getCharacterEncoding()); } } // 解密后的请求包装类,将解密后的内容作为新的请求体 class DecryptedRequestWrapper extends HttpServletRequestWrapper { private final byte[] decryptedBody; public DecryptedRequestWrapper(HttpServletRequest request, String decryptedBody) { super(request); this.decryptedBody = decryptedBody.getBytes(StandardCharsets.UTF_8); } @Override public ServletInputStream getInputStream() { return new CachedServletInputStream(this.decryptedBody); } } // 缓存流 class CachedServletInputStream extends ServletInputStream { private final ByteArrayInputStream buffer; public CachedServletInputStream(byte[] contents) { this.buffer = new ByteArrayInputStream(contents); } @Override public int read() { return buffer.read(); } @Override public boolean isFinished() { return buffer.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { throw new UnsupportedOperationException(); } } ``` 在Spring Cloud Gateway中,可以使用以下方式: ```java @Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("decrypt_route", r -> r.path("/api/**") .filters(f -> f.modifyRequestBody(String.class, String.class, (exchange, originalRequestBody) -> { // 解密 String decryptedBody = decrypt(originalRequestBody); return Mono.just(decryptedBody); })) .uri("http://downstream-service")) .build(); } ``` 总结: 1. 网关层解密请求体HttpServletRequest只能读取一次不矛盾,因为我们可以通过缓存请求体来解决。 2. 特殊处理是必须的,即使用请求包装类(缓存原始请求体构建一个新的请求对象(携带解密后的请求体)进行转发。 3. 对于不同的网关技术,实现方式不同: - 传统Servlet容器(如Zuul1):使用过滤器+HttpServletRequestWrapper缓存请求体。 - Spring Cloud Gateway(WebFlux):使用修改请求体的过滤器,基于反应式编程缓存和修改请求体。 注意事项: - 内存管理:大文件请求体可能导致内存溢出,可以考虑使用磁盘缓存或流式解密(边读边解密,但解密通常需要完整数据)。 - 性能:加解密操作是CPU密集型,可能成为性能瓶颈。 因此,在网关层处理请求体时,必须考虑使用缓存机制,根据网关类型选择相应的实现方式。 </think>### 网关层解密处理与HttpServletRequest单次读取矛盾的解决方案 网关层接收请求后做解密处理再转发到应用层,**确实与HttpServletRequest输入流只能读取一次的特性相矛盾**,但通过以下技术手段可以解决: #### 一、矛盾核心分析 1. **解密操作必须读取请求体** 解密操作需要完整获取请求体数据才能处理,这必然消耗输入流: $$ \text{网关读取} \xrightarrow{\text{消耗}} \text{HttpServletRequest输入流} $$ 2. **转发需要原始请求流** 转发到应用层时,需重新构造完整HTTP请求(含请求体),但原生输入流已被消耗: $$ \text{容器转发} \xrightarrow{\text{需未读流}} \text{目标服务} $$ 3. **直接矛盾点** ```mermaid graph LR A[客户端请求] -->|原始数据| B(网关) B -->|读取解密| C{输入流已消耗} C -->|无法转发| D[下游服务] ``` #### 二、解决方案:缓存与流重构 通过**请求体重构技术**解决矛盾,核心原理: ```mermaid sequenceDiagram participant Client as 客户端 participant Gateway as 网关 participant Cache as 缓存层 participant App as 应用服务 Client->>Gateway: 发送加密请求 Gateway->>Cache: 读取缓存请求体 Gateway->>Gateway: 解密缓存数据 Gateway->>App: 转发重构后的请求 Note left of Gateway: 新请求携带解密后数据 ``` 具体实现方案: ##### 方案1:内存缓存(适合小数据) ```java // 网关过滤器示例 public class DecryptFilter implements GatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1. 读取缓存请求体 byte[] encryptedData = exchange.getRequest().getBody().blockFirst(); // 2. 执行解密操作 byte[] decryptedData = decrypt(encryptedData); // 3. 重构请求 ServerHttpRequest newRequest = exchange.getRequest().mutate() .body(DataBufferUtils.join(DataBufferFactory.allocateBuffer() .write(decryptedData))) .build(); // 4. 转发新请求 return chain.filter(exchange.mutate().request(newRequest).build()); } } ``` ##### 方案2:磁盘缓存(适合大文件) ```java // 使用临时文件缓存 Path tempFile = Files.createTempFile("reqbody", ".tmp"); try (InputStream is = request.getInputStream(); OutputStream os = Files.newOutputStream(tempFile)) { IOUtils.copy(is, os); // 请求体写入磁盘 } // 解密文件内容 decryptFile(tempFile); // 转发时使用FileInputStream forwardRequest(new FileInputStream(tempFile)); ``` #### 三、关键技术实现 1. **请求包装器模式** 创建`HttpServletRequestWrapper`扩展类,覆盖输入流获取方法: ```java public class CachedBodyRequestWrapper extends HttpServletRequestWrapper { private final byte[] cachedBody; public CachedBodyRequestWrapper(HttpServletRequest request) { super(request); this.cachedBody = IOUtils.toByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() { return new CachedBodyServletInputStream(this.cachedBody); } } ``` 2. **响应式编程处理(WebFlux)** 在Spring Cloud Gateway中使用`ModifyRequestBodyGatewayFilterFactory`: ```java @Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("decrypt_route", r -> r.path("/api/**") .filters(f -> f.modifyRequestBody( byte[].class, byte[].class, (exchange, body) -> Mono.just(decrypt(body))) ) .uri("http://backend-service")) .build(); } ``` #### 四、性能与安全优化 1. **内存控制** $$ \text{阈值控制} : \text{maxInMemorySize} = 256\text{KB} $$ 超过阈值自动切换磁盘缓存 2. **流式解密** 边读取解密的管道处理: ```java PipedInputStream in = new PipedInputStream(); PipedOutputStream out = new PipedOutputStream(in); new Thread(() -> { try (InputStream src = request.getInputStream()) { decryptStream(src, out); // 流式解密 } }).start(); forwardRequest(in); // 转发解密流 ``` 3. **安全增强** - 解密后立即清除内存中的原始密文 - 磁盘缓存使用`SecureRandom`命名临时文件 - 设置文件权限`600`(仅所有者可读写) ### 总结 1. **矛盾本质**:解密需消耗请求流 vs 转发需完整请求流 2. **解决核心**:请求体缓存与重构技术 3. **方案选择**: - 内存缓存:适合<10MB请求体 - 磁盘缓存:适合大文件/视频流 - 流式处理:平衡内存与延迟 在网关层实现解密转发时,必须通过请求体重构技术绕过Servlet输入流单次读取限制。Spring Cloud Gateway的响应式模型(WebFlux)为此提供了更优雅的解决方案[^1][^2]。 --- ### 相关问题 1. 如何评估网关层解密对系统延迟的影响? 2. 网关解密和微服务本地解密的性能对比? 3. 流式解密在HTTPS场景下的实现挑战? 4. Servlet容器与WebFlux在处理大文件请求时的资源消耗差异?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十方来财

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值