Spring Boot Filter. Введение в фильтрацию запросов

Привет, меня зовут Николай Пискунов,  я руководитель направления Big Data и автор медиа вАЙТИ. В этой статье поговорим о фильтрации запросов.

e0767f107a7f97574f1a7881b230e73e.png

Примером может быть авторизация: Filter проверит, прошел ли пользователь аутентификацию до отправки запроса. Если нет, то можно отказать ему в доступе к ресурсу. Еще один пример — логирование: с помощью Filter вы можете записывать информацию о каждом запросе и ответе, что может быть полезно для отладки или мониторинга.

В предыдущей статье мы создали аспект для логирования запросов через аннотации. Сегодня рассмотрим, как осуществить то же самое с использованием Spring Boot Filter.

Как создать собственный фильтр в Spring Boot Filter

Для создания своего фильтра нужно реализовать интерфейс `javax.servlet.Filter` и переопределить метод `doFilter`. Внутри этого метода вы можете получить доступ к запросу и ответу через объекты `ServletRequest` и `ServletResponse`. Затем добавьте свой фильтр в файл конфигурации `web.xml` или используя JavaConfig.

Простой пример логирования запросов с использованием фильтров:

@Slf4j
@Component

public class RequestLoggingFilter implements Filter {
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, 
              FilterChain chain) throws IOException, ServletException {
       HttpServletRequest httpRequest = (HttpServletRequest) request;
       HttpServletResponse httpResponse = (HttpServletResponse) response;
       // Запись информации о запросе
       logRequest(httpRequest);
       long startTime = System.currentTimeMillis();
       try {
           // Продолжение цепочки фильтров
           chain.doFilter(request, response);
       } finally {
           long duration = System.currentTimeMillis() - startTime;
           // Запись информации об ответе и времени выполнения
           logResponse(httpRequest, httpResponse, duration);
       }
   }
   private void logRequest(HttpServletRequest request) {
       String requestURI = request.getRequestURI();
       String method = request.getMethod();
       log.info("Запрос: {} - {}", method, requestURI);
   }
   private void logResponse(HttpServletRequest request, HttpServletResponse response, long duration) {
       int statusCode = response.getStatus();
       log.info("Ответ: HTTP {} - {}, время выполнения: {}ms", statusCode, request.getRequestURI(), duration);
   }
}

Этот фильтр будет добавлять записи о каждом запросе и ответе в журнал. А также посчитает время обработки запроса с момента получения и до момента ответа.

ОФТОП. Если вдруг вы используете ванильный spring, то просто указать аннотацию @Component не получится. Вам понадобится объявить bean, чтобы использовать этот фильтр. Для этого добавьте его в файл `src/main/java/resources/META-INF/filters.xml`:

```xml

    RequestLoggingFilter
    com.example.RequestLoggingFilter

 

    RequestLoggingFilter
    /*

```

Как указать очередность срабатывания фильтров

Давайте представим, что у нас получился крайне сложный и громоздкий фильтр и мы решили разделить его на два: один для логирования, второй для подсчета времени. При этом нам нужно сначала запустить счетчик, а уже за ним логирование.

Для того чтобы выстроить фильтры в очередь, в Spring Boot есть аннотация @Order, в которую передается целочисленный параметр. Чем меньше этот параметр, тем раньше будет выполняться код. 

Давайте разделим логику нашего фильтра. Для счетчика времени обработки запроса укажем аннотацию @Order (1), чтобы он выполнялся первым:

@Slf4j
@Order(1)
@Component

public class DurationFilter implements Filter {
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, 
              FilterChain chain) throws IOException, ServletException {
       long startTime = System.currentTimeMillis();
       try {
           // Продолжение цепочки фильтров
           chain.doFilter(request, response);
       } finally {
           long duration = System.currentTimeMillis() - startTime;
           // Запись информации о времени выполнения
           log.info("Время выполнения: {}ms", duration);
       }
   }
}

Второй фильтр, который занимается логированием, мы пометим аннотацией @Order (2):

@Slf4j
@Order(2)
@Component

public class RequestLoggingFilter implements Filter {
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, 
              FilterChain chain) throws IOException, ServletException {
       HttpServletRequest httpRequest = (HttpServletRequest) request;
       HttpServletResponse httpResponse = (HttpServletResponse) response;
       // Запись информации о запросе
       logRequest(httpRequest);
       try {
           // Продолжение цепочки фильтров
           chain.doFilter(request, response);
       } finally {
           // Запись информации об ответе и времени выполнения
           logResponse(httpRequest, httpResponse);
       }
   }
   private void logRequest(HttpServletRequest request) {
       String requestURI = request.getRequestURI();
       String method = request.getMethod();
       log.info("Запрос: {} - {}", method, requestURI);
   }
   private void logResponse(HttpServletRequest request, HttpServletResponse response) {
       int statusCode = response.getStatus();
       log.info("Ответ: HTTP {} - {}", statusCode, request.getRequestURI());
   }
}

Теперь у нас два фильтра, каждый из которых отвечает за свою логику.

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

Иногда нам требуется применять фильтр только для запросов, в которых указан определенный путь. Например, мы должны проверить, есть ли в запросе заголовок x-api-key, если в пути запроса есть, допустим, /docs/*. Затем сверить значение с константой. Если что-то не соответствует, то вернуть ошибку 403.

Можно, конечно, реализовать проверку в самом фильтре, например так:

@Slf4j
@Order(0)
@Component

public class CheckApiKeyFilter implements Filter {
   private static final String API_KEY = "strong_api_key";
   private static final String DOCS_PATH = "/docs";
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, 
              FilterChain chain) throws IOException, ServletException {
       HttpServletRequest httpRequest = (HttpServletRequest) request;
       HttpServletResponse httpResponse = (HttpServletResponse) response;
       String xApiKey = httpRequest.getHeader("x-api-key");
       String uri = httpRequest.getRequestURI();
       if (uri.startsWith(DOCS_PATH)) {
           // Проверка наличия и корректности API-ключа
           if (checkApiKey(xApiKey)) {
               chain.doFilter(request, response);
           } else {
               // Отбрасывание запроса с кодом 403
               httpResponse.sendError(403);
           }
       } else {
           chain.doFilter(request, response);
       }
   }
   private boolean checkApiKey(String apiKey) {
       return apiKey != null && apiKey.equals(API_KEY);
   }
}

Но это, во-первых,  не очень красиво, во-вторых, сложно будет дорабатывать, если появятся еще пути в запросе, доступ к которым потребуется также ограничивать. 

Удобнее реализовать такую задачу через класс FilterRegistrationBean — это интерфейс в Spring Framework, который позволяет создавать фильтры для обработки HTTP-запросов. Он дает возможность регистрировать собственные фильтры в процессе обработки запроса. Мы можем использовать этот класс для проверки, модификации или отбрасывания запросов на основе определенных критериев. 

Используем FilterRegistrationBean и зарегистрируем фильтр, который проверяет, что в запросе имеется корректный x-api-key, прежде чем открыть доступ к определенным ресурсам.

Для начала уберем проверку наличия в пути запроса »/docs»:  

@Slf4j

public class CheckApiKeyFilter implements Filter {
   private static final String API_KEY = "strong_api_key";
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, 
              FilterChain chain) throws IOException, ServletException {
       HttpServletRequest httpRequest = (HttpServletRequest) request;
       HttpServletResponse httpResponse = (HttpServletResponse) response;
       String xApiKey = httpRequest.getHeader("x-api-key");
       // Проверка наличия и корректности API-ключа
       if (checkApiKey(xApiKey)) {
           chain.doFilter(request, response);
       } else {
           // Отбрасывание запроса с кодом 403
           httpResponse.sendError(403);
       }
   }
   private boolean checkApiKey(String apiKey) {
       return apiKey != null && apiKey.equals(API_KEY);
   }
}

Также убрали аннотации @Order и @Component, так как для регистрации фильтра будем добавлять bean в конфигурационный файл и дополнительно настраивать класс FilterRegistrationBean:

@Slf4j
@Configuration

public class CommonConfig {
   private static final String DOCS_PATH = "/docs/*";
  
   @Bean
   public FilterRegistrationBean checkApiKeyFilter(){
       FilterRegistrationBean registrationBean
               = new FilterRegistrationBean<>();
       registrationBean.setFilter(new CheckApiKeyFilter());
       registrationBean.addUrlPatterns(DOCS_PATH);
       registrationBean.setOrder(0);
       return registrationBean;
   }
}

Как вы уже поняли, здесь мы регистрируем фильтр CheckApiKeyFilter для обработки запросов, у которых путь начинается с »/docs». А также определяем очередность выполнения фильтра, заменив аннотацию @Order на метод FilterRegistrationBean.setOrder ().

В этой статье я описал, что такое спринговые фильтры и как их использовать. В следующей расскажу, почему для логирования я выбрал именно аннотации, а не фильтры и при чем здесь спринговый controller advice.

вАЙТИ — DIY-медиа для ИТ-специалистов. Делитесь личными историями про решение самых разных ИТ-задач и получайте вознаграждение.

Что еще почитать

#Big Data

Обзор Greenplum Database. Назначение и ключевые преимущества
Аналитическая СУБД с высоты птичьего полета. Архитектура, лицензия, примеры использования.

Массивные данные: как проходит регистрация и учет животных в масштабах России
Цифровизация учета животных в агропромышленном комплексе как основа продовольственной безопасности страны.

Опыт внедрения и особенности перехода с Power BI на PIX BI
В рамках импортозамещения мы помогли клиенту перейти на PIX BI. Рассказываю, что и как делали для этого и что получилось в итоге.

#Data Engineering

Как готовить из сырых хитовых данных Google Analytics 4
Нам потребуются: стриминг данных из Google Analytics, источник сессий и источник события

Как обновлять витрины данных в ClickHouse по партициям
Ускоряем выполнение запросов в больших таблицах и одновременно снижаем потребление ресурсов

ClickHouse vs BigQuery: 4 отличия в SQL
Сравниваю популярные СУБД, чтобы вам не пришлось разбираться в отличиях самим

© Habrahabr.ru