Skip to content

升级 Spring Boot 2.7.0 中已弃用的 WebSecurityConfigurerAdapter

问题描述

从 Spring Boot 2.7.0 开始,WebSecurityConfigurerAdapter 类已被标记为弃用,开发者需要寻找替代方案来配置 Spring Security。当你尝试将原有的基于 WebSecurityConfigurerAdapter 的安全配置迁移到新的方式时,可能会遇到各种错误,其中最常见的是关于 AuthenticationManager 配置的问题。

典型的错误信息如下:

text
org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': 
Unsatisfied dependency expressed through method 'setFilterChains' parameter 0;
...
nested exception is java.lang.IllegalStateException: 
Cannot apply org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$EnableGlobalAuthenticationAutowiredConfigurer@3fdc705c to already built object

解决方案

1. 基本配置迁移

以下是传统的基于 WebSecurityConfigurerAdapter 的配置和新的基于组件的配置方式的对比:

传统配置(已弃用)

java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private AuthEntryPointJwt unauthorizedHandler;
    
    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll()
            .antMatchers("/api/test/**").permitAll()
            .anyRequest().authenticated();
        
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

新配置方式(推荐)

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private AuthEntryPointJwt unauthorizedHandler;
    
    @Autowired
    private AuthTokenFilter authenticationJwtTokenFilter;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .exceptionHandling(exp -> exp.authenticationEntryPoint(unauthorizedHandler))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/test/**").permitAll()
                .anyRequest().authenticated());
        
        http.addFilterBefore(authenticationJwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

重要变化

  1. 不再使用 @EnableGlobalMethodSecurity,改用 @EnableMethodSecurity
  2. antMatchers() 方法已弃用,改用 requestMatchers()
  3. AuthenticationManagerBuilder 的配置方式发生了变化
  4. configure(HttpSecurity http) 方法被 filterChain(HttpSecurity http) 方法取代

2. 完整的 JWT 安全配置示例

下面是一个完整的 JWT 身份验证安全配置示例:

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    
    private final UserDetailsService userDetailsService;
    private final AuthEntryPointJwt unauthorizedHandler;
    private final AuthTokenFilter authenticationJwtTokenFilter;
    
    public SecurityConfig(UserDetailsService userDetailsService, 
                         AuthEntryPointJwt unauthorizedHandler,
                         AuthTokenFilter authenticationJwtTokenFilter) {
        this.userDetailsService = userDetailsService;
        this.unauthorizedHandler = unauthorizedHandler;
        this.authenticationJwtTokenFilter = authenticationJwtTokenFilter;
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
            .exceptionHandling(exp -> exp.authenticationEntryPoint(unauthorizedHandler))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/test/**").permitAll()
                .anyRequest().authenticated());
        
        http.addFilterBefore(authenticationJwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                    .allowedMethods("*");
            }
        };
    }
}

3. 基本身份验证配置示例

如果你使用的是基本身份验证而非 JWT,可以参考以下配置:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests()
            .requestMatchers("/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .httpBasic();
        
        return http.build();
    }
    
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web -> web.ignoring().requestMatchers("/images/**", "/js/**", "/webjars/**"));
    }
}

常见问题与解决方案

1. AuthenticationManager 配置问题

错误示例

不要尝试手动构建 AuthenticationManagerBuilder:

java
// 不推荐的写法
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userDetailsService);
authenticationManager = authenticationManagerBuilder.build();

正确做法

使用 Spring 提供的 AuthenticationConfiguration:

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

2. UserDetailsService 配置

确保你的 UserDetailsService 正确配置:

java
@Service
public class CustomUserDetailService implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    public CustomUserDetailService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
        
        return User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(convertAuthorities(user.getRoles()))
            .build();
    }
    
    private Set<GrantedAuthority> convertAuthorities(Set<Role> roles) {
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority(role.getName()))
            .collect(Collectors.toSet());
    }
}

3. Spring Security 6.x 的额外变化

如果你使用的是 Spring Boot 3.x 或 Spring Security 6.x,还需要注意以下变化:

  1. antMatchers() 完全被 requestMatchers() 取代
  2. Lambda DSL 成为推荐配置方式
  3. 一些类包路径发生了变化(从 javax 迁移到 jakarta)

总结

迁移已弃用的 WebSecurityConfigurerAdapter 主要涉及以下几个关键变化:

  1. AuthenticationManager 配置:使用 AuthenticationConfiguration 而非 AuthenticationManagerBuilder
  2. HTTP 安全配置:使用 SecurityFilterChain Bean 而非重写 configure 方法
  3. 方法安全注解:使用 @EnableMethodSecurity 替代 @EnableGlobalMethodSecurity
  4. 请求匹配:使用 requestMatchers() 替代 antMatchers()

遵循这些变化,你可以顺利地将你的 Spring Security 配置迁移到新的方式,避免弃用警告并确保应用程序的长期可维护性。