반응형

1. 개요

 스프링 시큐리티에서 제공하는 OAuth2Login을 통해 사용자 정보를 받아오고, 자체적으로 JWT 토큰을 발급하고 있다. JwtAuthorizationFilter에서 JWT 토큰에 대한 인증 처리 로직을 구현하였고, 구글링을 통해 필터의 위치를 UsernamePasswordAuthenticationFilter 이후로 설정하였다.

 요청마다 JWT Filter가 호출되긴 했지만 막상 Security Filter 로그를 통해 Security Filter Chain 리스트를 보니  UsernamePasswordAuthenticationFilter 가 보이지 않았다. 😲

눈씻고 찾아봐도 없는 UsernamePasswordAuthenticationFilter

 이 필터가 없는 이유없는 필터에 커스텀 필터가 추가되는 것이 이해 되지 않았다. 이 이유를 알아보자.


2. UsernamePasswordAuthenticationFilter 가 없는 이유

 이유는 매우 간단했다. UsernamePasswordAuthenticationFilter는 클라이언트에서 요청한 username과 password를 통해 인증을 처리하는 필터이다. formLogin 시 UsernamePasswordAuthenticationFilter 가, OAuth2Login시 OAuth2AuthorizationRequestRedirectFilter와 OAuth2LoginAuthenticationFilter 가 추가된다.

OAuth2Login 을 사용하므로 UsernamePasswordAuthenticationFilter 가 없는건 매우 당연했다. 

oauth2Login


3. OAuth2AuthorizationRequestRedirectFilter와 OAuth2LoginAuthenticationFilter

 OAuth2AuthorizationRequestRedirectFilter는 OAuth2 서비스 제공 서버의 로그인 페이지를 호출하는 필터이다. 기본 제공하지 않는 Naver나 Kakao의 로그인 페이지는 yml 설정한 정보를 조합하여 uri를 만든 후 호출한다.

 

 OAuth2LoginAuthenticationFilter는 AbstractAuthenticationProcessingFilter의 서브클래스로 RedirectURI를 통해 받은 AuthorizationCode토큰 인증 API를 호출하여 accessToken 및 refreshToken을 받아오고, 이를 통해 유저 정보 조회 API를 호출하여 유저 정보도 받아온다. 관련 로직은 아래와 같다.

 

1) getAuthenticationManager().authenticate() 메서드를 호출

OAuth2LoginAuthenticationFilter 의 AuthorizationCode를 통한 인증 메서드

 

2) OAuth2LoginAuthenticationProvider 클래스의 authorizationCodeAuthenticationProvider.authenticate() 메서드 호출하여 accessToken과 refreshToken 취득

token 취득 부분

 

3) OAuth2LoginAuthenticationProvider 클래스의 userService.loadUser() 메서드 호출하여 유저 정보 취득 (OAuth2UserService 를 재정의하여 사용하기도 함.)

유저 정보 취득 부분


4. Filter Chain에 없는 AbstractAuthenticationProcessingFilter 

 디버깅을 하면서 추적해 나가다보니 AbstractAuthenticationProcessingFilter 클래스의 특정 메서드가 호출되는 부분이 있었다. 이 필터에서는 요청 URI를 추출하여 OAuth2 서비스에 대한 redirectURI로 온 요청일 경우 attemptAuthentication 메서드를 통해 OAuth2LoginAuthenticationFilter의 인증 처리를 하고 있었다. 그런데 이 필터는 Security Filter Chain 리스트에 없다. 이 녀석의 정체는 뭘까? 🤔

AbstractAuthenticationProcessingFilter 코드 일부

 

 스프링 공식문서를 보면 아래와 같이 해당 클래스의 서브 클래스 리스트가 나온다. 그런데 아주 눈에 익은 클래스가 보인다. 그렇다. OAuth2LoginAuthenticationFilter가 이 클래스의 서브클래스였다.  😲

AbstractAuthenticationProcessingFilter

Direct Known Subclasses:
CasAuthenticationFilter, OAuth2LoginAuthenticationFilter, Saml2WebSsoAuthenticationFilter, UsernamePasswordAuthenticationFilter

 

 필터를 신경써서 봤다면 OAuth2LoginAuthenticationFilter는 이 클래스를 상속받고 있고, 추상 메서드인 attemptAuthentication 를 구현함을 알 수 있었을것이다. 참고로 OAuth2LoginAuthenticationFilter는 Security Filter Chain 목록에 있다.

OAuth2LoginAuthenticationFilter


5. UsernamePasswordAuthenticationFilter 가 없어도 JwtAuthorizationFilter 가 추가된 이유

 두번째로 궁금했던 UsernamePasswordAuthenticationFilter 가 없어도 커스텀한 필터가 추가된 이유를 알아보았다. 추가에 사용한 메서드는 addFilterAfter() 이며 Security 설정 부분에 아래와 같이 사용하였다.

.addFilterAfter(new JwtAuthorizationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);

 

 결론부터 말하면 스프링 시큐리티에서 필터를 유연하게 추가할 수 있도록 내부 로직이 구성되어 있기 때문이었다. UsernamePasswordAuthenticationFilter 처럼 실제로 사용하지 않는 필터라도 말이다.

 

 아래 코드는 addFilterAfter 메서드를 실행했을 때 FilterOrderRegistration 클래스의 getOrder 메서드를 통해 UsernamePasswordAuthenticationFilter의 위치를 Integer로 추출하는 로직이다.

필터의 순서를 조회하는 getOrder

 

 filterToOrder은 필터 클래스 이름과 순서를 Map 타입으로 관리하는 변수이다. 이는 FilterOrderRegistration 클래스의 생성자 메서드를 통해 생성된다. 여기서 중요한 점은 이 값은 순수하게 필터의 순서를 관리하기 위해 존재한다는 것이다.

필터의 클래스명과 순서를 관리하는 filtersToOrder

 

 위 로직을 통해 UsernamePasswordAuthenticationFilter가 1900번째 필터임을 알았으며, 해당 값에 offset 인 1을 추가하여 새로 추가될 필터인 JwtAuthorizationFilter를 1901번 필터로 추가하였다. 즉, UsernamePasswordAuthenticationFilter의 바로 다음 순서의 필터로 JwtAuthorizationFilter를 지정한 것이다.

그 후 filters에 JwtAuthorizationFilter와 1901 정보를 갖는 OrderedFilter 타입 인스턴스를 추가하였다.

filters가 실제 어플리케이션에서 사용될 필터들을 관리한다.

filters에 추가되는 부분

 

 최종적으로 등록된 filters 리스트를 살펴보면 filter와 order 필드를 갖는 OrderedFilter 타입의 객체들이 있으며, order를 오름차순으로 정렬해보면 이 포스팅의 제일 첫 그림과 동일한 순서를 갖는 필터 리스트임을 확인할 수 있다. 이러한 내부 로직에 의해 실제로 사용하지 않는 필터에 대해 addFilterAfter와 같은 메서드를 사용하여 필터를 추가해도 에러가 발생하지 않았던 것이었다.

filters 리스트


6. JwtAuthrizationFilter의 위치

 위를 근거로 하여 JwtAuthrizationFilter는 OAuth2LoginAuthenticationFilter 다음으로 수정하였다. 

.addFilterAfter(new JwtAuthorizationFilter(jwtProvider), OAuth2LoginAuthenticationFilter.class);

7. 회고

 단순히 UsernamePasswordAuthenticationFilter가 왜 없지? 라는 단순한 호기심으로 시작했지만 내부 코드를 까보며 OAuth2 인증이 스프링 내부에서 어떻게 처리되는지, 필터는 어떻게 구성되는지에 대해 이해하게 되었고, 1,2년 전쯤 스프링 시큐리티의 폼 인증에 대한 내부 로직에 대해 정리한 적이 있는데, 이를 한번 더 상기하게 되었고, 모든 코드에는 근거가 있어야함을 다시한번 느끼게된 좋은 계기가 되었다.

반응형

+ Recent posts