스프링 부트 3.0.6 버전으로 진행한 잇츠무비타임 프로젝트를 리팩토링 하다가
과거 토큰을 검증하는 JwtAuthenticationProcessingFilter 만 건너뛰게 아래와 같이 설정했던 것을
WebSecurity 의 WebSecurityCustomizer를 이용해서 스프링 시큐리티를 적용하지 않을 리소스로 변경하고자하였다.
하지만 소셜로그인을 진행하는데 WebSecurityCustomizer를 통해 해당 oauth 관련 url 에 대해 스프링 시큐리티가 적용이 되지 않아 아예 리소스를 판단하지 못하는 것을 확인하였다.
처음에는 단순히 JwtAuthenticationEntryPoint 로 가서 기존 에러 처리한 부분의 로직을 타서 어디서 에러가 났는지 몰랐다가
설정파일의 로깅 레벨을 debug 로 바꿔 아래와 같은 오류를 찾았다
스프링 시큐리티 보안 관련 작업을 거치지 않기 때문에 oauth 관련 기능이 작동하지 않는 것이었다!!
따라서 문제의 원인을 찾아 WebSecurityCustomizer 파트를 주석처리해놓고 실행시켜보았다
그랬더니 예상대로 올바르게 소셜로그인 페이지가 나오는 것을 확인했다
문제를 해결했으면 이제 문제의 원인이었던 WebSecurity 에 대해 자세히 알아보자
WebSecurity는 HttpSecurity의 상위에 있다. WebSecurity의 ignoring에 endpoint를 만들면, Security Filter Chain이 적용되지 않는다.
이 경우 Cross-Site Scripting, XSS 공격, content-sniffing에 취약해진다고 한다
즉 아래 그림에서 HttpSecurity를 통해 실제 필터가 생성되고 이 필터들이 WebSecurity 클래스에게 전달이 되고,
WebSecurity는 각각 설정 클래스로 부터 필터 목록들을 전달받고, 다시 FilterChainProxy의 생성자의 인자로 전달하는 과정에서 필터체인이 적용되지 않는다고 이해하면 될 것 같다
HttpSecurity: 실제 필터를 생성하는 클래스
WebSecurity: 각각 설정 클래스로 부터 필터 목록들을 전달받고, 다시 FilterChainProxy의 생성자의 인자로 전달
위에서 말한 것처럼 WebSecurity는 각각 설정 클래스로 부터 필터 목록들을 전달받고, 다시 FilterChainProxy를 생성자의 인자로 전달하는데
이때 FilterChainProxy는 각각의 설정 클래스 별(SecurityConfig1, SecurityConfig2)로 필터 목록들을 아래와 같이 가지고 있다
다시 사용자 요청 후의 상황으로 돌아가자
사용자가 처음 요청을 하면 그림처럼 DelegatingFilterProxy가 먼저 요청을 받아 FilterChainProxy에게 요청을 위임한다
DelegatingFilterProxy: 서블릿 필터이며 위임을 할 때에는
springSecurityFilterChain
이라는 이름을 가진 Bean을 찾게 되는데, 그 Bean이 바로 FilterChainProxy이다
FilterChainProxy: 초기화될 때 이미 빈으로 등록되며 필터 목록을 지닌다
이제 위임받은 요청을 가지고 있는 각각의 Filter들에게 순서대로 요청을 맡긴다
이 때 각각의 필터들이 체인으로 연결되어 수행 → 넘김 → 수행 → 넘김으로 진행되는 형태이고 이 때, 수행되는 메소드가 doFilter이다
이렇게 초기화 과정과 사용자 요청 후의 시큐리티에서의 상황을 알아보았다
이제 FilterChain 에 대해 알아보자
SecurityContext 객체를 생성하고, 세션에 저장한다.
즉, 세션에 저장된 SecurityContext를 조회하고 참조하는 클래스이다.
처음 인증인지 아닌지에 따라 새로 SecurityContext를 생성할지, 아니면 기존의 것을 가져올지 결정한다.
로그아웃 관련 필터
ID와 Password를 사용하는 실제 Form 기반 유저 인증을 처리한다
한편 spring Security에서는 일반 Form Login을 기본적으로 제공하지만,
JSON 형식의 RequestBody로 하는 로그인 방식은 기본적으로 제공하지 않는다.
따라서, JSON 로그인 방식을 사용하기 위해 커스텀 필터를 구현해야한다
이때, Form Login 시 기본적으로 사용되는
UsernamePasswordAuthenticationFilter의 코드를 참고하면된다
나는 Form Login 시 기본적으로 사용되는 UsernamePasswordAuthenticationFilter에서
AbstractAuthenticationProcessingFilter를 상속받아 구현하기 때문에,
커스텀 JSON 필터에서도 AbstractAuthenticationProcessingFilter를 상속받아 구현하였다
📎 https://ksh-coding.tistory.com/60
한편 이 필터에서는 인증 객체를 만들어서 Authentication 객체를 만들어 아이디 패스워드를 저장하고, AuthenticationManager에게 인증처리를 맡긴다
Authentication?
Authentication객체는 인증 시 id 와 password 를 담고 인증 검증을 위해 전달되어 사용된다
인증 후 최종 인증 결과 (user 객체, 권한정보) 를 담고 SecurityContext 에 저장되어 전역적으로 참조가 가능하다
사용자의 인증 정보를 저장하는 토큰이라고 생각하면 좋다
또 한편
AuthenticationManager가 실질적인 인증을 검증 단계를 총괄하는 클래스인 AuthenticationProvider에게 인증 처리를 위임한다. 그럼 AuthenticationProvider가 UserDetailsService와 같은 서비스를 사용해서 인증을 검증한다.
최종적으로
인증을 성공한 경우, 인증에 성공한 결과를 담은 인증객체(Authentication)를 생성한 다음에 SecurityContext에 저장한다
현재 사용자 계정으로 인증을 받은 사용자가 두 명 이상일 때 실행되는 필터
세션이 만료되거나 무효화되어서 세션안에 있는 SecurityContext 내의 인증 객체가 null일 경우 해당 필터가 작동한다
사용자 정보가 인증되지 않았다면 익명 사용자 토큰을 반환한다
로그인 후 Session과 관련된 작업을 처리
필터 체인 내에서 발생되는 인증, 인가 예외를 처리
인증 이후 자원에 접근할 때 가장 큰 역할을 하는 필터라고 한다
인가 관련
이러한 필터들 이외에도 OAuth2AuthenticationFilter가 OAuth2 로그인을 처리하며, 이를 통해 소셜 로그인 등을 수행한다. 따라서 webSecurityCustomizer 로 oauth 관련 url 에대해 보안 작업을 하지 않게 설정한다면 소셜로그인 기능이 잘 작동하지 않는 다는 것을 알 수 있다
스프링 시큐리티에서 permitall 에 등록하는 url 을 JwtAuthenticationProcessingFilter에서 만나면 건너뛰게 하여 토큰 검사 로직을 건너뛰게 할 것이다