내배캠 TIL 62일차 (Spring Plus 과제)

오병택·2025년 5월 14일

내배캠

목록 보기
70/73

한 줄 요약

과제 하면서 배운 부분 정리, 트러블 슈팅

총 Lv4로 이루어져 있으며, Lv2까지는 필수 과제, 그 이상은 도전 과제이며, 도전하는 것보다 복습을 더 집중적으로 해야 할 것 같아서 필수 과제까지만 했습니다.

Spring Security

  • Spring 기반 애플리케이션의 보안을 담당하는 프레임워크
  • 인증, 인가, CSRF, 세션 관리, 비밀번호 암호화 등 다양한 기능 제공

기본 흐름

주요 컴포넌트

1. SecurityFilterChain

  • 여러 개의 보안 필터들이 연결되어 있는 체인 구조
  • 사용자의 요청을 먼저 처리하는 진입 지점
  • FilterChainProxy를 통해 등록된 보안 필터가 순서대로 실행됨
  • 필터 목록에는 다음과 같은 필터가 존재:
    • SecurityContextPersistenceFilter: 기존 인증 정보 로드 및 저장
    • UsernamePasswordAuthenticationFilter: 폼 로그인 요청 처리
    • BasicAuthenticationFilter: HTTP Basic 인증 처리
    • ExceptionTranslationFilter: 인증/인가 실패 시 예외 처리
    • FilterSecurityInterceptor: 인가(권한) 결정 후 컨트롤러 호출 여부 판단

2. UsernamePasswordAuthenticationToken

  • Authentication 인터페이스를 구현한 클래스
  • SecurityContextHolder에 securityContext 안에 Authentication으로 저장
  • UserDetails의 정보들을 담고 있음

3. AuthenticationManager

  • 인증 로직을 총괄하는 인터페이스
  • 기본 구현체는 ProviderManager
  • 다양한 AuthenticationProvider에게 인증 요청을 위임

4.AuthenticationProvider

  • 실제 인증을 수행하는 컴포넌트
  • 사용자 정보를 DB에서 조회하고 비밀번호를 비교하는 로직을 포함
  • supports() 메서드를 통해 처리 가능한 Authentication 타입 지정
  • 여러 Provider를 등록하여 다양한 인증 방식 처리 가능

5. UserDetailsService

  • 사용자 정보를 로드하는 서비스
  • 커스텀으로 loadUserByUsername(String username) 메서드를 오버라이드하여 구현
  • 반환 타입은 UserDetails 객체이며, 사용자 정보 및 권한을 포함

6. UserDetails

  • 사용자의 정보를 담음
  • 커스텀으로 아래와 같이 오버라이드하여 구현
  • 접두사 ROLE_을 붙여주는 것은 관례(Spring Security 규칙)이며, @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을 반환하는게 맞지 않나라고 생각하여 좀 더 찾아보고 있는 중이다.

profile
걱정하지 말고 일단 해봐!

0개의 댓글