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/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 {
...
}
Security Filter Chain 의 마지막인 ExceptionTranslationFilter 와 FilterSecurityInterceptor 이 인증 / 인가 예외 처리를 최종적으로 진행 하는 곳이기 때문에 해당 클래스들을 살펴보았다.
ExceptionTranslationFilter 는 도달하자마자 후속 chain 으로 우선 넘기는 모습을 확인할 수 있다. 후속 filter 로는 FilterSecurityInterceptor.java 가 걸리게 되는데, 그 이유는 내가 Method 별 annotation 으로 인가를 사용하는 것이 없어서 그렇다. 만약 그렇다면 MethodSecurityInterceptor 가 걸리게 된다. (둘다 AbstractSecurityInterceptor 를 상속받는 클래스들이다)
FilterSecurityInterceptor class 도 보면 invoke() 라는 함수를 수행하게 되고, invoke() 를 수행할 때 자신의 부모 함수인 AbstractSecurityInterceptor.java 의 함수들을 통해 invoke 를 진행하는 것을 확인할 수 있다 (beforeInvocation(), afterInvocation() 등 모두 부모 함수를 사용).
갑자기 Library Code 를 붙여 놓으니 나가실 수도 있겠지만, 그냥 내가 BreakPoint 건 곳만 확인하면 된다. 해당 함수에서는 MetaDataSource 에 담겨진 권한과, 현재 SecurityContext 안에 들어있는 인증 정보를 확인하는 것으로 보이고, 인증 정보가 비어있지 않다면 attemptAuthorization - [권한 확인]을 시도하는 것으로 보인다.
위의 상황으로 돌아가보자. 나는 인증이 필요한 경로에 인증 정보를 아무 입력하지 않은채로 요처을 던졌다. 그렇다면 첫 예상은 여기서 인증된 객체가 없으니, 두번째 null point 에서 걸리겠구나! 였다. 하지만 내 생각이 짧아서였는지, 해당 부분을 보기 좋게 통과하고 attemptAuthorization() 까지 내려가는 모습을 확인할 수 있었다.
Security 를 한번이라도 공부했으면 당연히 알겠지만, Spring Security 는 인증되지 않으면 위 필터 사진 중 AnonymousAuthenticationFilter.java 란 곳에서 Anonymous 인증 객체를 SecurityContext 안에 넣어준다. (참고로 SecurityContextHodler 는 ThreadLocal 에 할당되어 해당 요청 Session 도중 언제든 사용할 수 있게 한다)
또한, 여기서 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 을 통과하면서 발생한다.
여기서 AccessDecisionManager 가 decide() 를 수행하면, Voter 들이 권한이 충분한지를 판별하고, 통과 여부를 결정짓게 되는데, ROLE_ANONYMOUS 를 가지고는 authenticatied() path 를 통과할 수 없기 때문에, 해당 decide 시 exception 을 던지게 된다. 이에 따라 AccessDeniedException 을 바로 잡아서 던지게 된다 (AuthorizationFailureEvent 도 등록해준다)
이는 Java Exception 에 따라 더 이상 후속을 진행하지 않고, 호출한 클래스로 넘기기 때문에 FilterSecurityInterceptor, 여기서 catch 하지 않으므로 ExceptionTranslationFilter 로 다시 올라가서 해당 클래스에서 다시 이 예외를 잡게 된다. 다시 맨 위에 첫 코드로 돌아가게 되는 것이다. 다만 AccessDeniedException 을 잡은 상황이다.
사진에 설명해 놓은 바와 같이 진행이 되며, 다음 handleSpringSecurityException 으로 그대로 전달이 된다. handleSpringSecurityException() 은 단순히 AccessDeniedException 임을 확인하고, 다시 handleAccessDeniedException() 으로 전달하게 되는데, 이 부분에서 중요한 일이 발생한다
InsufficeintAuthenticationException 은 sendStartAuthentication() 으로 전달하는데, 해당 함수에서는 SecurityContextHolder 를 초기화시킨 이후, AuthenticationEntryPoint 의 commence 함수를 호출하게 된다. 이 AuthenticationEntryPoint 는 Authentication 즉, 인증을 핸들링 하는 도중 발생하는 예외를 처리하는 곳이다. 이 역할체에 무엇이 구현되어 들어가 있는지 확인해보자.
드디어 베일이 벗겨지기 시작한다. 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 을 사용하는 것을 허용하지 않겠다는 의미이기도 하다고 한다. 챗 지피티는 다음과 같이 설명한다 (믿거나 말거나..)
** 모든 출처 **
일단 별로 없음.. 걍 다 따라가보면서 확인함.
- Anonymous 객체에 대하여
Anonymous 유저도 Authentication 객체가 있는데 AuthenticationException이 발생할 수 있나요? - 인프런 | 질문
안녕하세요 강의 감사히 잘 보고 있습니다. SpringSecurity를 자세히 설명해주는 강의가 이만한게 없네요 ㅎㅎ다름이 아니라 FilterSecurityInterceptor 에서 Authentication 객체가 null인 경우 AuthenticationExceptin
www.inflearn.com
'Spring > Spring Security' 카테고리의 다른 글
Ajax로 새로운 Security 설정을 연습해보자 (0) | 2022.12.14 |
---|---|
[Spring Security] 1 - 무작정 시작하는 Security와 개요 (0) | 2022.11.25 |