Как избежать закрытия входного потока в dispatcherServlet после фильтра

Мне нужно сохранить всю информацию о запросах / ответах, которые отправляются для приложения, в виде статуса http, текущего времени, токена, URI запроса и т. д. Это API, и ресурсы:

  • POST локальный: 8080 / v1 / авторизация / логин с адресом электронной почты и паролем в запросе на аутентификацию. Ответ представляет собой токен JWT.

  • GET локальный: 8080 / v1 / auth / правила с токеном в заголовке запроса. Ответ - это тело с информацией о владельце токена, такой как адрес электронной почты и имя.

Для этого мой метод переопределяет метод 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 отлично работает для GET в локальный: 8080 / v1 / auth / правила.

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 / войти, перенаправляются для фильтра.

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 для сохранения того, что было запросом и каким будет ответ. Для /авторизоваться нет Контроллер, только 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
    }

}

Но для /авторизоваться это не работает. Когда ApiLog пытается получить тело запроса в getRequestPayload, я получаю java.io.IOException: поток закрыт

Что я могу сделать, чтобы этого избежать? JWTLoginFilter должен знать тело запроса для аутентификации и LogDispatcherServlet, но request.getInputStream () вызывается в попытка аутентификации. Есть ли другое менее сложное решение?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
8
0
1 602
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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.

Другие вопросы по теме