본문 바로가기
SpringBoot/오류

SpringSecurity 순환 참조(circular references) 발생

by se0nghyun2 2024. 9. 11.

스프링 순환 참조란?

서로 다른 빈들이 서로 참조를 맞물리게 주입되면서 발생하는 현상

스프링은 순환 참조 관계에 있는 빈들 중 어떤 빈을 먼저 생성해야 할지 결정할 수 없게 되는 상황이다.

 

 

문제 발생

시큐리티를 적용하고 비즈니스 로직이 실행이 아닌 애플리케이션 구동 시 아래와 같은 에러가 발생하였다.

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  securityConfig defined in file [C:\Users\sungh\IdeaProjects\football\build\classes\java\main\com\sunghyun\football\config\SecurityConfig.class]
↑     ↓
|  customAuthenticationProvider defined in file [C:\Users\sungh\IdeaProjects\football\build\classes\java\main\com\sunghyun\football\domain\member\infrastructure\auth\custom\provider\CustomAuthenticationProvider.class]
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

 

SecurityConfig와 customAuthenticationProvider 간에 순환참조가 발생하였다.

 

  • SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final AuthenticationConfiguration authenticationConfiguration;
    private final CustomAuthenticationProvider customAuthenticationProvider;

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

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        ProviderManager providerManager = (ProviderManager) authenticationConfiguration.getAuthenticationManager();
        providerManager.getProviders().add(this.customAuthenticationProvider);
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public AbstractAuthenticationProcessingFilter abstractAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
        return new CustomAuthenticationFilter(
                "/api/v1/auth/login",
                authenticationManager
        );
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .formLogin(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                //h2 허용 설정
                .headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
                .authorizeHttpRequests(authorizeRequest->
                        authorizeRequest.requestMatchers("/api/v1/match/rules").hasAnyAuthority("s")
                                .anyRequest().permitAll())
                .addFilterAt(this.abstractAuthenticationProcessingFilter(this.authenticationManager()),UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

  • CustomAuthenticationProvider 일부분
@AllArgsConstructor
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //아직 인증되지 않은 인증객체
        final Authentication unAuthentication = authentication;

        //이메일(인증객체 필드에서 가져오기)
        final String email = unAuthentication.getName();
        final String pwd = (String)unAuthentication.getCredentials();

        //유저 정보 가져오기
        UserDetails user = this.userDetailsService.loadUserByUsername(email);

        //유저 비밀번호 일치여부 체크
        if(!passwordEncoder.matches(pwd,user.getPassword())){
            throw new BadCredentialsException("pwd is not matches");
        }

        //인증완료된 인증객체 리턴
        return UsernamePasswordAuthenticationToken.authenticated(email,authentication.getCredentials(),user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

 

원인

  • CustomAuthenticationProvider 일부분
@AllArgsConstructor
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;

   ...
}

우선, CustomAuthenticationProvider는 PasswordEncoder를 참조하고 있다.

 

  • SecurityConfig 일부분
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final AuthenticationConfiguration authenticationConfiguration;
    private final CustomAuthenticationProvider customAuthenticationProvider;

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

PasswordEncoder는 SecurityConfig에서 Bean으로 등록해주고 있으며, CustomAuthenticationProvider를 참조하고 있다.

 

 

정리하면 아래와 같다.

1. SecurityConfig를 bean으로 등록하기 위해 CustomAuthenticationProvider bean이 필요

2. CustomAuthenticaitonProvider를 bean으로 등록하기 위해선 BcryptPasswordEncoder bean이 필요한데 이는 SecurityConfig가 bean등록이 필요함을 의미

이처럼 서로가 서로를 필요로 하기에 스프링에선 어떤 bean을 먼저 생성할지 결정하지 못하여 순환참조가 발생하게 되었다.

 

해결

@Lazy 활용이 있지만, 스프링에선 @Lazy는 추천하지 않는다고 한다.

근본적으로 설계에 대한 고민이 필요하다고 한다. 따라서 순환참조가 되지 않도록 설계를 해야한다.

 

1. PasswordEncoder를 새로운 Config 파일에 등록

  • AppConfig
@Configuration
public class AppConfig {

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

}

새로운 config파일에서 PasswordEncoder bean을 관리하도록 한다.

Securityconfig는 CustomAuthenticationProvider를 참조하고, CustomAuthenticationProvider은 AppConfig를 참조하므로

(SecurityConfig-> CustomAuthenticationProvider -> AppConfig)

순환참조 고리를 끊을 수 있다.

 

2. CustomAuthenticationProvider를 @Component 어노테이션 bean 등록이 아닌 설장 파일을 통한 bean 등록

해당 방식에 대한 큰 개념은 각각 서로 다른 class 파일에서 bean 등록을 하고 있기에 순환참조가 발생하였으니 각각 다른 class파일이 아닌 동일한 class 파일에서 bean으로 등록해 준다면 순환참조의 고리가 끊어질 수 있다는 것이다.

 

  • CustomAuthenticationProvider 일부분
//@Component
@AllArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;

    ...
}

 

 

  • SecurityConfig 일부분
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final AuthenticationConfiguration authenticationConfiguration;
    private final UserDetailsService userDetailsService;

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

    @Bean
    public AuthenticationProvider customAuthenticationProvider(){
        return new CustomAuthenticationProvider(userDetailsService,passwordEncoder());
    }

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        ProviderManager providerManager = (ProviderManager) authenticationConfiguration.getAuthenticationManager();
        providerManager.getProviders().add(this.customAuthenticationProvider());
        return authenticationConfiguration.getAuthenticationManager();
    }
    
    ...
 }

 

오직 SecurityConfig 파일 내에서만 CustomAuthenticationProvider와 PasswordEncoder를 bean 등록하고 있기에 순환참조 고리가 끊어지게 된다.

 


참고

https://beaniejoy.tistory.com/85

 

[Spring] 설정파일과 Bean 사이의 순환참조(circular references) 이슈 및 해결

Spring Security만을 사용해서 개인 프로젝트에 간단한 회원가입과 인증 프로세스를 개발하면서 부딪혔던 내용 중 하나를 정리하고자 합니다. Spring Security 설정파일 작성 후 애플리케이션 실행 시

beaniejoy.tistory.com

https://green-bin.tistory.com/52

 

Spring - Spring Security 적용시 순환 참조 발생 (Spring circular reference)

스프링 순환 참조(Circular reference)란? 서로 다른 빈(Bean)이 서로를 참조하면서 스프링이 어떤 빈을 먼저 생성해야 할지 결정하지 못하기 때문에 발생한다. 순환 참조는 DI 상황에 발생한다. DI 방법

green-bin.tistory.com