过滤器或拦截器读取请求中流的数据后导致后续控制器读取报错问题分析

博客围绕Java中流读取问题展开,指出通过过滤拦截器读取流时,IO流关闭只能读一次,不关闭后续控制器也可能读不到数据。介绍了利用reset方法重置读取位置,但并非所有流都适用。提出包装request将流重新写入供后续读取的解决办法,还提及springcloud项目中拦截器读取流的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

错误如图所示,提示流已经关闭,因为如果通过过滤拦截器读取流的话,IO流关闭只能读取一次, 即使不关闭的话,流有个read标志位,后续控制器会从read标志位开始读,读过流之后就读取不到数据了,除非利用void reset()方法,把pos位置位开始,重新读,但是不是任何流都可以使用,所以我们写通用的方法时候,读取完流中数据之后,需要进行包装request,将流重新写入,供后续控制器读取。

网络上解释如下:那是因为流对应的是数据,数据放在内存中,有的是部分放在内存中。read 一次标记一次当前位置(mark position),第二次read就从标记位置继续读(从内存中copy)数据。 所以这就是为什么读了一次第二次是空了。 怎么让它不为空呢?只要inputstream 中的pos 变成0就可以重写读取当前内存中的数据。javaAPI中有一个方法public void reset() 这个方法就是可以重置pos为起始位置,但是不是所有的IO读取流都可以调用该方法!ServletInputStream是不能调用reset方法,这就导致了只能调用一次getInputStream()。

包装request请求代码如下:

public class MyRequest extends HttpServletRequestWrapper {

private String body="";
public MyRequest(HttpServletRequest request) throws IOException {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
throw ex;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
throw ex;
}
}
}
body = stringBuilder.toString();
}

public String getBody(){
return this.body;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

@Override
public ServletInputStream getInputStream() throws IOException {

final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());

return new ServletInputStream() {

@Override
public int read() throws IOException {
return bais.read();
}

@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {

}
};
}
}
过滤器处理如下图:

 

拦截器处理如下:

Controller层正常获取post参数即可,利用requestBody等等。

PS:在springcloud项目中,拦截器不能正常从request中读取流,暂未发现是我自己的例子问题还是跟cloud项目相关。

 



转载于:https://www.cnblogs.com/tnt-33/p/11052295.html

<think>好的,我现在需要解答用户的问题:“过滤器拦截器读取请求中的token会消费请求体嘛”。首先,我得理解这个问题的核心是什么。用户想知道在过滤器拦截器读取请求中的token时,是否会导致请求体被消费,也就是后续的处理中无法再次读取请求体。 接下来,我需要回忆一下HTTP请求的结构。HTTP请求通常由请求头和请求体组成。请求头包含诸如Content-Type、Authorization(可能携带token)等信息,而请求体一般用于POST、PUT等方法传输数据。 然后,关于过滤器拦截器同的框架可能有同的实现。比如,Java中的Servlet过滤器和Spring的拦截器。需要明确它们的处理流程。在Servlet过滤器中,请求体是通过ServletRequest对象获取的,而一旦读取了输入流,就会消费掉,导致后续无法再次读取。同样,Spring的拦截器可能也会有类似的问题,因为底层也是基于Servlet的机制。 但是,用户提到的是读取token,而token通常存放在请求头里,比如Authorization头。这种情况下,读取请求头的信息会触及请求体的输入流,因此会消费请求体。这时候应该影响后续请求体读取过,如果token是放在请求体中的,比如某些API设计可能将token放在JSON数据里,这时候读取请求体就会消费输入流。这时候如果在过滤器拦截器读取请求体中的token,后续处理就无法再次读取请求体导致错误。 接下来需要考虑解决方案。如果必须从请求体获取token,可以使用包装请求对象的方法,比如HttpServletRequestWrapper,缓存请求体的内容,使得可以多次读取。这样在过滤器读取后,后续处理还能获取请求体数据。 还需要验证一下同框架中的行为是否一致。例如,在Spring MVC中,使用@RequestBody注解时,输入流只能读取一次,如果过滤器已经读取过,这里会抛出异常。因此,正确的做法是使用内容缓存包装器。 总结起来,回答需要分情况讨论:token在请求头还是请求体读取方式的导致是否消费请求体,以及对应的解决方法。同时,要确保回答结构清晰,逐步引导用户理解问题所在和解决方案。</think>在过滤器拦截器读取请求中的 **Token** 是否会消费请求体,取决于 **Token 的存储位置**和**读取方式**。以下是分步分析: --- ### 1. **Token 的常见存储位置** - **请求头**(如 `Authorization` 头):这是最常见的方式,例如 `Bearer Token`。 - **请求参数**:如 URL 参数表单参数。 - **请求体**:例如 JSON XML 格式的请求体中包含 Token。 --- ### 2. **读取 Token 是否消费请求体** #### 情况 1:Token 在请求 URL 参数中 - **会消费请求体** 如果通过以下方式获取 Token: ```java // 从请求读取 String token = request.getHeader("Authorization"); // 从 URL 参数读取 String token = request.getParameter("token"); ``` **原因**:HTTP 请求头和 URL 参数与请求体是独立的部分,读取它们会触发请求体的解析。 #### 情况 2:Token 在请求体中(如 JSON) - **会消费请求体** 如果通过以下方式获取 Token: ```java // 从输入流读取请求体 InputStream inputStream = request.getInputStream(); String body = IOUtils.toString(inputStream, StandardCharsets.UTF_8); ``` **原因**:HTTP 请求的输入流(`InputStream`)是单向的,一旦被读取后续代码无法再次读取。 **后果**:后续 Controller 业务逻辑中使用 `@RequestBody` 注解时会抛出异常(如 `Stream closed`)。 --- ### 3. **解决方案:避免请求体被消费** 如果必须从请求体读取 Token,可通过以下方法复用请求体: #### 方法 1:使用包装类缓存请求体 - 自定义 `HttpServletRequestWrapper`,将请求体内容缓存到内存: ```java public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { private byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); this.cachedBody = IOUtils.toByteArray(request.getInputStream()); } @Override public ServletInputStream getInputStream() { return new CachedBodyServletInputStream(this.cachedBody); } } ``` - **过滤器/拦截器中**:将原始请求替换为包装后的请求: ```java CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request); chain.doFilter(cachedRequest, response); // 传递包装后的请求 ``` #### 方法 2:框架提供的工具 - 在 Spring 中,可使用 `ContentCachingRequestWrapper`: ```java ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request); String body = new String(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding()); chain.doFilter(wrapper, response); ``` --- ### 4. **总结** | Token 位置 | 是否消费请求体 | 解决方案 | |----------------|--------------|-----------------------| | 请求头/URL 参数 | 否 | 无需处理 | | 请求体 | 是 | 使用请求体缓存包装类 | **关键原则**: - 优先将 Token 放在请求头中(符合 RESTful 设计规范)。 - 避免在过滤器/拦截器中直接读取请求体输入流,除非使用缓存机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值