[Spring Security] Servlet Application의 Authentication(인증) 구조

sangwoo·2022년 8월 10일
0

인증이 어떻게 수행되는지 알아보는 챕터이다. 스프링 시큐리티 버전은 5.7.6 버전을 참고했다. Spring Security Servlet Application Authenctication Architecture

📍 1. Authentication (인증)

스프링 시큐리티는 인증에 대해 포괄적인 지원을 제공한다. 여러 인증의 구조를 추상적인 Authentication Architecture에 대해 알아보고, 좀더 각 유형의 인증 구조는 Authentication Mechanisms을 알아보자.

📍 2. 서블릿 인증 구조의 핵심 기능

서블릿 인증에 사용되는 스프링 시큐리티의 주요 구성 요서를 살펴보자.

  • SecurityContextHolder

    인증된 사용자의 세부정보를 저장한 스프링 시큐리티를 가지고 있다. 즉 SecurityContext를 가지고 있는 것이다.

  • SecurityContext

    SecurityContextHolder에서 꺼내오고, 현재 인증된 사용자의 정보를 가지고 있다.

  • Authentication

    사용자가 인증을 위해 제공한 자격 증명(credentias) 또는 SecurityContext로부터 현재 유저 AuthenticationManager에 입력으로 넣어 줄 수 있다. 그니까, AuthenticationAuthenticationManager에 넣고 인증 수행을 한다.

  • GrantedAuthority

    위에서 기술한 Authentication에 부여된 권한. (roles, scopes, 등)

  • AuthenticationManager

    스프링 시큐리티의 필터들이 어떻게 실제로 인증을 수행하는 방법을 정의한 API

  • ProviderManager

    위의 AuthenticationManager을 구현한 가장 흔한 구현체

  • AuthenticationProvider

    ProviderManager에서 특정 유형의 인증을 수행하는데 사용

  • Request Credentials with AuthenticationEntryPoint :

    클라이언트에게 자격 증명을 요청하기 위해 사용

  • AbstractAuthenticationProcessingFilter

    인증에서 사용되는 기본 필터, 인증과 각 구성들이 어떻게 작동하는지에 대한 높은 수준의 흐름을 파악할 수 있다.

📍 3. SecurityContextHolder

스프링 시큐리티 인증의 모델의 중심에는 SecurityContextHolder가 있다.

SecurityContextHolder에 스프링 시큐리티가 인증된 사용자의 세부사항을 저장한 내용이 있다. 즉, SecurityContext를 가지고 있다는 것이다.

스프링 시큐리티는 SecurityContextHolder가 어떻게 내부를 채우는지 신경쓰지 않고, 오로지 값이 포함되어있다면, 그것을 현재 인증된 사용자로서 인식하고 사용한다.

사용자가 인증되었음을 나타내는 가장 간단한 방법은 SecurityContextHolder를 직접 설정하는 것이다.

SecurityContextHolder 설정 코드

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

SecurityContextHolder.setContext(context); 
  1. SecurityContext를 생성한다. 여러 스레드에서의 경합 조건(race condition)을 피하려면 SecurityContextHolder.getContext().setAuthentication(authentication) 대신에 새로운 SecurityContext를 생성하는 것이 중요하다.

  2. 새로운 Authentication 객체를 생성한다. 스프링 시큐리티는 SecurityContext에서 Authentication 구현의 타입 설정이 무엇이든 신경쓰지 않는다. 여기서는 매우 간단한 TestingAuthenticationToken을 사용한다. 일반적으로는 UsernamePasswordAuthenticationToken(userDetails, password, authorities)를 사용한다.

  3. SecurityContextAuthentication을 담아 SecurityContextHolder에 저장하고 스프링 시큐리티는 authorization의 정보를 사용할 것이다.

만약 인증된 유저에 대한 정보를 얻고 싶은 경우, SecurityContextHolder를 통해서 얻을 수 있다. 아래의 코드를 보자.

현재 인증된 user 접근

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

기본적으로 SecurityContextHolderThreadLocal을 사용하여 세부정보를 저장한다. 이는 SecurityContext가 해당 메서드에 대한 인수로 명시적으로 전달되지 않더라도 동일한 스레드의 메서드에서 항상 SecurityContext를 사용할 수 있다.

ThreadLocal의 사용은 현재 주체의 요청이 처리된 후에 스레드가 제거되는 것이 염려된다면 아주 좋은 방법일 수 있다. 스프링 시큐리티의 FilterChainProxy는 항상 SecurityContext가 지워지도록 한다.

일부 애플리케이션에는 스레드를 사용하는 특정 방법 떄문에 완전히 사용하기엔 적합하지 않다. 예를 들어, Swing 클라이언트는 자바 가상 머신에 있는 모든 스레드가 같은 시큐리티 컨텍스트에서 사용하길 원할지도 모른다.

시작 시점에 SecurityContextHolder를 구성하여 컨텍스트를 저장할 방법을 지정할 수 있다. 독립 실행형 애플리케이션인 경우, SecurityContextHolder.MODE_GLOBAL 전략을 사용할 수 있다.

다른 애플리케이션에서는 보안 스레드에의해 생성된 스레드도 동일한 보안 주체로서 판단하는 경우도 있을 수 있다. 이것은 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL을 사용하여 수행할 수 있다.

기본 SecurtiyContextHolder.MODE_THREADLOCAL에서 다른 모드로 바꾸는 방법은 두가지가 있다.

  1. system property를 설정하는 것이다.
  2. SecurityContextHolder에서 static 메서드를 호출한다.

하지만 대부분의 애플리케이션들은 기본 모드에서 변경할 필요가 없다. 그렇지만 변경하고 싶다면, Javadoc의 SecurityHolder를 살펴보자.

📍 4. SecurityContext

SpringContextHolder에서 꺼내오고, Authentication 객체를 포함하고 있다.

📍 5. Authentication

Authentication가 스프링 시큐리티에서 제공하는 중요한 목적은 두가지가 있다.

  1. AuthenticationManager에 대한 입력으로 사용자가 인증을 위해 넘겨준 자격 증명(credentials)를 넘겨준다. 이러한 과정이 진행될 때, isAuthenticated()false를 반환한다.

  2. 현재 인증된 사용자를 보여준다. 현재 AuthenticationSecurityContext에서 얻을 수 있다.

Authentication에 포함된 요소들

  • principal
    사용자를 식별한다. username/password를 사용하여 인증할 때, UserDetails의 인스턴스가 principal이 된다.

  • credentials
    패스워드로 사용되기도 한다. 보통 사용자가 유출되지 않도록 인증한 후 삭제'

  • authorities
    사용자에게 부여된 상위 수준 레벨(역할, 스코프 등)


📍 6. GrantedAuthority

GrantedAuthority는 사용자게에 부여되는 상위 수준 권한이다(roles, scopes 등)

GrantedAuthorityAuthentication.getAuthorities()에서 얻을 수 있다. 이 메서드는 GrantedAuthority 객체들을 컬렉션으로 반환한다.

여기서 GrantedAuthorityprincipal에 부여된 권한이다. 이러한 권한은 보통 ROLE_ADMINISTRATOR, ROLE_HR_SUPERVISOR와 같은 ROLE이다. ROLES는 나중에 웹 권한 부여, 메서드 권한 부여, 도메인 개체 권한 부여를 위해 구성된다.

스프링 시큐리티의 다른 부분에서 이러한 권한을 알고 있고, 해석할 수 있다. username/password 인증을 사용할 때 GrantedAuthority는 보통 UserDetailsService 에서 로드 된다.

보통 GrantedAuthority 개체는 특정 도메인에 한해서가 아닌 애플리케이션 전반에서의 사용 권한을 말한다. 대신 스프링에서는 특정 도메인 보안 기능을 제공한다.

📍 7. AuthenticationManager

AuthenticationManager는 Spring Security의 필터들이 인증을 실제 어떻게 수행하는지에 대해 정의한 API이다. 반환된 AuthenticationAuthenticationManager를 통해 호출된 컨트롤러(예: 스프링 시큐티의 필터들)에 의해 SecurityContextHolder에 설정된다.

만약 스프링 시큐리티의 필터들을 통합하지 않는다면, 직접 SecurityContextHolder를 설정할 수 있고, AuthenticationManager를 사용하지 않아도 된다.

AuthenticationManager의 구현체는 무엇이든 될 수 있지만, 가장 흔한 구현체는 ProvideManager이다.

📍 8. ProviderManager

ProviderManager는 가장 일반적으로 사용되는 AuthenticationManager의 구현체이다. ProviderManagerAuthenticationProvider들의 리스트에 역할을 위임한다.

각각의 AuthenticationProvider는 인증이 성공, 실패 또는 후속의 ProviderManager가 결정할 수 없음을 표시해야한다. 구성된 AuthenticationProvider들 중 어느 것도 인증을 수행 할 수 없을 경우,

전달된 Authentication의 유형을 지원하는 ProviderManager가 없음을 나타내는 AuthenticationExceptionProviderNotFoundException가 발생하며 실패할 것이다.


AuthenticationProvider는 특정 유형의 인증을 수행하는 방법을 알고 있다. 예를 들어, 하나의 AuthenticationProvider는 username/password 검증을 할수 있고, 다른 AuthenticationProvider는 SAML assertion 인증을 할수 있다.

AuthenticationProvider는 여러 유형의 인증을 제공하고, 단일 AuthenticationManager을 bean으로 보내면서 매우 특정한 유형의 인증을 수행할 수 있다.

또한 ProviderManagerAuthenticationProvider가 인증을 수행할 수 없는 경우 참조되는 상위 AuthenticationManager를 선택사항으로 설정할 수 있다.

상위 AuthenticationManager의 타입은 무엇이 되도 상관은 없지만, 보통 ProviderManager의 인스턴스이다.

사실, 여러 ProviderManager 인스턴스들은 같은 부모 AuthencticationManager를 공유한다.

그래서 보통 상위 AuthenticationManager는 공통으로 같지만, 실제 ProviderManager의 인스턴스는 모두 다른 인증 메커니즘을 가지고있고 이러한 ProviderManager 인스턴스들을 여러 SecurityFilterChain 인스턴스가 가지고있는 방식이다.

기본적으로, ProviderManager는 인증 요청에 성공하여 반환된 Authentication 객체의 민감한 자격 증명 정보를 지우려고 할것이다. 이는 HttpSession에서 필요 이상으로 패스워드 같은 정보가오래 유지되는 것을 막기 위해서다.

stateless 애플리케이션의 성능을 개선하기 위해 사용자 객체의 캐시를 사용할 때 문제를 일으 킬 수 있다. 만약 Authentication이 포함한다면 UserDetails 인스턴스와 같은 캐시에서 객체 참조를 갖고 있고, 해당 객체의 자격 증명을 삭제한다면 캐시된 값에 대해 더이상 인증할 수 없다.

만약 캐시를 사용한다면 고려해야할 필요가 있다. 확실한 해결책은 반환된 Authentication을 생성하는 AuthenticationProvider 또는 캐시 구현에서 객체의 복사본을 만드는 것이다.

다른 방법으로는, ProviderManager에서 eraseCredentialsAfterAutheication 속성을 사용하지 않게 할 수 있다. 자세한 내용은 Javadoc 참조.

📍 9. AuthenticationProvider

여러 AuthenticationProviderProviderManager에 주입 할 수 있다. 각 AuthenticationProvider는 특정 유형의 인증을 수행한다.

예를 들어 DaoAuthenticationProvider는 Username/Password 기반 인증을 지원하고, JwtAuthenticationProvider는 JWT 토큰 인증 방식을 지원한다.

📍 10. Request Credentials with AuthenticationEntryPoint

클라이언트에게 자격 증명을 요청하라는 HTTP 응답을 보내는데 사용한다. 가끔 클라이언트가 username/password 와 같은 자격 정보를 리소스 요청을 위해 사전에 포함하기도 하는데, 이런 경우에는 이미 자격 정보가 포함되어있기 때문에 스프링 시큐리티는 클라이언트에게 자격 증명 요청 정보 HTTP Response를 보내지 않아도 된다.

하지만 대부분, 클라이언트는 접근 권한이 없는 리소스에 인증되지 않은 요청을 한다. 이럴 때, AuthenticationEntryPoint의 구현체는 클라이언트에게 자격 증명 요청으로 사용된다.

AuthenticationEntryPoin 구현체는 로그인 페이지로 리디렉트를 수행하거나, WWW-Authenticate 헤더 응답을 보낸다.

📍 11. AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 는 사용자의 자격 증명을 위한 기본 필터로 사용된다. 자격 증명(credentials)가 인증되기 전에, 스프링 시큐리티는 일반적으로 AuthenticationEntryPoint를 사용하여 자격 증명(credentials)를 요청한다.

AbstractAuthenticationProcessingFilterSubmit 된 모든 유형의 인증 요청을 수행할 수 있다.

  1. 사용자가 자격 증명(credentials)를 Submit 할때, AbstractAuthenticationProcessingFilter는 인증할 HttpServletRequest 에서 Authentication을 생성한다.

    생성된 Authentication의 타입은 AbstractAuthenticationProcessingFilter의 하위 클래스에 의존한다. 예를 들어, UsernamePasswordAuthenticationFilterHttpServletRequest에서 넘어온 username과 password를 통해 UsernamePasswordAuthenticationToken을 생성한다. 이 구현 객체가 Authenctication이 된다.

  1. 다음으로, Authentication은 인증을 위해 AuthenticationManager로 전달 된다.

  2. 만약 인증 실패라면, Failure 단계로 진행된다.

    • SecurityContextHolder를 초기화한다.
    • RememberMeService.loginFail이 호출된다. 만약 Remember me가 설정되어 있지 않다면, 작동하지 않는다.
    • AuthenticationFailureHandler를 호출한다.
  3. 인증에 성공할 경우. Success 단계로 진행된다.

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

    • AuthenticationSecurityContextHolder에서 설정되고, 나중에 SecurityContextPersistenceFilterSecurityContextHttpSession에 저장

    • RememberMeService.loginSuccess가 호출, Remember Me가 설정되어 있지 않다면 작동되지 않음.

    • ApplicationEeventPublisherInteractiveAuthenticationSuccessEvent를 올린다.

    • AuthenticationSuccessHandler가 호출

📍 12. 정리

  • SecurityContextHolderSecurityContext를 가지고 있는 인증의 핵심 부분이다.

  • SecurityContext는 인증된 사용자의 정보를 가지고 있는 데, 이 정보가 Authentication이다.

  • Authentication은 두가지의 사용 목적이 있다.

    1. AutheticationManager에게 사용자에게 받은 자격 증명 정보를 넘겨주어 인증을 수행한다.

    2. 현재 인증된 유저의 정보를 확인 할 수 있다. 현재 AuthenticationSecurityContext에서 꺼낼 수 있다.

  • GrantedAuthority는 사용자에게 부여하는 권한이다. 권한은 애플리케이션 전반에 걸쳐 적용된다.
    각 도메인 마다 사용자에게 여러권한을 주면 한 사람당 여러 권한이 들어오게 되고 사용자가 많아진다면 메모리에 부하가 걸린다.
    그래서 각 도메인마다 특정 제약을 걸어 접근 제한을 할수 있다고한다.

  • AuthenticationManager는 실제 스프링 시큐리티 필터가 인증을 수행하는 API이다.

  • ProviderManagerAuthenticationManager의 가장흔한 구현체이다. 그리고 여러 유형의 ProviderManager가 있고, 시큐리티의 필터체인 인스턴스는 각 유형의 ProviderManager를 사용한다.

  • AuthenticationProviderProviderManager에 주입할 수 있다. jwt 방식의 인증, username/password 방식의 인증 등

  • AuthenticationEntryPoint 인증되지 않은 요청이 올때, 서버가 클라이언트로부터 사용자의 자격 증명 정보를 요청하기위해 로그인 페이지로 리디렉트하거나, 헤더에 WWW-Authenticate 담아 응답한다.

  • AbstractAuthenticationProcessingFIlter가 이제 실제 인증을 수행하는 필터이다.

그래서?

그래서 AbstarctAuthenticationProcessingFilter가 실제 인증을 수행하기 위한 부분이고, 여기서 AuthenticationManager 등으로 인증 수행을 해야하는 것같다. AbstractAuthenticationPorocessingFilter의 구현체도 찾아보고 상속받아서 내가 원하는 유형의 인증 필터를 만들어보자, 그리고 ProviderManager를 상속받아 JwtProviderManager로 만들어 커스텀 해보자.

0개의 댓글