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

时间:2018-06-19 15:20:35

标签: java spring-boot spring-security servlet-filters

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

  • POST localhost:8080 / v1 / auth / login ,其中包含电子邮件和密码以请求身份验证。响应是一个JWT令牌。

  • 获取 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 负责获取有关请求的示例信息,并且 LogDispatcherServlet 对于 localhost: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 的请求都将重定向到过滤器。

AppSecurity

@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:流已关闭

该如何避免这种情况? JWTLoginFilter也需要知道身份验证请求主体和LogDispatcherServlet,但是在 attemptAuthentication 中调用了 request.getInputStream()。还有没有那么简单的解决方案?

1 个答案:

答案 0 :(得分:0)

我认为您不需要更新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