지난 포스팅인 인증/인가 로직 구현에 이어 Spring Security의 핵심 설정 클래스인 SecurityConfig에 대해 간단히 정리해보고자 한다.
필드 및 생성자
하단 CORS에 적용할 프론트 측 오리진 문자열은 yml 파일로 부터 가져오도록 작성하였고,
AuthenticationConfiguration의 경우 Spring Security의 기본 인증 설정이 주입된다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${spring.front.origin-http}")
private String frontOriginHttp;
@Value("${spring.front.origin-https}")
private String frontOriginHttps;
private final AuthenticationConfiguration authenticationConfiguration;
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
private final JWTUtil jwtUtil;
@Autowired
public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, CustomOAuth2UserService customOAuth2UserService,
CustomSuccessHandler customSuccessHandler, JWTUtil jwtUtil) {
this.authenticationConfiguration = authenticationConfiguration;
this.customOAuth2UserService = customOAuth2UserService;
this.customSuccessHandler = customSuccessHandler;
this.jwtUtil = jwtUtil;
}
// 이하 내용 생략
}
Bean Definition
SecuritFilterChain을 제외한 빈에는 AuthenticationManager와 BCryptPasswordEncoder를 추가해주었다.
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
// 하단엔 SecurityFilterChain 설정
SecurityFilterChain 설정
CORS 설정
프론트 통합 과정에서 가장 기본적으로 발생하는 문제인 CORS 설정 관련 부분이다.
WebMvcConfigurer를 구현한 Config 클래스의 `addCorsMappings()` 메소드도 따로 설정해주어야 한다.(이 포스팅에서는 제외했다)
httpSecurity
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(frontOriginHttp, frontOriginHttps));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
configuration.setExposedHeaders(Arrays.asList("Authorization", "Set-Cookie"));
return configuration;
}
}));
경로별 인가 작업 및 Exception Handling
인증/인가가 필요한 엔드포인트에 `hasAnyAuthority()` 를 통해 설정을 추가하였으며, 그 이외의 요청에 대해서는 검증을 진행하지 않도록 하였다. 처음에는 anyRequest에 `authenticated()` 설정을 해주었다가, 존재하지 않는 엔드포인트에 대해서도 401이나 403 응답이 가는 문제를 발견하여 `permitAll()`로 변경, 정상적으로 404 응답이 가도록 수정하였다.
httpSecurity
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/api/auth/sign-out").hasAnyAuthority(ADMIN.getValue(), USER.getValue())
.requestMatchers("/api/auth/withdraw").hasAnyAuthority(ADMIN.getValue(), USER.getValue())
.requestMatchers("/api/user/**").hasAnyAuthority(ADMIN.getValue(), USER.getValue())
.requestMatchers("/api/beverages/**").hasAnyAuthority(ADMIN.getValue(), USER.getValue())
.anyRequest().permitAll())
.exceptionHandling(customizer -> customizer
.authenticationEntryPoint((request, response, authException) -> {
// 인증 실패 처리 (401 Unauthorized)
InnerFilterResponseSender.sendInnerResponse(response, 401, 999,
"인증이 필요합니다. 로그인해주세요.", null);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
// 권한 부족 처리 (403 Forbidden)
InnerFilterResponseSender.sendInnerResponse(response, 403, 999,
"권한이 부족합니다.", null);
}));
기타 HttpSecurity 설정
CORS 설정과 경로별 인가 설정 외에도 다른 설정들을 필터 체인에 추가하였다.
//CSRF(Cross-Site Request Forgery) 보호 기능 비활성화
httpSecurity
.csrf((auth) -> auth.disable());
//Spring Security의 기본 폼 로그인 방식(Username & Password 로그인 폼) 비활성화
httpSecurity
.formLogin((auth) -> auth.disable());
//HTTP Basic 인증 방식 비활성화
httpSecurity
.httpBasic((auth) -> auth.disable());
//OAuth2 로그인 설정
httpSecurity
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
);
//LoginFilter 추가 - 일반 로그인
httpSecurity
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);
//CustomLogoutFilter 추가
httpSecurity
.addFilterBefore(new CustomLogoutFilter(jwtUtil), LogoutFilter.class);
//JWTFilter 추가
httpSecurity
.addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class);
//세션을 사용하지 않는 방식(STATELESS)으로 설정
httpSecurity
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
마치며
테스트 진행 시 보안관련 설정이 어떻게 잘 구성되어 있느냐에 따라 백엔드 작업자 측에서도, 프론트 측에서도 영향을 많이 받는 것 같다. 프로젝트 진행 초반에 정확히 설정하고 넘어가야 이후 진행이 수월하다고 느꼈다.
