과제 하면서 배운 부분 정리, 트러블 슈팅
총 Lv4로 이루어져 있으며, Lv2까지는 필수 과제, 그 이상은 도전 과제이며, 도전하는 것보다 복습을 더 집중적으로 해야 할 것 같아서 필수 과제까지만 했습니다.
기본 흐름

FilterChainProxy를 통해 등록된 보안 필터가 순서대로 실행됨SecurityContextPersistenceFilter: 기존 인증 정보 로드 및 저장UsernamePasswordAuthenticationFilter: 폼 로그인 요청 처리BasicAuthenticationFilter: HTTP Basic 인증 처리ExceptionTranslationFilter: 인증/인가 실패 시 예외 처리FilterSecurityInterceptor: 인가(권한) 결정 후 컨트롤러 호출 여부 판단Authentication 인터페이스를 구현한 클래스 Authentication으로 저장ProviderManagerAuthenticationProvider에게 인증 요청을 위임supports() 메서드를 통해 처리 가능한 Authentication 타입 지정loadUserByUsername(String username) 메서드를 오버라이드하여 구현UserDetails 객체이며, 사용자 정보 및 권한을 포함@PreAuthorize("hasRole('ADMIN')")는 내부적으로 ROLE_ADMIN을 찾고 다른 접두사를 쓰게 되면 hasAuthority()로 검사해야 함@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_"+ user.getUserRole().toString()));
}
Q. 왜 Spring Security는 인증 실패와 인가 실패를 모두 자동적으로 403으로 반환할까?
A. 레거시 시스템과의 호환성 + 보안 관례(어떤 것이 실패했는지 노출하지 않기 위해)
Q. 필터 체인 순서
A. 공식 문서에서 가져왔다
DisableEncodeUrlFilter
ForceEagerSessionCreationFilter
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextHolderFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
GenerateOneTimeTokenFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
DefaultResourcesFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
DefaultOneTimeTokenSubmitPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
AuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
AuthorizationFilter
SwitchUserFilter
Spring Security를 공부하다가 JWT Filter 내부 코드에 의문이 들었다.
JWT가 null인데 예외처리 안 하고 그냥 다음 필터로 왜 넘기지?
if (bearerJwt == null || !bearerJwt.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable) // csrf 기능 비활성화
.sessionManagement(sm ->
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 비활성화
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // WHITE_LIST 느낌
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()) // 나머지는 인증 필요
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
permitAll()이 WHITE_LIST처럼 아예 통과할 줄 알았지만 디버그해보니 Jwt필터에 null로 걸리는 것이다. 그래서 예외처리를 직접 해버리면 예외가 터진다.
JWT에서도 startsWith로 return filterChain.doFilter(request, response); 하는 식으로 검증을 해주고 직접 에러를 처리하는 게 좋은 것 같다.
그러고나서 더 테스트를 해봤는데 애초에 저렇게 바로 다음 필터로 넘기면 401이 뜨는 것이 아니라 403을 반환하는 것이였다. AI한테 물어볼 땐 401이 자동적으로 뜬다고 해서 믿고 넘겼는데 직접 해보니 403을 계속 반환해서 위처럼 놔두지 않고 무조건적으로 해결 방법처럼 해주는 게 좋다고 생각하게 되었다.
어제까지는 이렇게 생각했는데 오늘 튜터님에게 물어보니 보통 Security는
SecurityContextHolder의 Authentication 객체로 판단하여 requestMatchers로 따로 지정해둔 요청이 아니고 Holder가 비어있다면 .authenticated()이 자동적으로 에러로 403을 반환해준다. 따로 401을 에러처리로 지정해주고 return으로 끝내도 자동적으로 필터체인을 이어나가서 Security 자체 error로 도달해서 401을 403으로 덮어쓰게 되어 403을 반환해준다.
401을 반환하고 싶다면 SecurityConfig에서 .exceptionHandling()로 따로 지정해줘야 한다는 것을 깨달았고 무엇이든 얕을 때는 몰랐는데 알면 알수록 더 어려워진다는 것을 느꼈다.
근데 403이 되기 전에 토큰이 없으면 401을 반환하는게 맞지 않나라고 생각하여 좀 더 찾아보고 있는 중이다.