이번 포스트에서는 Spring Security의 설정 파일인 SecurityConfig
클래스를 구현하고, 커스텀 로그인 필터 및 OAuth2 로그인 기능을 추가하는 방법을 설명합니다.
/**
* 인증은 CustomUsernamePasswordAuthenticationFilter에서 authenticate()로 인증된 사용자로 처리
* JwtAuthenticationProcessingFilter는 AccessToken, RefreshToken 재발급
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final LoginService loginService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JwtService jwtService;
private final UserRepository userRepository;
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
private final CustomOAuth2UserService customOAuth2UserService;
private final String[] SWAGGER = {
"/v3/api-docs",
"/swagger-resources/**", "/configuration/security", "/webjars/**",
"/swagger-ui.html", "/swagger/**", "/swagger-ui/**"};
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin().disable()
.httpBasic().disable()
.cors().configurationSource(corsConfigurationSource())
.and()
.csrf().disable()
.headers().frameOptions().disable()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.mvcMatchers("/**").permitAll()
.antMatchers(SWAGGER).permitAll()
.antMatchers(HttpMethod.GET, "/api/v1/**").authenticated()
.antMatchers(HttpMethod.POST, "/api/v1/users/join", "/api/v1/users/login").permitAll()
.antMatchers(HttpMethod.POST, "/api/v1/**").authenticated()
.antMatchers("/api/v1/users/{userId}/role").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT).authenticated()
.antMatchers(HttpMethod.DELETE).authenticated()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().permitAll() // 위의 경로 이외에는 모두 접근 가능
.and()
//== 소셜 로그인 설정 ==//
.oauth2Login()
.successHandler(oAuth2LoginSuccessHandler) // 동의하고 계속하기를 눌렀을 때 Handler 설정
.failureHandler(oAuth2LoginFailureHandler) // 소셜 로그인 실패 시 핸들러 설정
.userInfoEndpoint().userService(customOAuth2UserService); // customUserService 설정
// 원래 스프링 시큐리티 필터 순서가 LogoutFilter 이후에 로그인 필터 동작
// 따라서, LogoutFilter 이후에 우리가 만든 필터 동작하도록 설정
// 순서 : LogoutFilter -> JwtAuthenticationProcessingFilter -> CustomUsernamePasswordAuthenticationFilter
http.addFilterAfter(customUsernamePasswordAuthenticationFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationProcessingFilter(), CustomUsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(false);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
/**
* AuthenticationManager 설정 후 등록
* PasswordEncoder를 사용하는 AuthenticationProvider 지정 (PasswordEncoder는 위에서 등록한 PasswordEncoder 사용)
* FormLogin(기존 스프링 시큐리티 로그인)과 동일하게 DaoAuthenticationProvider 사용
* UserDetailsService는 커스텀 LoginService로 등록
* 또한, FormLogin과 동일하게 AuthenticationManager로는 구현체인 ProviderManager 사용(return ProviderManager)
*/
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(bCryptPasswordEncoder);
provider.setUserDetailsService(loginService);
return new ProviderManager(provider);
}
/**
* 로그인 성공 시 호출되는 LoginSuccessJWTProviderHandler 빈 등록
*/
@Bean
public LoginSuccessHandler loginSuccessHandler() {
return new LoginSuccessHandler(jwtService, redisTemplate, objectMapper);
}
/**
* 로그인 실패 시 호출되는 LoginFailureHandler 빈 등록
*/
@Bean
public LoginFailureHandler loginFailureHandler() {
return new LoginFailureHandler(objectMapper);
}
/**
* CustomUsernamePasswordAuthenticationFilter 빈 등록
* 커스텀 필터를 사용하기 위해 만든 커스텀 필터를 Bean으로 등록
* setAuthenticationManager(authenticationManager())로 위에서 등록한 AuthenticationManager(ProviderManager) 설정
* 로그인 성공 시 호출할 handler, 실패 시 호출할 handler로 위에서 등록한 handler 설정
*/
@Bean
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() {
CustomUsernamePasswordAuthenticationFilter customUsernamePasswordLoginFilter
= new CustomUsernamePasswordAuthenticationFilter(objectMapper);
customUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
customUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessHandler());
customUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
return customUsernamePasswordLoginFilter;
}
@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
return new JwtAuthenticationProcessingFilter(jwtService, userRepository,redisTemplate);
}
}
SecurityConfig
클래스는 Spring Security의 설정 파일로, 인증과 인가 관련된 모든 설정을 정의합니다.
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
...
}
@EnableWebSecurity
는 Spring Security를 활성화하는 어노테이션입니다. 이 어노테이션을 사용하면 Spring Security와 관련된 설정들이 동작하게 됩니다.
@RequiredArgsConstructor
는 final 필드가 포함된 생성자를 자동으로 생성해주는 Lombok의 어노테이션입니다. 이로 인해 의존성 주입이 필요한 필드들을 자동으로 생성자에 주입할 수 있습니다.
Spring Security 5.7 버전 이상부터는 SecurityFilterChain
을 통해 필터 설정을 Bean으로 등록하여 컨테이너가 관리하도록 합니다.
http
.formLogin().disable()
.httpBasic().disable()
.csrf().disable()
.headers().frameOptions().disable()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
formLogin().disable()
: 기본 제공되는 폼 기반 로그인을 비활성화합니다.httpBasic().disable()
: HTTP 기본 인증을 비활성화합니다. JWT 인증을 사용할 것이므로 비활성화합니다.csrf().disable()
: CSRF 보안을 비활성화합니다. REST API의 경우에는 필요하지 않기 때문에 비활성화합니다.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
: 세션을 사용하지 않도록 설정합니다. JWT 기반 인증을 사용하기 때문에 세션을 상태 비저장으로 설정합니다..oauth2Login()
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint().userService(customOAuth2UserService);
oauth2Login()
: 소셜 로그인을 설정하는 메서드입니다. 소셜 로그인 성공 시와 실패 시 각각의 핸들러를 설정합니다.successHandler()
: 소셜 로그인 성공 시 호출되는 핸들러를 설정합니다. (예: oAuth2LoginSuccessHandler
)failureHandler()
: 소셜 로그인 실패 시 호출되는 핸들러를 설정합니다. (예: oAuth2LoginFailureHandler
)userInfoEndpoint().userService()
: OAuth2 로그인 시 사용자 정보를 처리하는 CustomOAuth2UserService
를 설정합니다.Spring Security는 기본적으로 다양한 필터를 제공하지만, 필요한 경우 커스텀 필터를 구현할 수 있습니다. 아래는 우리가 만든 필터를 설정하는 부분입니다.
http.addFilterAfter(customUsernamePasswordAuthenticationFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationProcessingFilter(), CustomUsernamePasswordAuthenticationFilter.class);
addFilterAfter(A, B)
: B 필터 이후에 A 필터가 동작하도록 설정합니다. 여기서는 LogoutFilter
이후에 커스텀 필터가 동작하도록 설정했습니다.addFilterBefore(A, B)
: B 필터 이전에 A 필터가 동작하도록 설정합니다. JWT 인증 필터(JwtAuthenticationProcessingFilter
)가 커스텀 로그인 필터(CustomUsernamePasswordAuthenticationFilter
)보다 먼저 실행되도록 설정합니다.Spring Security에서 필요한 핸들러와 필터 등을 Bean으로 등록합니다.
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
Spring Security에서 비밀번호를 암호화하는 데 사용되는 PasswordEncoder를 빈으로 등록합니다.
java
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(loginService);
return new ProviderManager(provider);
}
AuthenticationManager
는 인증을 처리하는 핵심 객체입니다. 이 메서드를 통해 DaoAuthenticationProvider를 사용하여 인증을 처리할 수 있도록 설정합니다. LoginService
는 사용자 인증 정보를 조회하는 UserDetailsService
의 역할을 합니다.@Bean
public LoginSuccessHandler loginSuccessHandler() {
return new LoginSuccessHandler(jwtService, userRepository);
}
@Bean
public LoginFailureHandler loginFailureHandler() {
return new LoginFailureHandler();
}
로그인 성공 및 실패 시 호출되는 핸들러를 설정합니다. 로그인 성공 시 JWT 토큰을 발급하고, 실패 시에는 에러 메시지를 반환하는 로직이 실행됩니다.
@Bean
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() {
CustomUsernamePasswordAuthenticationFilter customFilter = new CustomUsernamePasswordAuthenticationFilter(objectMapper);
customFilter.setAuthenticationManager(authenticationManager());
customFilter.setAuthenticationSuccessHandler(loginSuccessHandler());
customFilter.setAuthenticationFailureHandler(loginFailureHandler());
return customFilter;
}
커스텀 로그인 필터를 빈으로 등록합니다. 이 필터는 UsernamePasswordAuthenticationFilter
를 대체하여 로그인 요청을 처리합니다.
@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
return new JwtAuthenticationProcessingFilter(jwtService, userRepository);
}
JWT 인증 필터는 클라이언트가 전송한 JWT 토큰을 검증하여 유효성을 확인하는 역할을 합니다.