Другое

Решение ошибки Spring Security JWT с несколькими сервлетами: Полное руководство

Узнайте, как исправить ошибку 'Failed to instantiate SecurityFilterChain' при настройке Spring Security с аутентификацией JWT в приложениях с несколькими сервлетами. Полное решение с примерами кода.

Как исправить ошибку “Failed to instantiate SecurityFilterChain” при настройке Spring Security с аутентификацией JWT в приложении Spring Boot с несколькими отображаемыми сервлетами?

Я реализую Spring Security с аутентификацией JWT в приложении Spring MVC (Spring Boot), где уже настроены CSRF и управление сессиями. При применении моей пользовательской конфигурации Spring Security возникает следующая ошибка:

Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: This method cannot decide whether these patterns are Spring MVC patterns or not. If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); otherwise, please use requestMatchers(AntPathRequestMatcher).

This is because there is more than one mappable servlet in your servlet context: {org.apache.jasper.servlet.JspServlet=[*.jspx, *.jsp], org.springframework.web.servlet.DispatcherServlet=[/]}.

For each MvcRequestMatcher, call MvcRequestMatcher#setServletPath to indicate the servlet path.

Мой SecurityConfig.java:

java
package com.java.HMS.security;

import com.java.HMS.jwt.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .csrf(AbstractHttpConfigurer::disable)
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeHttpRequests( authorize ->
                        authorize
                                .requestMatchers("/").permitAll()
                                .anyRequest().authenticated()
                )
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

Мой application.properties:

properties
server.port=8088
# servlet dispatcher configuration
spring.mvc.servlet.load-on-startup=-1
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
server.servlet.context-path=/hms
server.tomcat.redirect-context-root=false

#Database Configuration
spring.datasource.url= jdbc:sqlserver://198.38.88.124:1433;databaseName=db_HMS_Test;encrypt=true;trustServerCertificate=true;
spring.datasource.username=*********
spring.datasource.password=*********
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.SQLServerDialect

#=========================== 
#FOR DEVELOPMENT CONNECTION
#==========================
spring.profiles.active=PROD
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
spring.jpa.open-in-view=true
spring.jpa.generate-ddl=true
spring.jpa.properties.hibernate.globally_quoted_identifiers=true


#server.tomcat.additional-tld-skip-patterns=*.jar    https://howtodoinjava.com/spring-boot2/read-file-from-resources/
#for images

spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=20MB

#read static files or images 
spring.web.resources.static-locations=classpath:/resources/
spring.mvc.static-path-pattern=/**

#management.endpoints.web.exposure.include=mappings

springdoc.enable-hateoas=false

#JWT SECRET PROPERTIES
jwt.token.secret={SECRET_KEY}
jwt.token.expires=30

Я пробовал использовать MvcRequestMatcher, но в этом случае либо пропускаются, либо не распознаются указанные конечные точки или пути. Как можно правильно решить эту проблему и настроить Spring Security с аутентификацией JWT в моем приложении с несколькими отображаемыми сервлетами?

Ошибка “Не удалось создать экземпляр SecurityFilterChain” возникает из-за того, что Spring Security не может автоматически определить, относятся ли ваши шаблоны к конечным точкам Spring MVC или к общим сервлетным путям, когда в вашем приложении существует несколько отображаемых сервлетов. Решение требует явной настройки MvcRequestMatcher с правильным сервлетным путем для каждого шаблона конечной точки.

Содержание

Понимание основной причины

Сообщение об ошибке четко указывает, что Spring Security обнаружил несколько отображаемых сервлетов в сервлетном контексте вашего приложения:

{org.apache.jasper.servlet.JspServlet=[*.jspx, *.jsp], org.springframework.web.servlet.DispatcherServlet=[/]}

Когда у вас несколько сервлетов, Spring Security не может автоматически определить, следует ли сопоставлять ваши шаблоны URL-адресов с контроллерами Spring MVC (требуется MvcRequestMatcher) или с общими сервлетными путями (требуется AntPathRequestMatcher). Эта неоднозначность не позволяет фреймворку создать правильный SecurityFilterChain.

Согласно документации Spring Security, вам необходимо явно настроить тип сопоставителя запросов для каждого шаблона. Для конечных точек Spring MVC вы должны использовать MvcRequestMatcher.Builder с правильно настроенными сервлетными путями.

Правильная настройка MvcRequestMatcher

Чтобы решить эту проблему, вам необходимо:

  1. Создать бин MvcRequestMatcher.Builder
  2. Настроить его с правильным сервлетным путем
  3. Использовать построитель для создания экземпляров MvcRequestMatcher для конечных точек Spring MVC

Вот правильная конфигурация:

java
@Bean
public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
    return new HandlerMappingIntrospector();
}

@Bean
public MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
    MvcRequestMatcher.Builder builder = new MvcRequestMatcher.Builder(introspector);
    // Настройка для вашего DispatcherServlet
    builder.setServletPath("/"); // Это соответствует сервлетному пути вашего DispatcherServlet
    return builder;
}

Полное решение для SecurityFilterChain

Вот ваш полный SecurityConfig.java с правильной конфигурацией:

java
package com.java.HMS.security;

import com.java.HMS.jwt.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
        return new HandlerMappingIntrospector();
    }

    @Bean
    public MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
        MvcRequestMatcher.Builder builder = new MvcRequestMatcher.Builder(introspector);
        builder.setServletPath("/"); // Настройка для DispatcherServlet
        return builder;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
        
        http
            .csrf(AbstractHttpConfigurer::disable)
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests(authorize -> authorize
                // Используйте MvcRequestMatcher для конечных точек Spring MVC
                .requestMatchers(mvc.pattern("/")).permitAll()
                .requestMatchers(mvc.pattern("/api/auth/**")).permitAll()
                .requestMatchers(mvc.pattern("/hms/**")).authenticated() // Используйте контекстный путь
                .anyRequest().authenticated()
            )
            .formLogin(AbstractHttpConfigurer::disable)
            .httpBasic(AbstractHttpConfigurer::disable)
            .sessionManagement(sessionManagement ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

Расширенная настройка нескольких сервлетов

Если вам нужно обрабатывать несколько сервлетов или более сложную маршрутизацию, вы можете создать несколько бинов SecurityFilterChain с разными аннотациями @Order. Этот подход позволяет настраивать разные политики безопасности для разных сервлетных контекстов.

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // ... другие бины ...

    @Bean
    @Order(1)
    public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
        http
            .securityMatcher(mvc.pattern("/api/**"))
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers(mvc.pattern("/api/auth/**")).permitAll()
                .requestMatchers(mvc.pattern("/api/admin/**")).hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            // ... другие настройки ...
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
        http
            .securityMatcher(mvc.pattern("/hms/**"))
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers(mvc.pattern("/hms/public/**")).permitAll()
                .anyRequest().authenticated()
            )
            // ... другие настройки ...
        return http.build();
    }
}

Тестирование и проверка

После реализации конфигурации протестируйте ваши конечные точки, чтобы убедиться, что они работают правильно:

  1. Публичные конечные точки (например, /, /api/auth/**) должны быть доступны без аутентификации
  2. Защищенные конечные точки (например, /hms/**) должны требовать правильной JWT-аутентификации
  3. 403 Forbidden должен возвращаться для аутентифицированных пользователей без соответствующих ролей
  4. 401 Unauthorized должен возвращаться для неаутентифицированных запросов к защищенным конечным точкам

Лучшие практики

  1. Последовательно используйте контекстный путь: Поскольку ваше приложение использует server.servlet.context-path=/hms, включайте этот путь в ваши MVC-шаблоны.

  2. Разделяйте обязанности: Рассмотрите возможность создания нескольких бинов SecurityFilterChain для разных частей вашего приложения (API, администрирование, общедоступные области).

  3. Используйте правильное сопоставление запросов: Всегда используйте MvcRequestMatcher для конечных точек Spring MVC и AntPathRequestMatcher для общих сервлетных путей.

  4. Явно настраивайте сервлетные пути: Всегда вызывайте MvcRequestMatcher.Builder#setServletPath(), чтобы указать, с каким сервлетом сопоставлять.

  5. Учитывайте порядок безопасности: При использовании нескольких цепочек фильтров используйте аннотацию @Order для обеспечения правильной последовательности выполнения.

Как демонстрируется в руководстве Baeldung по нескольким точкам входа, этот подход обеспечивает лучшее разделение обязанностей и более детальный контроль безопасности.

Источники

  1. Spring Cloud - Как цепочки фильтров Spring Security сопоставляются с конкретными запросами
  2. Baeldung - Несколько точек входа в Spring Security
  3. Блог Дана Веги - Расширенная безопасность Spring - Как создать несколько конфигураций Spring Security
  4. Stack Overflow - Понимание requestMatchers() в spring-security
  5. GitHub Issue Spring Security - Несколько сервлетов (MvcRequestMatcher & AntPathRequestMatcher)

Заключение

Ошибка “Не удалось создать экземпляр SecurityFilterChain” решается путем правильной настройки MvcRequestMatcher с явным указанием сервлетного пути. Ключевые выводы:

  1. Создайте бин HandlerMappingIntrospector и MvcRequestMatcher.Builder
  2. Явно настройте сервлетный путь с помощью setServletPath("/")
  3. Используйте построитель для создания экземпляров MvcRequestMatcher для всех конечных точек Spring MVC
  4. Рассмотрите возможность использования нескольких бинов SecurityFilterChain для сложных приложений
  5. Всегда включайте ваш контекстный путь (/hms) в MVC-шаблоны

Этот подход гарантирует, что Spring Security может правильно сопоставлять запросы с правильным сервлетным контекстом и применять соответствующие правила аутентификации и авторизации.

Авторы
Проверено модерацией
Модерация