이동욱님의 스프링부트와 AWS로 혼자 구현하는 웹서비스 책을 쭉 따라가다보니 Spring Security를 적용하는 부분이 있었습니다.
책이 쓰여질 당시에는 boot 버전도 2이고, spring도 5버전대였던 것 같은데,
예제를 boot 3버전, spring은 6버전대로 적용해가며 따라가 보았습니다.
문제가 됐던게 spring security 적용하는 부분이었습니다.
spring security가 SecurityFilterChain을 적용하는 부분부터 많이 변경되어서 그런거 같은데, 만났던 오류 상황들을 정리해보려고 합니다.
1. Authentication(인증) vs Authorization(인가)
기본적으로 스프링 시큐리티가 해결하고자 하는 핵심적인 문제인 인증과 인가에 대해서 공식문서를 보면서 쭉 따라 읽어 보았습니다.
인증은 결국 사용자의 신원을 검증하는 과정,
인가는 인증된 사용자에게 리소스에 대한 권한을 부여하는 과정이라고 이해했습니다.
예를들어, 웹 서비스에 접근하는 사용자가에게 "너 누구야? 멀쩡한 사람 맞아?" 라고 물어보는 과정이 인증,
이 인증을 통해 검증된 사용자에게 "너는 여기여기에 접근할 수 있어."라고 권한을 부여하는 과정이 인가라고 머리속에 그려졌습니다.
스프링 시큐리티에선 인증은 AuthenticationManager 라는 객체가, 인가는 AuthorizationManager 라는 객체가 주도합니다.
얼마전까지는 AuthorizationManager라는 개념이 없던 것 같은데, 최근에 들어 추가 된 것으로 보입니다.
2. SecurityFilterChain 적용
스프링 시큐리티를 웹 환경에서 사용하려면 결국 SecurityFilterChain 이란 것을 관리해줘야 합니다.

공식 매뉴얼에 SecurityFilterChain과 Authentication, Authorization을 쭉 읽어본것이 이해하는데 많이 도움이 되었는데, 결국 핵심은 위 아키텍처입니다.
스프링 시큐리티는 Servlet을 때리기전에 Tomcat 같은 WAS의 Filter 레벨에서 처리됩니다.
그래서 꼭 스프링 컨테이너에 만들어놓은 Filter 빈들을 기존 Filter들 사이에 끼워넣기 위해 단순히 작업을 위임하는 DelegatingFilterProxy라는 Filter를 만들고 거기에 SecurityFilterChain을 연결하여 보안과 관련된 작업을 쭉 진행할 수 있게합니다.
이 Filter들에는 인증, 인가와 더불어 여러 널리 알려진 보안 취약점에 대한 방어 Filter들이 기본으로 설정되게 됩니다.
이렇게 하는 것의 장점으로는 스프링 시큐리티 작업에 대한 디버깅이 필요할 때 시작점을 명확히 할 수 있다는 점이라고 공식 문서에 나와있습니다.
이 SecurityFilterChain을 서비스에 맞게 커스터마이징 하기 위해선 @Configuation으로 자바 설정 파일을 만들고 SecurityFilterChain을 @Bean으로 만들어줘야합니다.

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf
.disable()
)
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.sameOrigin())
)
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(FORWARD, ERROR).permitAll()
.requestMatchers(PathRequest.toH2Console()).permitAll()
.requestMatchers("/", "/css/**", "/images/**", "/js/**", "/profile").permitAll()
.requestMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
)
.logout((logout) -> logout
.logoutSuccessUrl("/")
)
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
)
;
return http.build();
}
@Bean
static RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy(
"ROLE_ADMIN > ROLE_STAFF\n" +
"ROLE_STAFF > ROLE_USER\n" +
"ROLE_USER > ROLE_GUEST");
return hierarchy;
}
}
이전 방식인 WebSecurityConfigurerAdapter 을 상속받는 방법과 달리 SecurityFilterChain을 @Bean으로 직접 등록한다는 점에서 차이가 있습니다.
주의할 점은 @Configuration을 꼭 명시해줘야 한다는 점인데, 이전 방식에선 @Configuration을 사용하지 않았어서 처음에 빠뜨리고 지나가기 쉬웠습니다. @Configuration을 적용하지 않으면 당연히 빈들도 제대로 적용되지 않고 원하는 방향과 다르게 스프링 시큐리티가 동작하게 됩니다.
또한 차이점은 이전에 and() 메서드로 메서드 체인을 이어가는 방식과 다르게 메서드에서 시작점을 열고 내부에서 람다 방식으로 설정을 지정합니다.
2-1. h2 console Whitelabel Error Page 문제
서비스를 실행시키고 h2 console을 접속했을때, Whitelabel Error Page 403이 나오는 문제가 있었는데 결국 @Configuration을 등록하지 않아서 생긴 문제였습니다.
문제를 해결하려고 찾아보다가 발견한 설정중에, PathRequest.toH2Console()은 h2 console의 경로를 자동으로 설정해줘서 유용한 것 같아 직접적인 문자열 "/h2-console/**" 대신 적용하였습니다.
3. hasRole() vs hasAuthority() 의 차이
리소스 경로에 대해 권한을 확이하는 비슷한 메서드가 두개가 있습니다.
예제 코드들을 보면 어떨때는 USER_를 앞에 붙이고 어떨때는 안붙여서 메소드를 직접 확인하보면 아래와 같은 사실을 알 수 있습니다.
hasRole()
- hasRole(”USER”) 처럼 ROLE_ prefix가 필요없다.
- 내부적으로 결국 **hasAuthority**(ROLE_PREFIX + *role)*를 호출한다.
hasAuthority()
- ROLE_ prefix가 필요하다.
4. dispatcherTypeMatchers(FORWARD, ERROR)
일반적으로 사용자에게 요청을 받아 들어오는 요청은 dispatcher 타입이 REQUEST로 구분됩니다.
그런데 MVC를 공부하다 보면 jsp로 dispatch 하는 부분을 많이 접하셨을겁니다.
사용자의 요청이 아닌 내부적인 호출이나 Error로 인한 다른 리소스로의 전파에 대해서도 스프링 시큐리티는 필터를 통해 보안 체크를 하게됩니다.
이때 에러 페이지로 이동하는데 권한이 없다고 표시된다든가 할 수 있기 때문에, FORWARD와 ERROR에 대해선 모든 권한을 열어주었습니다.
사실 현재 상태에선 적용할 필요가 없지만 공식 문서를 보다가 이런 문제가 있다는 것을 확인하고 공부 차원에서 적용을 추가해보았습니다.