解决HttpServletRequest中的InputStream/getReader只能被读取一次的问题

发布于:2024-05-04 ⋅ 阅读:(37) ⋅ 点赞:(0)

一、事由

由于我们业务接口需要做签名校验,但因为是老系统了签名规则被放在了Body里而不是Header里面,但是我们不能在每个Controller层都手动去做签名校验,这样不是优雅的做法,然后我就写了一个AOP,在AOP中实现签名校验,之后Controller层就报以下错误:

java.lang.IllegalStateException: getReader() has already been called for this request
	at org.apache.catalina.connector.Request.getInputStream(Request.java:1074)
	at org.apache.catalina.connector.RequestFacade.getInputStream(RequestFacade.java:365)
	at javax.servlet.ServletRequestWrapper.getInputStream(ServletRequestWrapper.java:151)
	at javax.servlet.ServletRequestWrapper.getInputStream(ServletRequestWrapper.java:151)
	at javax.servlet.ServletRequestWrapper.getInputStream(ServletRequestWrapper.java:151)

二、分析源码

它告诉我们,getReader方法在这次请求中已经被调用了,可是为什么getReader已经被调用了呢,我们来分析一下源码,只有看懂了源码我们才能理解为什么,同时也为自己积累源码Debug的经验,这是我的Controller层代码(这里只是做了个实例读取流数据):

 @RequestMapping("/2")
    public String index2(HttpServletRequest request) throws IOException {
        return "请使用前端页面进行访问 " + request.getInputStream().read();
    }

判断条件
但是usingReader为什么会为true呢,我们点击看一下这个变量赋值的地方:

它一共有两个赋值的地方,第一个赋值为fase我们可以直接忽略,看一下true那个
在这里插入图片描述
在这里插入图片描述

三、解决办法

我看了这个getReader方法调用地方就是我在AOP中读取流中数据做签名校验的时候取的,那这咋办?AOP做了一层数据读取就导致Controller拿不到数据了,想了又想,我想到一个解决办法:

使用HttpServletRequest的setAttribute方法。

这个方法用于在HTTP请求直接共享数据,并且在请求完成之后自动销毁,完全不用担心生命周期问题,用它再适合不过了,我们可以写个请求过滤器,过滤所有的请求,将body数据设置到请求域当中:

/**
 * 用于处理读取多次请求体内容
 * 读取Token一次,解析数据一次
 * 
 * Created By XuanRan
 */
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class MultipleReadHttpRequestFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = request.getReader();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        request.setAttribute("CACHED_REQUEST_BODY", sb.toString());
        filterChain.doFilter(request, response);
    }
}

然后我这里使用的是SpringSecurity,我们把它添加到Filter链当中

@Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 注解标记允许匿名访问的url
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
        permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
        // 中间省略了一部分.....
        // 中间省略了一部分.....
        // 中间省略了一部分.....
        // 中间省略了一部分.....
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);


        // 添加请求体读取
        httpSecurity.addFilterAfter(multipleReadHttpRequestFilter, CorsFilter.class);
    }

然后我们就可以在Controller的HttpServletRequest的getAttribute根据CACHED_REQUEST_BODY这个key取对应的数据了,我这里为了方便,进一步写了一个AOP,自动的将数据完成转换:
在这里插入图片描述

然后就可以在Controller层直接拿到处理后的对象了
在这里插入图片描述

版权所有:XuanRan
未经书面授权,禁止转载


网站公告

今日签到

点亮在社区的每一天
去签到