항해 플러스 6기 프레임워크 사전 스터디에서 개별적으로 진행하는 API를 개발하고 있다.
실행 환경
SpringBoot 3.3.x
Java 21
Member가 로그인(SignIn)할 때 Spring Security를 사용해서 인증을 해보기로 했다.
- 우선 Spring Security가 간단하게 무엇인지 알아보고, 아키텍처로 동작 과정을 살펴본 후
로그인 인증 구조도 아키텍처로 동작 과정 살펴보자.그 다음에는 구현을 해보고, 시행착오 및 해결에 대해 써보자.
Spring Security
Wikipedia를 보면 Spring Security는 Java 애플리케이션에 인증, 권한 부여 및 기타 보안 기능을 제공하는 프레임워크다.
여기서 인증, 인가(권한 부여)에 대해 살펴보고 가자.
인증(Authentication), 인가(Authorization)
인증(Authentication)은 사용자가 자신의 신원을 확인하는 프로세스다.
예를 들어 시스템에 로그인하려면 ID를 확인하는 과정이다.
인가(Authorization)는 사용자에게 특정 액세스 권한을 부여하는 프로세스다.
예를 들어 사용자가 액세스 할 수 있는 리소스 또는 서비스를 결정하는 과정이다.
인증과 인가의 차이점을 알아보자.
리소스에 대한 액세스 권한을 얻으려면 인증과 인가가 모두 필요하지만, 이는 프로세스에서 고유한 두 단계이다.
인증은 키를 통해 사용자의 신원을 확인하는 과정이고,
인가는 그 키가 실제로 액세스 권한을 부여하는지 여부를 결정하는 과정이다.
따라서 인증은 사용자에 의해 시작되는 반면, 인가는 정책에 의해 결정되고 액세스 되는 애플리케이션, 시스템, 리소스에 의해 제공된다.
Spring Security Architecture
이제 Servlet 기반 애플리케이션 내에서 Spring Security 아키텍처에 대해 알아보자.
Filter
Spring Security의 Servlet 지원은 Servlet Filter에 기반을 두고 있으므로, 먼저 Filter의 역할을 살펴보자.
아래 그림은 단일 HTTP 요청에 대한 핸들러의 일반적인 계층화를 보여준다.
클라이언트가 애플리케이션에 요청을 보내면, 서블릿 컨테이너는 FilterChain을 생성한다.
이 FilterChain은 요청 URI 경로에 따라 HttpServletRequest를 처리해야 하는 Filter 인스턴스와 Servlet을 포함한다.
Spring MVC 애플리케이션에서 Servlet은 DispatcherServlet의 인스턴스이다.
따라서 하나의 HttpServletRequest와 HttpServletResponse는 최대 한 개의 Servlet에 의해 처리될 수 있다.
그러나 여러 개의 Filter가 사용될 수 있으며, 이를 통해 아래와 같은 작업을 수행할 수 있다.
- 하위 Filter 인스턴스나 Servlet이 호출되지 않도록 방지한다.
이 경우, Filter는 일반적으로 HttpServletResponse를 작성한다. - 하위 Filter 인스턴스와 Servlet이 사용하는 HttpServletRequest나 HttpServletResponse를 수정한다.
Filter와 DispatcherServlet
Filter와 DispatcherServlet에 대한 흐름을 확인해 보자.
Tomcat과 같은 웹 애플리케이션을 서블릿 컨테이너라고 부르는데, 기본적으로 Filter와 DispatcherServlet으로 구성되어 있다.
Spring Security가 적용되기 전인 아래 그림을 보자.
- 클라이언트가 애플리케이션에 요청을 보내면
- Filter를 차례대로 거친 다음
- URL에 따라 Servlet이 분기되고, DispatcherServlet은 요청을 적절한 Controller로 라우팅 한다.
Servlet : HTTP 요청과 응답을 직접 처리하는 역할
DelegatingFilterProxy
Spring은 DelegatingFilterProxy라는 Filter 구현체를 제공하는데,
이를 통해 Servlet 컨테이너의 생명주기와 Spring의 ApplicationContext 간의 연결이 가능하다.
Servlet 컨테이너는 자체 표준을 사용해 Filter 인스턴스를 등록할 수 있지만,
Spring에서 정의된 Bean에 대해서는 인식하지 못한다.
DelegatingFilterProxy를 표준 Servlet 컨테이너 메커니즘을 통해 등록할 수 있으며, 모든 작업을 Filter를 구현한 Spring Bean에게 위임할 수 있다.
DelegatingFilterProxy를 활용하면 SpringContext에 정의된 Bean을 Filter로 사용할 수 있게 해준다.
아래 그림은 DelegatingFilterProxy가 Filter 인스턴스와 FilterChain 내에서 어떻게 작동하는지 보여준다.
FilterChainProxy
Spring Security의 Servlet 지원은 FilterChainProxy에 포함되어 있다.
FilterChainProxy는 Spring Security에서 제공하는 특별한 Filter로, 여러 Filter 인스턴스에 대한 위임을 SecurityFilterChain을 통해 처리할 수 있게 해준다.
FilterChainProxy는 Bean이기 때문에 일반적으로 DelegatingFilterProxy로 래핑된다.
Spring Security는 DelegatingFilterProxy에 의해 Bean으로 등록된 FilterChainProxy를 활용해 SecurityFilterChain에 있는 Filter들을 사용할 수 있다.
이대로 사용하면 Proxy를 2번 활용하게 된다. 왜 2번 활용할까? (SecurityFilterChain 제목에서 확인)
아래 그림은 FilterChainProxy의 역할을 보여준다.
SecurityFilterChain
SecurityFilterChain은 FilterChainProxy가 현재 요청에 대해 어떤 Spring Security Filter 인스턴스가 호출되어야 하는지를 결정하는 데 사용된다.
아래 그림은 SecurityFilterChain의 역할을 보여준다.
SecurityFilterChain에 있는 SecurityFilter는 일반적으로 Bean으로 정의되지만, DelegatingFilterProxy가 아니라 FilterChainProxy에 등록된다.
DelegatingFilterProxy 외에 FilterChainProxy도 사용해서 Proxy를 2번 활용하게 되는 이유가 나온다.
FilterChainProxy는 Servlet 컨테이너나 DelegatingFilterProxy와 직접 등록하는 것보다 아래와 같은 장점을 제공한다.
- SpringSecurity의 Servlet 지원의 출발점을 제공한다.
따라서 문제 해결 시, FilterChainProxy에 디버그 포인트를 추가해서 빠른 오류 수정이 가능하다. - SpringSecurity 사용의 중심이므로, 선택적이지 않은 작업을 수행할 수 있다.
예를 들어 메모리 누수를 방지하기 위해 SecurityContext를 정리하거나 HttpFirewall을 적용하여 특정 유형의 공격으로부터 애플리케이션을 보호한다. - SecurityFilterChain이 언제 호출되어야 하는지 결정하는 데 더 많은 유연성을 제공한다.
Servlet 컨테이너에서는 Filter 인스턴스가 매핑된 URL에 따라 호출되지만,
FilterChainProxy는 RequestMatcher 인터페이스를 사용하여 HttpServletRequest의 어떤 요소든 기반으로 호출을 결정할 수 있다.
Multiple SecurityFilterChain
다중 SecurityFilterChain에서 FilterChainProxy는 어떤 SecurityFilterChain이 사용되어야 하는지 결정하는데,
매칭되는 첫 번째 SecurityFilterChain만 호출된다.
예를 들어 요청된 URL이 "/api/messages/"인 경우, 이 URLdms "/api/**" 패턴과 매칭되는 SecurityFilterChain0에 먼저 일치하므로, SecurityFilterChain0만 호출된다.
각 SecurityFilterChain을 보면 SecurityFilter 인스턴스를 각각 3개, 4개로 다르게 구성되어 있다.
즉, SecurityFilterChain은 독립적으로 구성될 수 있으며, 특정 요청을 무시하도록 SpringSecurity를 설정할 수 있다.
Handling Security Exceptions
ExceptionTranslationFilter는 AccessDeniedException과 AuthenticationException을 HTTP 응답으로 변환하는 역할을 한다.
이 ExceptionTranslationFilter는 FilterChainProxy에 Security Filter 중 하나로 들어간다.
아래 그림은 ExceptionTranslationFilter가 다른 구성 요소들과 어떻게 관계되는지 보여준다.
- ExceptionTranslationFilter는 `FilterChain.doFilter(request, response)`를 호출하여 나머지 애플리케이션을 실행한다.
- 사용자가 인증되지 않았거나 AuthenticationException이 발생한 경우
- Authentication을 시작한다.
- SecurityContextHolder를 지운다.
- HttpServletRequeset를 저장하여 인증이 성공하면 원래의 요청을 재전송할 수 있도록 한다.
- AuthenticationEntryPoint를 사용하여 클라이언트에게 자격 증명을 요청한다.
예를 들어 로그인 페이지로 redirect 할 수 있다.
- 인증에 실패하여 AccessDeniedExcpetion이 발생하면 접근이 거부된 상태로 AccessDeniedHandler가 호출되어 접근 거부를 처리한다.
Spring Security Full Architecture
위에서 Filter와 DispatcherServlet의 흐름을 보기 위해 Servlet 컨테이너 흐름을 그림으로 확인해 봤다.
그 그림에 Spring Security가 적용된 전체 그림을 확인해 보자.
도움 받은 곳
https://www.entrust.com/ko/resources/learn/authentication-vs-authorization
https://docs.spring.io/spring-security/reference/servlet/architecture.html
https://youseong.me/auth/skillboard/details/33