- TIL -
문제 발생)
잘 구현해 놓았던 JWT 토큰을 검증하던 로직에서 아래와 같은 에러가 발생하였다.
발생된 오류
해당 내용에 대해 구글링을 해보니 이 에러의 원인으로 의심되는 부분이 한 군데 발견되었다.
바로 authenticate 메소드를 가진 객체인 AuthenticationManager 객체인데 이 부분에서 순환 참조 오류가 발생한 것을 확인할 수 있었다.
구글링 결과 AuthenticationManager 의 경우 프록시 객체 상태로 사용하면 안되고 명시적으로 메소드를 사용하여 객체를 생성하여 반환한 뒤에 사용해야
한다는 것을 확인했다.
그렇지만 나는 아래와 같이 이미 SecurityConfig.class 에 명시적으로 authenticationManager 메소드를 통해 AuthenticationManager 를 빈 객체로
등록해 놓은 상황인데.... 이 객체를 사용하는 부분에서 내가 선언한 객체가 아닌 프록시 객체를 사용한다는 부분을 확인하게 되었다.
SecurityConfig.class)
더보기
package com.randomchat.main.config.security;
import com.randomchat.main.jwt.JWTFilter;
import com.randomchat.main.jwt.JWTUtil;
import com.randomchat.main.jwt.LoginFilter;
import com.randomchat.main.repository.users.UsersRepository;
import com.randomchat.main.service.jwt.CustomUserDetailsService;
import lombok.RequiredArgsConstructor;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
// AuthenticationManager 가 인자로 받을 AuthenticationConfiguration 객체 주입
private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
private final UsersRepository usersRepository;
private final CustomUserDetailsService customUserDetailsService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
// AuthenticationManager Bean 등록
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 폼 로그인 비활성화
.formLogin(formLogin -> formLogin.disable())
// HTTP Basic 인증 비활성화
.httpBasic(httpBasic -> httpBasic.disable())
// CSRF 보호 비활성화 (Token 인증 방식에서는 불필요)
.csrf(csrf -> csrf.disable())
// CORS 커스텀을 적용하는 메소드
.cors(c -> {
CorsConfigurationSource source = request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("*"));
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
corsSource.registerCorsConfiguration("/**", config);
return config;
};
c.configurationSource(source);
})
// 세션 비활성화
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// H2 콘솔 접근 허용
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.sameOrigin())
)
// 경로별 권한 인증 설정
.authorizeRequests(auth -> auth
// 메모리를 사용하는 H2 데이터베이스의 접속 경로를 오픈
// http://localhost:8080/h2-console 경로로 콘솔 접근 가능
.requestMatchers("/h2-console/**", "/login", "/register/**").permitAll()
.requestMatchers("/chat/**", "/room/**", "/send/**", "/enter/**").permitAll() // 채팅 관련 엔드포인트 접속 허용 설정
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterAfter(new JWTFilter(jwtUtil), LoginFilter.class)
// 커스텀 로그인 필터를 등록
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, usersRepository), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
그렇다면 여기서 프록시 객체를 사용하지 않고 현재 사용방식과 다른 방식으로 AuthenticationManager 객체를 명시적으로 선언하여 사용하는 방법에 대해 구글링을 해보았다.
Spring boot3 - AuthenticationManager + Stackoverflow feat. UserDetailsService
사건의 발단.. 이번에도 팀 시큐리티를 맡게 되었다 이번엔 기필코 깔끔하게 하겠다는 생각으로 임하게 되었는데.. 각 파트 다 짜고 이제 로그인해보자! 하고 들어가는 순간...!!!! 아.... 제일 보
hbyun.tistory.com
( 문제를 해결할 수 있게끔 도와주신 블로그에 무한한 감사를 드립니다.... )
정말 친절하게도 위의 블로그에서 DaoAuthenticationProvider 라는 객체를 사용해서 AuthenticationManager 를 생성하는 방법에 대해 설명해주고 있었고 나는 이 방법을 사용해보기로 하였는데 뭔가 이상함을 느꼈다....
위 블로그에서 작성해놓은 방법은 UserDetailsService 를 커스텀 구현한 서비스와 커스텀된 비밀번호 인코더가 필요한 부분이었는데 현재
SecurityConfig.class 에 비밀번호 인코더는 BCryptPasswordEncoder 객체를 반환하는 메소드를 통해 사용중인데 아무리 찾아봐도 이전에
구현해놓은 커스텀 CustomUserDetailsService 클래스가 보이질 않는다....
난 분명히 UserDetailsService 인터페이스를 구현한 CustomUserDetailsService 구현체를 작성했던 기억이 나는데 도통 보이질 않는다...
혹시 몰라 깃허브의 커밋 로그를 들어다 보았다.
초기 구현 부분에는 CustomUserDetailsService 가 존재한다.
스프링 시큐리티를 커스텀하면서 CustomUserDetailsService 클래스를 푸쉬했었고, 그 다음 JWT 토큰 커스텀 커밋 로그까지도 해당 클래스는
살아있었는데 이메일 인증 로직을 커밋한 로그를 살펴보니 아래와 같이 해당 서비스가 삭제되어 있는 모습을 확인할 수 있었다.
CustomUserDetailsService 가 삭제된 모습
아마도 코드를 정리하다가 CustomUserDetailsService 를 삭제한 모양이다....
위 클래스가 없으니 당연히 아래의 authenticationManager 메소드에서도 객체를 생성하여 반환할때 UserDetailsService 구현체를 불러오지 못해
프록시 객체로 동작 되는 것이 당연하였다....
삭제된 CustomUserDetailsService 파일을 다시 작성한뒤 메소드는 건들이지 않고 로직을 실행시켜 보았고 성공적으로 토큰을 받아오는 것을 확인할 수 있었다.
DaoAuthenticationProvider 객체를 사용하지 않고 기존 코드 유지
요청이 성공하여 토큰 정보를 헤더에 담아 응답하는 모습
랜덤 채팅을 구현하면서 화상 랜덤 채팅을 구현할때 토큰 값으로 사용자를 분류할 생각이었는데 이 오류를 잡는데 2주가 넘는 시간이 소비되었다...
이제 다시 토큰 값으로 사용자를 구분할 수 있게 되었다!