본문으로 바로가기
반응형

Servlet Authentication Architecture

본 섹션에서는 서블릿 인증에 사용되는 스프링 시큐리티의 주요 아키텍처 요소에 대해 설명한다. 이러한 요소들이 어떻게 결합되는지 설명하는 구체적인 흐름은 인증 섹션을 통해 설명한다. 아래의 요소들은 인증 관련한 주요 요소들에 대한 간략한 설명이다.

- SecurityContextHolder: 인증 정보를 저장하는 공간이다.

- SecutiryContext: SecurityContextHolder를 통해 획득되며 현재 인증된 유저의 Authentication 정보를 담고 있다.

- Authentication: 인증을 위해 사용자가 제공한 정보 또는 SecutiryContext의 현재 사용자 정보를 제공하기 위해 AuthenticationManager의 인풋으로 사용할 수 있다.

- GrantedAuthority: Authentication에게 허용된 권한 정보를 나타낸다.

- AuthenticationManager: 스프링 시큐리티 필터가 인증을 수행하는 방법을 나타내는 API이다.

- ProviderManager: AuthenticationManager의 가장 일반적인 구현체이다.

- AuthenticationProvider: 특정한 종류의 인증을 위해 ProviderManager로부터 사용되는 클래스이다.

- Request Credentials with AuthenticationEntryPoint: 클라이언트의 자격 증명을 요청하는데 사용된다.

- AbstractAuthenticationProcessingFilter: 인증을 위한 기본 필터이다.

SecurityContextHolder

스프링 시큐리티 인증의 핵심은 SecurityContextHolder이다. 이는 내부에 SecurityContext를 포함하고 있다.

SecurityContextHolder에는 인증된 사용자의 정보가 들어간다. 스프링 시큐리티는 SecurityContextHolder가 어떻게 채워지는지 전혀 신경쓰지 않는다. 단순히 안에 데이터가 존재한다면 해당 정보를 인증된 유저를 다룰 때 사용할 뿐이다. 

SecurityContext context = SecurityContextHolder.createEmptyContext();  // 1
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER");  // 2
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context);  // 3

1. 빈 Context를 생성한다. SecurityContextHolder.getContext().setAuthentication(authentication) 코드보다 위처럼 직접 SecurityContextHolder를 생성하는것이 더 좋은데, 이는 멀티스레드 환경에서 발생할 수 있는 레이스 컨디션을 방지하기 위함이다.

2. 다음으로 새로운 Authentication을 생성한다. 스프링 시큐리티는 어떠한 타입의 Authentication이 SecurityContext에 저장되는지 신경쓰지 않는다. 위 예제에서는 TestingAuthenticationToken을 사용했는데 이는 가장 간단한 예제이기 때문이다. 프로덕션 레벨에서 좀 더 흔한 예제는 UsernamePasswordAuthenticationToken(userDetails, password, authorities)이다. 

3. SecurityContextHolder에 SecurityContext를 저장한다. 스프링 시큐리티는 인증을 처리할 때 이 정보를 사용한다.

인증 주체(principal)에 대한 정보를 얻을때는 아래와 같이 접근한다.

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

기본적으로 SecurityContextHolder는 이러한 세부 정보를 저장하기위해 ThreadLocal을 사용한다. 이는 SecurityContext가 해당 메소드에 명시적인 인자로 전달되지 않더라도 동일한 스레드에서 항상 동일한 SecurityContext에 접근할 수 있음을 의미한다. 이러한 바식으로 ThreadLocal을 사용하는 것은 현재 인증 주체의 요청이 처리된 후 스레드를 삭제할 떄 상당히 안전한 방법을 제공한다. 참고로 스프링 시큐리티는 FilterChainProxy에서 SecurityContext를 항상 지워준다.

스레드와 긴밀하게 작업하는 몇몇의 어플리케이션에서는 ThreadLocal이 적합하지 않을수도 있다. 예를 들어 Swing 클라이언트는 모든 스레드에서 동일한 시큐리티 Context를 필요로 할 수 있다. 이러한 경우 SecutiryContextHolder의 strategy를 SecurityContextHolder.MODE_GLOBAL로 조정해주면 된다. 

SecurityContext

SecurityContext는 SecurityContextHolder를 통해 획득된다. 그리고 이는 Authentication 정보를 포함하고 있다.

Authentication

Authentication 인터페이스는 아래의 2가지 목적을 가진다.

- AuthenticationManager의 인자로써 사용자가 제공한 자격 증명을 제공하기 위해 사용된다. 이 시나리오에서 사용되는 isAuthenticated()의 경우 false를 반환한다.

- 현재 인증된 유저를 나타낸다. SecurityContext를 통해 현재 Authentication 정보를 가져올 수 있다.

Authentication은 아래의 항목들을 포함한다.

- principal: 유저에 대한 식별자. username/password를 통해 인증을 수행하는 경우 이 값은 보통 UserDetails 인스턴스가 된다.

- credentials: 대개 패스워드이다. 대부분의 경우 사용자가 인증된 후에 값을 지워줘서 유출되지 않도록 한다.

- authorities: GrantedAuthority 인스턴스는 사용자에게 부여된 고수준 권한이다. roles 또는 scopes가 그 예이다.

GrantedAuthority

GrantedAuthority 인스턴스는 유저 인증에 관한 고수준 인증 정보이다. roles와 scopes가 그 예이다.

GrantedAuthority는 Authentication.getAuthorities() 메소드를 통해 획득할 수 있다. 해당 메소드는 GrantedAuthority를 컬렉션 타입으로 제공한다. 이는 주체에게 부여되는 권한으로써, 보통 ROLE_ADMINISTRATOR 또는 ROLE_HR_SUPERVISOR와 같이 역할을 나타낸다. username/password 기반 인증을 수행하는 경우 GrantedAuthority 인스턴스는 보통 UserDetailService를 통해 로드된다.

보통 GrantedAuthority는 어플리케이션 전역적으로 사용하는 권한이다. 이는 주어진 도메인 객체에만 한정되지 않는다. 스프링 시큐리티는 당연히 일반적인 요구사항에 적합하게 설계되었지만 당신의 프로젝트 요구사항에 맞게 이를 사용하면 된다.

AuthenticationManager

AuthenticationManager는 스프링 시큐리티의 필터가 인증을 수행하는 방법을 정의하는 API이다. 반환된 Authentication 정보는 AuthenticationManager를 호출한 컨트롤러(스프링 시큐리티의 필터)에 의해 SecurityContextHolder에 저장된다. 만약 스프링 필터와 통합하고 싶지 않다면 AuthenticationManager를 사용하지 않고 SecurityContextHolder에 직접 접근해도 된다.

ProviderManager

ProviderManager는 AuthenticationManager의 가장 일반적인 구현체이다. ProviderManager는 AuthenticationProvider 인스턴스 목록에 위임한다. 각 AuthenticationProvider 인스턴스는 인증이 성공했거나, 실패했거나 또는 현재로썬 결정할 수 없고 다음 AuthenticationProvider에서 처리하도록 결정한다. 만약 설정된 모든 AuthenticationProvider가 인증을 수행할 수 없다면 ProviderNotFoundException과 함께 인증 실패 처리한다. ProviderNotFoundException는 AuthenticationException의 한 종류로써 ProviderManager가 인증을 수행할 수 없음을 의미한다.

각 AuthenticationProvider는 특정 유형의 인증 수행 방법을 알고 있다. 예를 들어 특정 AuthenticationProvider가 username/password 인증에 대한 역할을 가지고 있다면 또 다른 AuthenticationProvider는 SAML 인증에 대한 역할을 가지고 있을 수 있다. 이를 통해 각 AuthenticationProvider는 여러 유형의 인증을 지원하고 단일 AuthenticationManager 빈만 노출하도록 할 수 있다.

ProviderManager를 사용하면 AuthenticationProvider가 인증을 수행할 수 없는 경우 선택적으로 AuthenticationManager를 구성하도록 할수도 있다.

실제로 여러 ProviderManager 인스턴스가 동일한 상위 AuthenticationManager를 공유할 수 있다. 이를 통해 일부 인증이 공통 로직을 거치지만 각자 다른 인증 메커니즘도 가지고 있는 시나리오를 적용할 수 있다.

기본적으로 ProviderManager는 성공한 인증 요청에서 반환되는 인증 객체에서 중요한 정보를 지우려고 한다. 이는 패스워드와 같은 정보가 HttpSession에서 필요 이상으로 오래 유지되는것을 방지한다.

이는 성능 향상을 위해 Stateless 어플리케이션에서 유저 객체에 대한 캐시를 접근할 때 문제가 될 수 있다. 만약 Authentication이 캐시에 있는 객체 정보를 참조하고 있고 인증 정보가 제거된 경우 더이상 캐시된 객체 정보를 통해 인증을 수행할 수 없다. 따라서 캐시를 사용하는경우 이를 고려해야한다. 이에 대한 간단한 해결책은 먼저 캐시 구현체 또는 반환된 인증 객체를 생성하는 AuthenticationProvider의 복사본을 생성하는 것이다. 또는 ProviderManager의 eraseCredentialsAfterAuthentication 속성을 disable해도 된다.

AuthenticationProvider

ProviderManager에는 여러개의 AuthenticationProvider를 주입시킬 수 있다. 각 AuthenticationProvider는 특정한 타입의 인증 절차를 수행한다. 예를 들어 DaoAuthenticationProvider는 username/password 기반 인증을 수행하는 반면 JwtAuthenticationProvider는 JWT토큰 기반 인증을 수행할 수 있다.

Request Credentials with AuthenticationEntryPoint

AuthenticationEntryPoint는 클라이언트가 요청한 인증에 대한 HTTP 응답을 보내는데 사용된다. 경우에 따라 클라이언트는 리소스에 접근하기 위해 인증정보(username/password)를 요청에 포함시킨다. 이런 경우 스프링 시큐리티는 이미 인증정보가 포함되어있기 때문에 클라이언트로부터 인증정보 요청하는 HTTP응답을 제공하지 않아도 된다.

다른 경우를 살펴보자면, 클라이언트는 액세스 권한이 없는 리소스에 인증정보가 없는채로 요청을 보낼 수 있다. 이런 경우 AuthenticationEntryPoint의 구현체는 클라이언트에게 인증 정보를 요청하기 위해 사용된다. AuthenticationEntryPoint는 로그인 페이지로 리다이렉트 시키는 등의 행위를 수행할 수 있다.

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter는 유저 인증 정보를 위한 기본 필터 역할을 한다. 자격 증명이 인증되기 전에 스프링 시큐리티는 일반적으로 AuthenticationEntryPoint를 사용하여 인증 정보를 요청한다. 그러면 AbstractAuthenticationProcessingFilter가 클라이언트로부터 제출된 인증 요청을 처리할 수 있다.

1. 유저가 인증 정보를 제출하면 AbstractAuthenticationProcessingFilter는 HttpServletRequest로부터 Authentication 인증 정보를 생성한다. 생성되는 Authentication의 타입은 AbstractAuthenticationProcessingFilter의 하위 클래스에 따라 다르다. 예를 들어 UsernamePasswordAuthenticationFilter는 HttpServletRequest에 제출된 username/password를 통해  UsernamePasswordAuthenticationToken을 생성한다.

2. Authentication이 인증을 위해 AuthenticationManager로 전달된다.

3. 인증이 실패한다면,

- SecurityContextHolder가 비워진다.

- RememberMeServices.loginFail 메소드가 호출된다. 만약 해당 서비스가 설정되지 않은 경우 이 단계는 건너뛴다.

- AuthenticationFailureHandler가 호출된다.

4. 인증이 성공한다면,

- SessionAuthenticationStrategy는 새 로그인에 대한 알림을 받는다. 

- SecurityContextHolder에 Authentication 정보가 저장된다. 또한 이후의 요청을 위해 SecurityContext가 저장될 필요가 있는 경우 SecurityContextRepository의 saveContext 메소드가 명시적으로 호출되어야 한다. 

- RememberMeServices.loginSuccess 메소드가 호출된다. 만약 해당 서비스가 설정되지 않은 경우 이 단계는 건너뛴다.

- ApplicationEventPublisher는 InteractiveAuthenticationSuccessEvent를 발행한다.

- AuthenticationSuccessHandler가 호출된다.

Reference

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

반응형