Как создать собственный фильтр в 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
<filter>
<filter-name>RequestLoggingFilter</filter-name>
<filter-class>com.example.RequestLoggingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>RequestLoggingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
```
Как указать очередность срабатывания фильтров
Давайте представим, что у нас получился крайне сложный и громоздкий фильтр и мы решили разделить его на два: один для логирования, второй для подсчета времени. При этом нам нужно сначала запустить счетчик, а уже за ним логирование.
Для того чтобы выстроить фильтры в очередь, в 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> checkApiKeyFilter(){
FilterRegistrationBean<CheckApiKeyFilter> registrationBean
= new FilterRegistrationBean<>();
registrationBean.setFilter(new CheckApiKeyFilter());
registrationBean.addUrlPatterns(DOCS_PATH);
registrationBean.setOrder(0);
return registrationBean;
}
}
Как вы уже поняли, здесь мы регистрируем фильтр CheckApiKeyFilter для обработки запросов, у которых путь начинается с “/docs”. А также определяем очередность выполнения фильтра, заменив аннотацию @Order на метод FilterRegistrationBean.setOrder().
В этой статье я описал, что такое спринговые фильтры и как их использовать. В следующей расскажу, почему для логирования я выбрал именно аннотации, а не фильтры и при чем здесь спринговый controller advice.