[Spring Security] 아니 왜 Forbidden 만 계속 오지??

2023. 8. 17. 23:37·Spring/Spring Security
728x90
반응형

Spring Security 를 사용하여 인증 & 인가를 도입한 프로젝트를 개발중이였다. 다음과 같은 설정 정도로만 간단히 가져간 이후 개발을 진행하고 있었다. 

 

 

너무 설명을 대충 써놓은거 같아 추가해놓자면.. 

 

 

JWT TOKEN 을 사용하는 방식으로 바꾸었고, AuthenticationProvider, UsernamePasswordAuthenticationFilter 을 제어하여 로그인을 Custom 화 하였고, UsernamePasswordAuthenticationFilter (AbstsractAuthenticationProcessingFilter) 앞에 JwtAuthorizationFilter (그냥 OncePerRequestFilter 상속) 를 만들어줘서, Token 체킹을 해주도록 하였다. 그래서 인증 및 인가를 확인할 토큰이 없다면, 그냥 바로 뒤로 던져버린다. 

 

 

@EnableWebSecurity
public class SecurityJwtConfig extends WebSecurityConfigurerAdapter {

    /*
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {


        http
                .antMatcher("/api" + "/**")
                .authorizeRequests()
                .antMatchers("/api/v0/auth/**").permitAll()
                .anyRequest().authenticated();
        
        ...
    }
}

 

 

/api 를 통해 들어오는 경로에 대한 설정을 진행하는데, /api/v0/auth/** 로 들어오는 경로들은 아무 인증 & 인가가 필요하지 않지만, 그 외는 모두 인증이 된 상태여야 함을 명시하였다. 인가는 아직 구현하지 않은 상태였다. 

 

 

그러다 실수로 다음과 같은 테스트를 진행해버렸다. 

 

 

원래 회원가입 경로는 /api/v0/auth/signin 입니다

 

 

아 잘못쳤네 하다가 이상한 낌새를 느꼈다.

 

 

 

 

/api/v0/signin 은 없는 경로라 물론 404가 떠야 하지만 authentiacted() 를 요구하는 경로라
Unauthorized 인 401 이 떠야 하는거 아닌가..?

 

 

Security 마스터 하신 분들은 당연한거다 싶으실 수도 있는데, 저는 진지하게 든 생각이였습니다.. 잠깐 401 과 403 Http Error Response 의 차이를 간단히 알아보자. 

 

 

401 Unauthorize : 인증되지 않은 사용자이거나, 인증 여부를 파악하기에 정보가 부족할 경우 발생

403 Forbidden : 요청자의 권한은 매우 명확히 확인되었으나, 해당 요청을 수행하기에 권한이 부족할 경우 발생

 

 

인증된 객체임을 인증하기 위한 JWT Token 에 대한 어떠한 정보도 넘겨주지 않았는데, 403이 뜨는 것이 조금 이상했다. 그래서 한번 살펴보게 되었고, 그 과정이 아까워서 기록하기 위해 끄적여본다. 

 

 

일단 현재 나의 프로젝트에 적용되어 있는 Filter 를 분석하기 위해, 다음과 같이 Config 에 debug 모드를 설정하고, 아무 요청을 보냈을 때 조회되는 Security Filter 들을 확인해보았다.

 

 

 

@EnableWebSecurity(debug = true)
public class SecurityJwtConfig extends WebSecurityConfigurerAdapter {

    ...
    
}

 

debug mode 에서 보여준 Security Filter 들

 

 

Security Filter Chain 의 마지막인 ExceptionTranslationFilter 와 FilterSecurityInterceptor 이 인증 / 인가 예외 처리를 최종적으로 진행 하는 곳이기 때문에 해당 클래스들을 살펴보았다.

 

 

ExceptionTranslationFilter.java 의 doFilter 함수

 

 

ExceptionTranslationFilter 는 도달하자마자 후속 chain 으로 우선 넘기는 모습을 확인할 수 있다. 후속 filter 로는 FilterSecurityInterceptor.java 가 걸리게 되는데, 그 이유는 내가 Method 별 annotation 으로 인가를 사용하는 것이 없어서 그렇다. 만약 그렇다면 MethodSecurityInterceptor 가 걸리게 된다. (둘다 AbstractSecurityInterceptor 를 상속받는 클래스들이다) 

 

 

FilterSecurityInterceptor class 도 보면 invoke() 라는 함수를 수행하게 되고, invoke() 를 수행할 때 자신의 부모 함수인 AbstractSecurityInterceptor.java 의 함수들을 통해 invoke 를 진행하는 것을 확인할 수 있다 (beforeInvocation(), afterInvocation() 등 모두 부모 함수를 사용).

 

 

AbstractSecurityInterceptor 의 beforeInvocation 중

 

 

갑자기 Library Code 를 붙여 놓으니 나가실 수도 있겠지만, 그냥 내가 BreakPoint 건 곳만 확인하면 된다. 해당 함수에서는 MetaDataSource 에 담겨진 권한과, 현재 SecurityContext  안에 들어있는 인증 정보를 확인하는 것으로 보이고, 인증 정보가 비어있지 않다면 attemptAuthorization - [권한 확인]을 시도하는 것으로 보인다.

 

 

위의 상황으로 돌아가보자. 나는 인증이 필요한 경로에 인증 정보를 아무 입력하지 않은채로 요처을 던졌다. 그렇다면 첫 예상은 여기서 인증된 객체가 없으니, 두번째 null point 에서 걸리겠구나! 였다. 하지만 내 생각이 짧아서였는지, 해당 부분을 보기 좋게 통과하고 attemptAuthorization() 까지 내려가는 모습을 확인할 수 있었다. 

 

 

Security 를 한번이라도 공부했으면 당연히 알겠지만, Spring Security 는 인증되지 않으면 위 필터 사진 중 AnonymousAuthenticationFilter.java 란 곳에서 Anonymous 인증 객체를 SecurityContext 안에 넣어준다. (참고로 SecurityContextHodler 는 ThreadLocal 에 할당되어 해당 요청 Session 도중 언제든 사용할 수 있게 한다) 

 

 

AnonymousAuthenticationFilter 내부 doFilter 부분

 

 

또한, 여기서 createAuthenticatino(req) 부분을 보면 AnonymousAuthenticationToken 을 AuthenticationToken 으로 형성하는 것을 볼 수 있는데, 이 토큰의 생성자를 보면 다음과 같다.

 

 

private AnonymousAuthenticationToken(Integer keyHash, Object principal,
      Collection<? extends GrantedAuthority> authorities) {
   super(authorities);
   Assert.isTrue(principal != null && !"".equals(principal), "principal cannot be null or empty");
   Assert.notEmpty(authorities, "authorities cannot be null or empty");
   this.keyHash = keyHash;
   this.principal = principal;
   setAuthenticated(true);  // *** 이 부분
}

 

 

즉, Spring Security 는 인증되지 않은 상태에서, AnonymousAuthentication 객체를 인증되었다고 넣고, principal = "anonymousUser", authority = "ROLE_ANONYMOUS" 를 같이 넣어주게 된다. 그렇구나!

 

 

어쨌든 정확이 이걸 "인증된 상태" 라고 봐서인지는 모르겠으나, 아까 FiterSecurityInterceptor.java 에서 ContextHolder 에 Authentication 객체가 null 인지를 판단하는 부분을 통과하는 이유는 지금까지 설명한 바와 같이, Anonymous 객체가 Holder 에 저장되어 있기 때문이였다. 

 

 

뭐 처음부터 그게 궁금한건 아니였으니까, 그래서 과연 403 처리는 왜 발생하는건지 알아보기 위해 계속 내려가보자. 다시 위에 AbstractSecurityInterceptor.java 의 beforeInvocation() 함수를 다시 보길 바란다. 문제는 네번째 Break Point 인 attemptAuthorization 을 통과하면서 발생한다. 

 

 

AbstractSecurityInterceptor.java 의 attemptAuthorization()

 

 

여기서 AccessDecisionManager 가 decide() 를 수행하면, Voter 들이 권한이 충분한지를 판별하고, 통과 여부를 결정짓게 되는데, ROLE_ANONYMOUS 를 가지고는 authenticatied() path 를 통과할 수 없기 때문에, 해당 decide 시 exception 을 던지게 된다. 이에 따라 AccessDeniedException 을 바로 잡아서 던지게 된다 (AuthorizationFailureEvent 도 등록해준다) 

 

 

이는 Java Exception 에 따라 더 이상 후속을 진행하지 않고, 호출한 클래스로 넘기기 때문에 FilterSecurityInterceptor, 여기서 catch 하지 않으므로 ExceptionTranslationFilter 로 다시 올라가서 해당 클래스에서 다시 이 예외를 잡게 된다. 다시 맨 위에 첫 코드로 돌아가게 되는 것이다. 다만 AccessDeniedException 을 잡은 상황이다. 

 

 

try 이후 catch 에서 예외를 잡고 처리하는 상황

 

 

사진에 설명해 놓은 바와 같이 진행이 되며, 다음 handleSpringSecurityException 으로 그대로 전달이 된다. handleSpringSecurityException() 은 단순히 AccessDeniedException 임을 확인하고, 다시 handleAccessDeniedException() 으로 전달하게 되는데, 이 부분에서 중요한 일이 발생한다

 

 

ExceptionTranslationFilter 에서 AccessDeniedHandler 를 처리하는 모습

 

 

InsufficeintAuthenticationException 은 sendStartAuthentication() 으로 전달하는데, 해당 함수에서는 SecurityContextHolder 를 초기화시킨 이후, AuthenticationEntryPoint 의 commence 함수를 호출하게 된다. 이 AuthenticationEntryPoint 는 Authentication 즉, 인증을 핸들링 하는 도중 발생하는 예외를 처리하는 곳이다. 이 역할체에 무엇이 구현되어 들어가 있는지 확인해보자. 

 

 

ExceptionTranslationFilter 에는 어떤 구현체들이 들어가 있는가?

 

 

드디어 베일이 벗겨지기 시작한다. AuthenticationEntryPoint 의 구현체로는 Http403ForbiddenEntryPoint.java 가 들어가 있었고, 해당 EntryPoint 에서 Authentication 예외를 401 이 아닌 403 으로 처리해주고 있는 것이였다. 

 

 

그렇다면 정말 인가가 부족할 경우에는 어떨까? 그럴 경우 바로 위 코드 사진인 handleAccessException() 에서 this.accessDeniedHandler.handle() 로 이동했을 것이다. 그리고 AccessDeniedHandler 는 컨테이너에서 주입하지 않고 바로 생성해서 사용하는데, AccessDeniedHandlerImpl.java 를 사용한다. 이 클래스 역시 들어가서 확인하면, response 에 FORBIDDEN 으로 설정해주는 모습을 확인할 수 있다. 

 

 

즉, EntryPoint 로 인증 예외가 발생하든, AccessDeniedHandler 가 처리해야할 인가 예외가 발생하든, 모두 403 Forbidden 으로 처리하기 때문에 제일 처음에 내가 401을 예상한 요청도 403 으로 응답이 되는 것이였다. 

 

 

왜 그럴까? ROLE_ANONYMOUS 를 만들어준 상태이기 때문이다. 미인증 객체는 ROLE_ANONYMOUS 라는 권한을 주어서 엄밀히 말하면 권한이 확인된 상태이지만 권한이 부족한 상태이기 때문에, 두 상황 모두 403 Forbidden 으로 응답을 받는 것이라 이해를 하였다. 논리적으로 따지면 그렇다 치는데, 왜 그럼 Anonymous 까지 사용해가며 이렇게 할까? 이 부분은 사실 잘 확인하기가 어려웠으나, 질문 같은걸 확인한 결과 Spring 에서 애초에 AuthenticationToken 이 없는 상태로 요청 Session 을 사용하는 것을 허용하지 않겠다는 의미이기도 하다고 한다. 챗 지피티는 다음과 같이 설명한다 (믿거나 말거나..)

 

 

챗지피티의 403 처리에 대한 이유 설명

 

 

 

 

 

** 모든 출처 **

 

 

일단 별로 없음.. 걍 다 따라가보면서 확인함. 

 

 

- Anonymous 객체에 대하여

https://www.inflearn.com/questions/524031/anonymous-%EC%9C%A0%EC%A0%80%EB%8F%84-authentication-%EA%B0%9D%EC%B2%B4%EA%B0%80-%EC%9E%88%EB%8A%94%EB%8D%B0-authenticationexception%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%82%98%EC%9A%94

 

Anonymous 유저도 Authentication 객체가 있는데 AuthenticationException이 발생할 수 있나요? - 인프런 | 질문

안녕하세요 강의 감사히 잘 보고 있습니다. SpringSecurity를 자세히 설명해주는 강의가 이만한게 없네요 ㅎㅎ다름이 아니라 FilterSecurityInterceptor 에서 Authentication 객체가 null인 경우 AuthenticationExceptin

www.inflearn.com

 

728x90
반응형

'Spring > Spring Security' 카테고리의 다른 글

Ajax로 새로운 Security 설정을 연습해보자  (0) 2022.12.14
[Spring Security] 1 - 무작정 시작하는 Security와 개요  (0) 2022.11.25
'Spring/Spring Security' 카테고리의 다른 글
  • Ajax로 새로운 Security 설정을 연습해보자
  • [Spring Security] 1 - 무작정 시작하는 Security와 개요
문케이크
문케이크
    반응형
  • 문케이크
    누구나 개발할 수 있다
    문케이크
  • 전체
    오늘
    어제
    • 전체 보기 (122)
      • CS 이론 (13)
        • 운영체제 (8)
        • 네트워크 (2)
        • 알고리즘 (0)
        • Storage (3)
      • Spring (26)
        • Spring 기본 (12)
        • Spring 심화 (0)
        • JPA (11)
        • Spring Security (3)
      • 리액티브 (0)
        • RxJava (0)
      • SW 설계 (14)
        • OOP (0)
        • UML (3)
        • OOAD (0)
        • Design Pattern (11)
      • Java (8)
      • 웹 운영 (17)
        • AWS (15)
        • 운영 구축 (2)
      • Testing (3)
        • Unit (3)
      • Extra (3)
        • API 적용 (1)
      • 인프라 기술 (5)
        • Kubernetes (2)
        • Elasticsearch (3)
      • Logging (7)
        • Spring (5)
        • 인프라 (2)
      • 일상 (2)
        • 음식점 리뷰 (2)
        • Extra (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 문케이크의 블로그
  • 인기 글

  • 태그

    Composite
    elasticsearch
    spring boot
    k8s
    spring container
    디자인 패턴
    di
    OOAD
    BEAN
    analyzer
    Design Pattern
    Configuration
    lombok
    Spring
    김영한
    runtime exception
    composition
    GoF
    Setter
    SRP
    mockito
    decorator
    JPA
    junit
    lazy loading
    n+1
    OOP
    Java
    단위테스트
    객체지향
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
문케이크
[Spring Security] 아니 왜 Forbidden 만 계속 오지??
상단으로

티스토리툴바