解决 Spring Boot 中 SecurityConfig 循环依赖问题的详解

简介: 本文详细解析了在 Spring Boot 中配置 `SecurityConfig` 时可能遇到的循环依赖问题。通过分析错误日志与代码,指出问题根源在于 `SecurityConfig` 类中不当的依赖注入方式。文章提供了多种解决方案:移除 `configureGlobal` 方法、定义 `DaoAuthenticationProvider` Bean、使用构造函数注入以及分离配置类等。此外,还讨论了 `@Lazy` 注解和允许循环引用的临时手段,并强调重构以避免循环依赖的重要性。通过合理设计 Bean 依赖关系,可确保应用稳定启动并提升代码可维护性。

theme: cyanosis

解决 Spring Boot 中 SecurityConfig 循环依赖问题的详解

在使用 Spring Boot 构建应用程序时,配置安全性 (SecurityConfig) 是一个常见的需求。然而,在配置过程中,开发者可能会遇到循环依赖(Circular Dependency)的问题,导致应用无法正常启动。本文将详细介绍如何分析和解决 Spring Boot 应用中 SecurityConfig 类引发的循环依赖问题。

image.png

引言

在构建基于 Spring Boot 的应用程序时,安全配置是确保应用程序安全性的关键环节。Spring Security 提供了强大的功能,但不当的配置可能会导致循环依赖问题,进而阻碍应用程序的启动。本文将通过实际案例,解析循环依赖的根源,并提供有效的解决方案。

问题描述

在启动 Spring Boot 应用程序时,出现如下错误日志:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'securityConfig': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
AI 代码解读

该错误表明在创建 SecurityConfig Bean 时,发生了循环依赖,导致 Spring 容器无法完成 Bean 的实例化。接下来,我们将深入分析日志和代码,找出问题的根源,并进行修复。

日志分析

以下是应用程序启动时的关键日志信息:

2024-12-17T18:27:34.453+08:00  WARN 19888 --- [ConveniencePOS] [  restartedMain] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityConfig' defined in file [E:\Word\ConveniencePOS\target\classes\com\example\conveniencepos\config\SecurityConfig.class]: Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'securityConfig': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?
AI 代码解读

从日志中可以看出,SecurityConfig Bean 在创建过程中依赖于自身,导致循环依赖问题。

原始代码

以下是导致问题的 SecurityConfig 类代码:

package com.example.conveniencepos.config;

import com.example.conveniencepos.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {
   

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
   
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/login", "/api/users/register", "/error").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/login")
                        .loginProcessingUrl("/perform_login")
                        .defaultSuccessUrl("/sales", true)
                        .failureUrl("/login?error")
                        .permitAll()
                )
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/login?logout")
                        .permitAll()
                );

        return http.build();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
   
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}
AI 代码解读

循环依赖原因分析

在上述代码中,SecurityConfig 类通过 @Autowired 注入了 UserDetailsServiceImpl,并定义了 PasswordEncoder Bean。同时,configureGlobal 方法中调用了 passwordEncoder() 方法。这种直接调用 @Bean 方法的方式可能导致 Spring 尝试重新创建 Bean,从而引发循环依赖。

具体来说:

  1. 构造函数注入引入的循环依赖:在 configureGlobal 方法中直接调用 passwordEncoder(),使得 SecurityConfig 在创建 PasswordEncoder Bean 时需要依赖自身,形成循环。
  2. 依赖注入方式:使用字段注入 (@Autowired on field) 可能导致依赖关系难以追踪和管理,增加循环依赖的风险。

解决方案

为了解决上述循环依赖问题,可以采取以下步骤:

1. 移除 configureGlobal 方法

Spring Security 的最新版本推荐使用 AuthenticationProvider 或通过 SecurityFilterChain 来配置认证,而不是使用 AuthenticationManagerBuilder。因此,可以移除 configureGlobal 方法,并通过定义 DaoAuthenticationProvider 来设置 UserDetailsServicePasswordEncoder

2. 定义 DaoAuthenticationProvider Bean

通过定义一个 DaoAuthenticationProvider Bean,可以将 UserDetailsServicePasswordEncoder 绑定起来,而不需要在 SecurityConfig 类中直接调用 passwordEncoder() 方法。

3. 重新配置 SecurityFilterChain

SecurityFilterChain 中配置 AuthenticationProvider,确保 Spring Security 能够正确处理认证过程。

4. 完整的重构后的 SecurityConfig

以下是重构后的 SecurityConfig 类,避免了循环依赖问题:

package com.example.conveniencepos.config;

import com.example.conveniencepos.service.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
   

    private final UserDetailsServiceImpl userDetailsService;

    // 使用构造函数注入,确保依赖关系的明确性
    public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
   
        this.userDetailsService = userDetailsService;
    }

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

    // 定义 DaoAuthenticationProvider Bean
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
   
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());

        return authProvider;
    }

    // 配置 SecurityFilterChain
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
   
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/login", "/api/users/register", "/error").permitAll()
                    .anyRequest().authenticated()
            )
            .formLogin(form -> form
                    .loginPage("/login")
                    .loginProcessingUrl("/perform_login")
                    .defaultSuccessUrl("/sales", true)
                    .failureUrl("/login?error")
                    .permitAll()
            )
            .logout(logout -> logout
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/login?logout")
                    .permitAll()
            )
            .authenticationProvider(authenticationProvider()); // 设置 AuthenticationProvider

        return http.build();
    }
}
AI 代码解读

5. 解释重构后的变化

  • 移除 configureGlobal 方法:通过移除该方法,消除了对 AuthenticationManagerBuilder 的依赖,从而避免了潜在的循环依赖。
  • 使用构造函数注入:通过构造函数注入 UserDetailsServiceImpl,使得依赖关系更加明确和易于管理。
  • 定义 DaoAuthenticationProvider Bean:通过 DaoAuthenticationProvider,将 UserDetailsServicePasswordEncoder 绑定起来,避免了在配置类中直接调用 passwordEncoder() 方法。
  • SecurityFilterChain 中配置 AuthenticationProvider:通过在 SecurityFilterChain 中设置 AuthenticationProvider,确保 Spring Security 能够正确处理认证过程。

6. 检查其他 Bean 的依赖关系

确保 UserDetailsServiceImpl 或其他相关 Bean 不依赖于 SecurityConfig 或其定义的 Bean。如果存在这种情况,可能需要进一步重构这些 Bean 以消除循环依赖。

7. 分离配置类(可选)

为了进一步降低循环依赖的风险,可以将 PasswordEncoder 的定义移动到另一个配置类中。例如:

package com.example.conveniencepos.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordConfig {
   

    @Bean
    public PasswordEncoder passwordEncoder() {
   
        return new BCryptPasswordEncoder();
    }
}
AI 代码解读

然后在 SecurityConfig 中注入 PasswordEncoder

package com.example.conveniencepos.config;

import com.example.conveniencepos.service.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
   

    private final UserDetailsServiceImpl userDetailsService;
    private final PasswordEncoder passwordEncoder;

    public SecurityConfig(UserDetailsServiceImpl userDetailsService, PasswordEncoder passwordEncoder) {
   
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
   
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder);

        return authProvider;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
   
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/login", "/api/users/register", "/error").permitAll()
                    .anyRequest().authenticated()
            )
            .formLogin(form -> form
                    .loginPage("/login")
                    .loginProcessingUrl("/perform_login")
                    .defaultSuccessUrl("/sales", true)
                    .failureUrl("/login?error")
                    .permitAll()
            )
            .logout(logout -> logout
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/login?logout")
                    .permitAll()
            )
            .authenticationProvider(authenticationProvider());

        return http.build();
    }
}
AI 代码解读

8. 使用 @Lazy 注解(不推荐)

如果无法通过重构消除循环依赖,可以在其中一个依赖上使用 @Lazy 注解,推迟 Bean 的初始化时间,从而打破循环依赖。

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, @Lazy PasswordEncoder passwordEncoder) throws Exception {
   
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
AI 代码解读

注意:使用 @Lazy 是一种临时解决方案,长期来看应尽量避免循环依赖。

9. 启用循环引用(不推荐)

作为最后的手段,可以在 application.properties 中设置 spring.main.allow-circular-references=true 来允许循环引用。然而,这不推荐作为长期解决方案,因为它可能导致不可预测的行为。

spring.main.allow-circular-references=true
AI 代码解读

结论

循环依赖问题在 Spring Boot 应用程序中并不少见,尤其是在配置复杂的安全性设置时。通过合理设计 Bean 的依赖关系,使用构造函数注入,避免在配置类中直接调用 @Bean 方法,可以有效避免循环依赖问题。

关键要点总结

  1. 避免在配置类中直接调用 @Bean 方法,改用构造函数或方法参数注入。
  2. 使用 DaoAuthenticationProviderUserDetailsServicePasswordEncoder 绑定起来,简化配置。
  3. 分离配置类,降低 Bean 之间的耦合度,减少循环依赖的可能性。
  4. 谨慎使用 @Lazy 注解和允许循环引用,优先通过重构消除循环依赖。
目录
打赏
0
0
0
0
17
分享
相关文章
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
118 2
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
296 24
|
6月前
|
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
220 1
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
86 4
面试问Spring循环依赖?今天通过代码调试让你记住
该文章讨论了Spring框架中循环依赖的概念,并通过代码示例帮助读者理解这一概念。
面试问Spring循环依赖?今天通过代码调试让你记住
spring如何解决循环依赖
Spring框架处理循环依赖分为构造器循环依赖与setter循环依赖两种情况。构造器循环依赖不可解决,Spring会在检测到此类依赖时抛出`BeanCurrentlyInCreationException`异常。setter循环依赖则通过缓存机制解决:利用三级缓存系统,其中一级缓存`singletonObjects`存放已完成的单例Bean;二级缓存`earlySingletonObjects`存放实例化但未完成属性注入的Bean;三级缓存`singletonFactories`存放创建这些半成品Bean的工厂。
122 2
【spring】如何解决循环依赖
【spring】如何解决循环依赖
289 56
循环依赖难破解?Spring Boot神秘武器@RequiredArgsConstructor与@Lazy大显神通!
【8月更文挑战第29天】在Spring Boot应用中,循环依赖是一个常见问题。当两个或多个Bean相互依赖形成闭环时,Spring容器会陷入死循环。本文通过对比@RequiredArgsConstructor和@Lazy注解,探讨它们如何解决循环依赖问题。**@RequiredArgsConstructor**:通过Lombok生成包含final字段的构造函数,优先通过构造函数注入依赖,简化代码但可能导致构造函数复杂。**@Lazy**:延迟Bean的初始化,直到首次使用,打破创建顺序依赖,增加灵活性但可能影响性能。根据具体场景选择合适方案可有效解决循环依赖问题。
315 0
Spring循环依赖问题之Spring循环依赖如何解决
Spring循环依赖问题之Spring循环依赖如何解决
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等