JiYoung Dev 🖥

Spring Security 적용기 (5) Spring Security Authentication 동작원리 본문

Study/Java

Spring Security 적용기 (5) Spring Security Authentication 동작원리

Shinjio 2024. 5. 5. 14:55

jwt에 대해 공부하고 인증 및 인가 기능을 구현하려던 중 spring security에 대한 이해가 아직 부족하다는 생각이 들었다. 

어떻게 spring security는 인증 및 인가 기능을 구현하고, 어떻게 작동하는가에 대한 문제에 부딪혔고, 

이번 포스팅에서는 spring security 공식 사이트를 해석하여 Authentication의 아키텍처에 대해 공부해보고자 한다. 

 

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

 

Servlet Authentication Architecture :: Spring Security

ProviderManager is the most commonly used implementation of AuthenticationManager. ProviderManager delegates to a List of AuthenticationProvider instances. Each AuthenticationProvider has an opportunity to indicate that authentication should be successful,

docs.spring.io


Spring Security Authentication Mechanism

SecurityContextHolder 

인증된 사람의 세부 정보(details)를 저장하는 저장소

 

SecurityContext

위의 SecurityContextHolder에서 가져오는 정보로, 현재 인증된 사용자의 Authentication 정보를 포함

 

Authentication

사용자가 인증을 위해 제공한 자격 증명이나 현재 사용자의 정보

AuthenticationManager에게 전달되어 인증을 수행할 수 있다. 

 

GrantedAuthority

Authentication에 부여된 권한을 나타내며, 역할, 범위 등을 포함

 

AuthenticationManager

Spring Security의 필터가 인증을 수행하는 방식을 정의하는 API

 

ProviderManager

AuthenticationManager의 가장 일반적인 구현체

 

AuthenticationProvidor

ProviderManager가 특정 유형의 인증을 수행할 수 있도록하는데 사용된다. 

 

Request Credential with AuthenticationEntryPoint

클라이언트로부터 자격 증명을 요청할 때 사용

예를 들어, 로그인 페이지로 리다이렉션하거나 www-Authenticate 응답을 보내는 등의 작업을 수행한다. 

 

AbstarctAuthenticationProcessingFilter

인증에 사용되는 기본 필터로, 인증의 고수준 흐름과 구성 요소들이 어떻게 함께 작동하는지에 대한 개요를 제공한다. 

 

먼저 공식 문서에 정의된 각 구성 요소의 정의이다. 

일단 이것만 봐서는 잘 이해가 가지 않는다. 아래 내용을 보며 천천히 이해해해 보자. 


⚙️SecurityContextHolder

Spring Security Authentication 모델의 핵심이다. SecurityContextHolder는 SecurityContext를 포함한다. 

 

 

SecurityContextHolder는 인증된 사용자의 상세 정보를 저장하는 저장소이다. 

Spring security는 이 세부 정보가 어떻게 채워지는지에 대해서는 전혀 신경쓰지 않는다. 

 

만약 SecurityContextHolder가 어떠한 값으로 채워져 있다면, 그 값은 현재 인증된 사용자로 사용된다. 

 

즉, Spring Security는 현재 인증된 사용자의 세부 정보를 SecurityContextHolder에 저장하고, 이 정보가 어떻게 채워지는지는 중요하지 않다. 단지 SecurityContextHolder에 값이 있으면, 해당 값이 현재 인증된 사용자로 간주되어 인증된 요청을 처리한다. 

 

SecurityContextHolder Setting

//비어있는 SecurityContext 생성
//반드시 새로운 SecurityContext를 생성해야 한다. 
//SecurityContextHolder.getContext().setAuthentication(authentication)와의 경합 조건을 피하기 위함
SecurityContext context = SecurityContextHolder.createEmptyContext(); 

//Authentication 객체 생성
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);

//SecurityContextHoler에 Context 저장
SecurityContextHolder.setContext(context);

 

현재 인증된 사용자로의 접근

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

 

Spring Security에서는 ThreadLocal을 사용하여 현재 쓰레드의 SecurityContext를 저장하고, 이를 통해 동일한 쓰레드 내의 메서드에서 언제든지 해당 정보를 사용할 수 있다. 이를 통해 각각의 요청이 처리된 후에는 쓰레드가 안전하게 초기화되어 다음 요청에 영향을 주지 않도록 한다. 

 

ThreadLocal
Java에서 여러 쓰레드가 동시에 실행되는 환경에서 각 쓰레드 별로 고유한 값을 유지하는 데 사용되는 클래스
일반적으로, 전역 변수는 여러 쓰레드에 의해 공유되므로 다중 쓰레드 환경에서 안전하지 않다. 그러나 ThreadLocal을 쓰면 각 쓰레드가 자체적으로 값을 보유하고, 한 쓰레드에서 설정한 값은 다른 쓰레드에 영향을 주지 않는다. 

⚙️SecurityContext

Authentication 객체를 포함하며, SecurityContextHolder에서 가져올 수 있다. 


⚙️Authentication

Authentication은 Spring Security에서 2가지 주요 목적을 제공한다. 

 

1. AuthenticationManager에게 사용자가 제공한 자격 증명을 제공하는 역할. 이 경우 isAuthenticated() 메서드는 false를 반환한다. 

2. 현재 인증된 사용자를 나타내는 역할

 

Authentication에는 아래와 같은 정보가 포함된다. 

1. principle : 사용자 식별 정보로 이름/암호로 인증하는 경우 UserDetails의 인스턴스가 될 수 있다. 

2. credential : 자격 인증으로 보통은 비밀번호이다. 많은 경우 사용자가 인증되면 이것이 노출되지 않도록 지워진다. 

3. authorities : 부여된 권한. 역할(role) 혹은 범위(scope)


⚙️GrantedAuthority

사용자에게 부여된 고수준 권한(authorites)이다. 예를 들어 ROLE_ADMINISTOR 혹은 ROLE_HR_SUPERVISOR와 같은 것을 말한다.

GrantedAuthority 인스턴스를 Authentication.getAuthorites() 메서드를 통해 얻을 수 있다. 

이러한 역할은 웹 인가, 메서드 인가, 도메인 객체 인가와 같은 것들을 나중에 구성할 때 사용된다.

사용자 이름/비밀번호 기반 인증을 사용할 때는 보통 UserDetailsService를 통해 GrantedAuthority 인스턴스가 로드된다. 

 

일반적으로 GrantedAuthority 객체는 애플리케이션 전반적인 권한이다. 특정 도메인 객체에 특정 권한을 나타내는 GrantedAuthority가 없는 것이 일반적이다. 예를 들어, Employee 객체 번호 54에 대한 권한을 나타내는 GrantedAuthority가 없다. 이러한 권한이 많아지면 메모리 부족 문제가 발생할 수 있다. 그러나 SpringSecurity는 이러한 일반적인 요구사항을 처리할 수 있도록 설계되어 있으며, 이를 위해 프로젝트의 도메인 객체 보안 기능을 사용할 수 있다. 


⚙️AuthenticationManager / ProviderManager

AuthenticationManager는 Spring Security의 필터가 인증을 수행하는 방식을 정의하는 API이다. 즉 Spring security의 인증 작업을 수행하는데 사용되는 API라는 것이다. (AuthenticationManager = Spring Security 사용자 인증 역할을 담당하는 인터페이스). AuthenticationManager는 필터들에 인증 작업을 위임하고, 인증에 성공하면 Authentication 객체를 반환한다. 

 

예를 들어 UsernamePasswordAuthenticationFilter는 사용자를 인증하고 AuthenticationManger를 통해 반환된 Authentication 객체를 받는다. 그리고 이를 SecurityContextHolder에 설정한다. 

 

AuthenticationManager의 구현은 다양할 수 있지만, 가장 일반적인 구현체는 ProviderManger이다. ProviderManager는 인증 과정을 처리하는 데 필요한 여러 AuthenticationProvider를 관리하고, 각각의 Provider에게 실제 인증을 위임하여 사용자를 인증한다. 

 

예를 들어, 사용자가 사용자 이름과 암호, SAML 어선셜를 제공하여 로그인을 시도하면 ProviderManager는 등록된 AuthenticationProvider들에게 이 정보를 전달하여 사용자를 인증한다. 각각의 Provider는 자신의 방식에 맞게 사용자를 인증하고, 성공 또는 실패 여부를 ProviderManager에게 알려준다. 만약 ProviderManager가 등록된 모든 AuthenticationProvider에게 인증을 시도했음에도 불구하고 어떤 제공자도 인증을 성공시키지 못한다면, 이 인증 요청을 실패로 간주하며 ProviderNotFoundException이 발생한다. 반대로, 하나의 제공자가 성공적으로 인증을 수행한다면, ProviderManager는 해당 AuthenticationProvider가 반환한 Authentication 객체를 사용자에게 반환한다. 

 

 

이렇게 두가지 방법을 동시에 사용하는 인증 방식도 있겠지만, 특정 상황에서는 SAML 어션셜을 제공하고, 어떤 경우에는 사용자 이름과 암호를 통해 인증하는 방식을 사용할 수도 있다. 이런 경우 ProviderManager는 우선 등록된 AuthenticationProvider들에게 인증을 시도한다. 그러나 만약 어떤 Provider도 인증을 수행할 수 없는 경우 ProviderManager는 설정된 부모 AuthenticatioManager에게 인증을 위임할 수도 있다. (부모 AuthenticationManager는 일반적으로 자식 Manager들이 처리할 수 없는 인증 요청을 처리한다.

 

또한, 여러 ProviderManager는 인스턴스가 동일한 붙 AuthenticationManager를 공유할 수도 있다. 

 

 

 

기본적으로 ProviderManager는 인증 요청이 성공한 경우 반환된 Authentication 객체에서 민가한 자격 증명 정보를 지우려고 시도한다. 이는 세션(HttpSession)에서 비밀번호와 같은 정보가 필요 이상으로 오랫동안 보존되지 않도록 방지한다. 

이는 stateless 애플리케이션에서 성능을 향상시키기 위해 사용자의 객체를 캐시를 사용할 때 문제가 될 수 있다. 만약 Authentication에 캐시된 객체에 대한 참조가 포함되어 있고, 이것에 대한 자격 증명이 제거된다면, 캐시된 값에 대해 더이상 인증을 수행할 수 없다. 따라서 캐시를 사용하는 경우 이에 대한 내용을 고려해야 한다. 

명백한 해결책은 캐시 구현이나 반환된 Authentication 객체를 생성하는 AuthenticationProvider에서 객체를 먼저 복사하는 것이다. 또는 ProviderManager의 eraseCredentailsAfterAuthentication 속성을 비활성화 하는 것이다. 


⚙️ AuthenticationProvider

ProviderManager에 여러 AuthenticationProvider를 주입할 수 있다. AuthenticationProvider는 특정 유형의 인증을 수행한다. 예를들어, DaoAuthenticationProvider는 사용자 이름/암호 기반의 인증을 지원하고, JwtAuthenticationProvider는 JWT 토큰의 인증을 지원한다. 


⚙️Request Credentials with AuthenticationEntryPoint

AuthenticationEntryPoint는 클라이언트로부터 자격 증명을 요청하는 HTTP에 대한 응답을 보내는데 사용된다. 

가끔 클라이언트는 자격증명을 포함하여 리소스를 요청할 때가 있다. 이러한 경우 이미 자격증명이 포함되므로 SpringSecurity는 클라이언트로부터 자격 증명을 요청하는 HTTP 응답을 제공할 필요가 없다. 

 

다른 경우에는 클라이언트가 인증되지 않은 요청을 보내어 자격 증명을 제공하지 않은 경우이다.(로그인 하지 않은 상태에서 보호된 페이지에 접근하려 할 때)  AuthenticationEntryPoint의 구현이 사용되어 클라이언트로부터 자격증명을 요청한다. AuthenticationEntryPoint의 구현은 로그인 페이지로 리다이렉션을 하거나 WWW-Authenticate 헤더로 응답하는 등의 작업을 수행할 수 있다. 


⚙️AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter은 사용자의 자격증명을 위한 기본적인 Filter이다. 자격증명이 인증되기 전에는 Spring Security는 AuthenticationEntryPoint를 사용하여 자격증명을 요청한다. 

그 다음, AbstractAuthenticationProcessingFilter는 제출된 모든 인증 요청을 인증한다.

 

 

1. 사용자가 자격 증명을 제출하면, AbstractAuthenticationProcessingFilter는 HttpServletRequest에서 Authentication을 생성하여 인증된다.

생성되는 Authentication의 유형은 AbstractAuthenticatonProcessingFilter의 하위 클래스에 따라 달라진다. 예를 들어 UsernamePasswordAuthenticationFilter는 HttpServletRequest에 제출된 사용자 이름과 암호로부터 UsernamePasswordAuthenticationToken을 생성한다. 

2. 그 다음으로, Authentication은 인증을 위해 AuthenticationManger에 전달된다. 

3. 인증이 실패하면, 

 - SecurityContextHolder가 비워진다. 

 - RememberMeService.loginFail이 호출된다. Remember me가 구성되지 않은 경우, 이것은 no-op이다. 

 - AuthenticationFailureHandler가 호출된다. 

4. 인증이 성공하면,

 - SessionAuthenticationStrategy가 새로운 로그인을 알린다. 

 - Authentication은 SecurityContextHolder에 설정된다. 나중에 SecurityContext를 저장하여 자동으로 향후 요청에 설정할 수 있도록 하려면 SecurityContextRepository #saveContext를 명시적으로 호출해야 한다. 

 - RememberMeServices.loginSuccess가 호출된다. 구성되지 않은 경우, no-op이다. 

 - ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent를 게시한다. 

 - AuthenticationSuccessHandler가 호출된다.