如何避免在过滤器后在 dispatcherServlet 中关闭输入流

Posted

技术标签:

【中文标题】如何避免在过滤器后在 dispatcherServlet 中关闭输入流【英文标题】:How avoid input stream closed in dispatcherServlet after a filter 【发布时间】:2018-11-28 14:56:15 【问题描述】:

我需要将有关为应用程序发送的请求/响应的所有信息保存为 http 状态、当前时间、令牌、请求 URI 等。它是一个 API,资源是:

POST localhost:8080/v1/auth/login 在请求身份验证时使用电子邮件和密码。响应是一个 JWT 令牌。

GET localhost:8080/v1/auth/rules 在请求的标头中带有令牌。响应是一个正文,其中包含有关令牌所有者的信息,例如电子邮件和姓名。

为此,我的方法重写了 doDispatch 方法:

LogDispatcherServlet

@Component
public class LogDispatcherServlet extends DispatcherServlet 

    @Autowired
    private LogRepository logRepository;

    private static final Logger logger = LoggerFactory.getLogger(LogDispatcherServlet.class);

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception 
        if (!(request instanceof ContentCachingRequestWrapper)) 
            request = new ContentCachingRequestWrapper(request);
        
        if (!(response instanceof ContentCachingResponseWrapper)) 
            response = new ContentCachingResponseWrapper(response);
        
        HandlerExecutionChain handler = getHandler(request);

        try 
            super.doDispatch(request, response);
         finally 
            try 
                ApiLog log = ApiLog.build(request, response, handler, null);
                logRepository.save(log);
                updateResponse(response);
             catch (UncheckedIOException e) 
                logger.error("UncheckedIOException", e);
             catch (Exception e) 
                logger.error("an error in auth", e);
            
        
    

    private void updateResponse(HttpServletResponse response) throws IOException 
        ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        responseWrapper.copyBodyToResponse();
    


ApiLog.build 负责获取有关请求的示例信息,LogDispatcherServletlocalhost:8080/v1/auth/rules 中的 GET 工作正常 em>。

ApiLog

public static ApiLog build(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain handler, Authentication auth) 
        ApiLog log = new ApiLog();
        log.setHttpStatus(response.getStatus());
        log.setHttpMethod(request.getMethod());
        log.setPath(request.getRequestURI());
        log.setClientIp(request.getRemoteAddr());
        try 
            if (request.getReader() != null) 
                log.setBodyRequest(getRequestPayload(request));
            
         catch (IOException e) 
            e.printStackTrace();
        
        if (handler != null) 
            log.setJavaMethod(handler.toString());
        
        if (request.getHeader("Authorization") != null) 
            log.setToken(request.getHeader("Authorization"));
         else if (response.getHeader("Authorization") != null) 
            log.setToken(response.getHeader("Authorization"));
        
        log.setResponse(getResponsePayload(response));
        log.setCreated(Instant.now());
        logger.debug(log.toString());
        return log;
    

    @NotNull
    private static String getRequestPayload(HttpServletRequest request) 
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        try 
            return wrapper
                    .getReader()
                    .lines()
                    .collect(Collectors.joining(System.lineSeparator()));
         catch (IOException e) 
            e.printStackTrace();
        
        return "";
    

    @NotNull
    private static String getResponsePayload(HttpServletResponse responseToCache) 
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(responseToCache, ContentCachingResponseWrapper.class);
        if (wrapper != null) 
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) 
                int length = Math.min(buf.length, 5120);
                try 
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                 catch (UnsupportedEncodingException ex) 
                    logger.error("An error occurred when tried to logging request/response");
                
            
        
        return "";
    

我最大的问题是:我正在使用 Spring Security 生成 JWT 令牌,因此发送到 /v1/auth/login 的所有请求都被重定向到过滤器。

应用安全

@Configuration
@EnableWebSecurity
public class AppSecurity extends WebSecurityConfigurerAdapter 

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception 
        httpSecurity.csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .and()
                .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                        UsernamePasswordAuthenticationFilter.class);
    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(customUserDetailsService);
    


认证成功后,过滤器必须调用 LogDispatcherServlet 来持久化请求和响应。 /login 没有 Controller,只有 JWTLoginFilter。

JWTLoginFilter

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter 

    @Autowired
    JWTLoginFilter(String url, AuthenticationManager authManager) 
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException 

        AccountCredentials credentials = new ObjectMapper()
                .readValue(request.getInputStream(), AccountCredentials.class);

        return getAuthenticationManager().authenticate(
                new UsernamePasswordAuthenticationToken(
                        credentials.getUsername(),
                        Md5.getHash(credentials.getPassword()),
                        Collections.emptyList()
                )
        );
    

    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain,
            Authentication auth) throws IOException, ServletException 

        TokenAuthenticationService.addAuthentication(response, auth.getName());
        //Must call LogDispatcherServlet
        filterChain.doFilter(request, response);
    

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException 
        super.unsuccessfulAuthentication(request, response, failed);

        //Must call LogDispatcherServlet
    


但它不适用于 /login。当 ApiLog 尝试在 getRequestPayload 中获取请求正文时,我得到一个 java.io.IOException: Stream closed

我能做些什么来避免这种情况? JWTLoginFilter 需要知道身份验证的请求正文和 LogDispatcherServlet,但 request.getInputStream() 是在 attemptAuthentication 中调用的。是否有另一种不那么复杂的解决方案?

【问题讨论】:

【参考方案1】:

我认为您不需要更新 Spring 的 DispatcherServlet。我将创建一个过滤器(在链中的第一个位置),将原始请求/响应包装在允许缓存的对象中(例如ContentCachingRequestWrapper / ContentCachingResponseWrapper)。

在您的过滤器中,您只需要执行以下操作:

doFilter(chain, req, res) 

   ServletRequest wrappedRequest = ...
   ServletResponse wrappedResponse = ...   

   chain.doFilter(wrappedRequest, wrappedResponse);


你可以注册一个HandlerInterceptor

public class YourHandlerIntercepter extends HandlerInterceptorAdapter 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        if (handler instanceof HandlerMethod) 
            HandlerMethod handlerMethod = (HandlerMethod) 

            ApiLog log = ApiLog.build(request, response, handler, null);
            logRepository.save(log);
        
        return true;
    

您最终需要更改您的 ApiLog 方法,使其使用 MethodHandler 而不是 HandlerExecutionChain

【讨论】:

以上是关于如何避免在过滤器后在 dispatcherServlet 中关闭输入流的主要内容,如果未能解决你的问题,请参考以下文章

应用过滤器后在 Extjs 网格列中添加过滤器图标一次

java.lang.IllegalStateException:未找到 WebApplicationContext:未注册 ContextLoaderListener 或 DispatcherServ

过滤后在熊猫中选择前一行的语法

Plotly-Dash:- 文件上传后在 plotly dash 中进行多列过滤

如何避免冗余并将过滤器应用于字符串组合

找到组合后在 VBA 中过滤