[스프링 시큐리티 - 인증] 공식문서 번역하며 공부하기 - 서블릿 인증 아키텍처
일러두기
- 본 글은 스프링 공식 페이지의 Servlet Authentication Architecture 절을 한국어로 번역한 자료입니다.
- 전문적인 교육을 받은 번역가가 번역한 글이 아니기 때문에 다소의 번역 실수가 있을 수 있습니다. (아마 많을..)
- 해당 기능에 대해 전문적인 지식을 갖춘 엔지니어가 아니라, 스프링 시큐리티를 처음 공부하는 학생이 작성한 글이기 때문에 번역 간의 다수의 오역과 의역이 있을 가능성이 있습니다.
- 번역이 매끄럽지 못하다고 판단되는 부분은 파란색으로 원본 문장을 첨부하여 원본을 확인할 수 있도록 하였습니다.
원본 출처
https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html
번역문서
서블릿 인증 아키텍처
이 절에 대한 논의는 서블릿 인증 과정에서 사용되는 스프링 시큐리티의 주요 아키텍처적 요소들을 논의하기 위해 서블릿 인증 : 개요를 확장하여 진행합니다. This discussion expands on Servlet Security: The Big Picture to describe the main architectural components of Spring Security’s used in Servlet authentication. 만약 이 절에서 언급되는 요소들이 구체적인 상황에서 어떻게 쓰여지는지 알고 싶다면, 인증 메커니즘의 특정 절들을 참고하길 바랍니다. If you need concrete flows that explain how these pieces fit together, look at the Authentication Mechanism specific sections.
- SecurityContextHolder - SecurityContextHolder은 스프링 시큐리티가 인증 받은 사용자의 세부 정보를 저장하는 공간입니다.
- SecurityContext - SecurityContextHolder에서 획득할 수 있는 SecurityContext는 현재 인증된 사용자의 Authentication을 보유합니다.
- Authentication - Can be the input to AuthenticationManager to provide the credentials a user has provided to authenticate or the current user from the SecurityContext. (해석 불가)
- GrantedAuthority - Authentication 주체(ex. roles, scopes 등)에 의해 부여된 권한
- AuthenticationManager - 스프링 시큐리티 필터가 인증을 수행하는 방법을 정의하는 API
- ProviderManager - AuthenticationManager의 주 구현체
- AuthenticationProvider - 특정 종류의 인증을 수행하기 위해 ProviderManager이 사용합니다.
- Request Credentials with AuthenticationEntryPoint - 클라이언트에 자격 증명을 요청하는데 사용됩니다. (ex. 로그인 페이지로 리다이렉션, WWW-Authenticate response 등)
- AbstractAuthenticationProcessingFilter - 인증에 사용되는 기본 필터입니다. 높은 수준(high level)에서의 인증 흐름과, 각각의 메커니즘이 함께 동작하는 방식을 알 수 있습니다.
SecurityContextHolder
Hi servlet/authentication/architecture there (문맥상의 의미를 몰라서 해석하지 않았다.)
스프링 시큐리티 인증 모델의 중심에는 SecurityContextHolder이 있습니다. SecurityContextHolder은 SecurityContext를 포함합니다.
SecurityContextHolder은 스프링 시큐리티가 인증받은 사용자의 세부정보를 저장하는 곳입니다. 스프링 시큐리티는 SecurityContextHolder이 채워지는 방식은 상관하지 않습니다. 값이 포함된 경우, SecutiryContextHolder은 현재 인증된 사용자로 간주됩니다. If it contains a value, then it is used as the currently authenticated user.
사용자가 인증되었음을 보여주는 가장 간단한 방법은 SecurityContextHolder을 직접 설정하는 것입니다.
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
- SecurityContext를 만드는 것으로 시작합니다. 이 때 멀티 스레드의 경쟁 상태(race condition)을 방지하기 위해 SecutiryContextHolder.getContext().setAuthentication(authentication) 대신 새로운 SecurityContext 객체를 만드는 것이 중요합니다.
- 다음으로 새로운 Authentication 객체를 만듭니다. 스프링 시큐리티는 어떤 타입의 Authentication 구현체가 SecurityContext 위에 올라가는지 상관하지 않습니다. 따라서 위의 예에서는 사용이 가장 간단한 TestingAuthenticationToken을 사용했습니다. 일반적으로는 UsernamePasswordAuthenticationToken(userDetails, password, authorities)가 사용됩니다. A more common production scenario is UsernamePasswordAuthenticationToken(userDetails, password, authorities).
- 최종적으로 SecurityContextHolder에 SecurityContext를 설치(set)합니다. 스프링 시큐리티는 이 정보를 인증을 위해 사용할 것입니다.
만약 인증 주체(principal)에 대한 정보를 얻기 원한다면 SecurityContextHolder에 접근하여 정보를 획득할 수 있습니다. If you wish to obtain information about the authenticated principal, you can do so by accessing the SecurityContextHolder.
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가 특정 메소드의 인자로 명확하게 passed around 되지 않더라도 같은 스레드 상의 메소드들에게 언제나 접근이 가능하는 것을 의미합니다. By default the SecurityContextHolder uses a ThreadLocal to store these details, which means that the SecurityContext is always available to methods in the same thread, even if the SecurityContext is not explicitly passed around as an argument to those methods. 이러한 방식으로 ThreadLocal을 사용하는 것은 현재 주체(principal)에 대한 요청이 처리된 후 스레드를 제거하는데 주의를 기울인다면 꽤 안전한 방식입니다. 스프링 시큐리티의 FilterChainProxy는 SecurityContext가 항상 제거되도록 합니다.
어떤 애플리케이션들은 스레드를 활용하는 특정한 방식 때문에 ThreadLocal을 사용하는 것이 완전히 적합하지는 않습니다. 예를 들어 Swing의 클라이언트는 같은 시큐리티 컨텍스트를 사용하기 위해 JVM(Java Virtual Machine) 내의 모든 스레드를 사용하기를 원할 수도 있습니다. SecurityContextHolder은 컨텍스트가 저장되는 방식을 명시하는 초기 설정 전력으로도 사용할 수 있습니다. SecurityContextHolder can be configured with a strategy on startup to specify how you would like the context to be stored. stand-alone 애플리케이션의 경우 SecurityContextHolder.MODE_GLOBAL 전략을 사용할 수 있습니다. 다른 애플리케이션들의 경우에도 같은 보안적 성격을 띄는 시큐리티 스레드로부터 파생되는 스레드들을 사용하는 것이 가능합니다. (완전 의역 - Other applications might want to have threads spawned by the secure thread also assume the same security identity.) 이러한 목적은 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL을 사용해 달성 가능합니다. 기본값인 SecurityContextHolder.MODE_THREADLOCAL 모드는 두 방식으로 전환 가능합니다. 첫 번째 방법은 시스템 프로퍼티를 사용하는 것이고, 두 번째 방법은 정적 메소드 SecurityContextHolder을 호출하는 것입니다. 대부분의 애플리케이션은 기본 설정을 바꿀 필요가 없지만, 만약 설정을 바꿔야 한다면 SecurityContextHolder에 관한 자바 공식 문서를 참조하시길 바랍니다.
SecurityContext
SecurityContext는 SecurityContextHolder에 의해 획득 가능합니다. SecurityContext는 Authentication 객체를 포함합니다.
Authentication
Authentication은 스프링 시큐리티의 주목적 중 두 가지를 수행합니다.
- An input to AuthenticationManager to provide the credentials a user has provided to authenticate. When used in this scenario, isAuthenticated() returns false. (해석 불가) 이 경우, isAuthenticated()는 false를 반환합니다.
- 현재 인증된 사용자를 대변합니다. 현재 Authentication은 SecurityContext를 통해 획득 가능합니다.
Authentication은 다음을 포함합니다.
- principal(주체) - 사용자를 식별합니다. 사용자명/비밀번호를 인증할 때, 보통 UserDetails의 객체로 사용됩니다.
- credentials(자격) - 비밀번호입니다. 일반적으로 비밀번호의 유출 가능성을 제거하기 위해 사용자의 인증절차가 완료되면 삭제됩니다.
- authorities(권한) - GrantedAuthority은 사용자에게 부여되는 높은 수준의 승인입니다. the GrantedAuthoritys are high level permissions the user is granted. role 혹은 scope를 예로 들 수 있습니다.
GrantedAuthority
GrantedAuthority은 사용자에게 부여되는 높은 수준의 승인입니다. role 혹은 scope를 예로 들 수 있습니다.
GrantedAuthority는 Authentication.getAuthorities() 메소드를 통해 획득할 수 있습니다. 이 메소드는 GrantedAuthority의 Collection 객체를 제공합니다. 하나의 GrantedAuthority는 당연히(not surprisingly) 주체(principle)에 부여되는 권한입니다. 이러한 권한들은 주로 ROLE_ADMINISTRATOR 혹은 ROLE_HR_SUPERVISOR과 같은 "roles"들입니다. 이러한 roles들은 이후 웹 인증, 메소드 인증, 도메인 객체 인증을 위해 구성됩니다. 스프링 시큐리티의 나머지 부분들은 이 권한들을 해석할 수 있으며 존재할 것이라 예측할 수 있습니다. Other parts of Spring Security are capable of interpreting these authorities, and expect them to be present. 사용자명/비밀번호 인증을 하는 경우 GrantedAuthority는 주로 UserDetailsService에 의해 호출(load)됩니다.
일반적으로 GrantedAuthority 객체들은 범애플리케이션적 승인 객체들(application-wide permissions)입니다. 이들은 특정 도메인 객체에 국한되지 않습니다. 따라서, GrantedAuthority는 가령 54번 직원에게 특정되는 승인으로 활용되기에 적합하지 않습니다. (완전 의역 - Thus, you wouldn’t likely have a GrantedAuthority to represent a permission to Employee object number 54) 이러한 권한들이 수천 개 존재한다면 빠르게 메모리가 고갈될 수 있기 때문입니다. (또는 적어도 애플리케이션이 사용자를 인증하는데 더 많은 시간이 걸릴 수 있습니다.) 스프링 시큐리티는 이러한 공동 요구사항을 처리하는데 특화되어 설계되었지만, 위의 경우는 GrantedAuthority를 사용하는 것보다 프로젝트의 도메인 객체 시큐리티 기능을 사용하는 것을 권장합니다. (완전 의역 - Of course, Spring Security is expressly designed to handle this common requirement, but you’d instead use the project’s domain object security capabilities for this purpose.)
AuthenticationManager
AuthenticationManger은 스프링 시큐리티 필터가 인증을 하는 방법을 정의하는 API입니다. 반환된 Authentication은 AuthenticationManager을 호출한 컨트롤러(즉, 스프링 시큐리티의 필터)에 의해 SecurityContextHolder에서 설정됩니다. 만약 스프링 시큐리티의 필터를 사용하지 않는다면 AuthenticationManager의 사용 없이 바로 SecurityContextHolder를 설정할 수 있습니다.
AuthenticationManager의 구현체는 어느 것이든 될 수 있으며, 주 구현체는 ProviderManager입니다.
ProviderManager
ProviderManager은 AuthenticationManager의 주 구현체입니다. ProviderManager은 AuthenticationProvider List에 역할을 위임합니다. 각각의 AuthenticationProvider은 인증의 성공, 실패 혹은 '결정을 내릴 수 없음'을 나타낼 수 있는 기회가 있고 이후에 호출되는(downstram) AuthenticaionProvider에게 세 번째 결정(결정을 내릴 수 없음)을 위임할 수 있습니다. (의역 - Each AuthenticationProvider has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream AuthenticationProvider to decide.) 만약 등록된 모든 AuthenticationProvider들이 인증 결정을 내릴 수 없다면 인증은 실패하고, 특정 종류의 Authentication을 처리할 수 있는 ProviderManager가 설정되지 않았음을 나타내는 특수한 AuthenticationException인 ProviderNotFoundException을 출력합니다.
실제로 각각의 AuthenticationProvider은 특정 종류의 인증을 처리하는 방법을 알고 있습니다. 예를 들어 특정 AuthenticationProvider은 사용자명/비밀번호를, 다른 AuthenticationProvider은 SAML assertion을 인증할 수 있습니다. AuthenticationProvider로 하여금 특정 종류의 인증을 수행할 수 있게 하기 때문에, 단일 AuthenticationManager 빈만을 노출시키는 행동만으로도 다양한 인증을 해결할 수 있습니다.
또한 AuthenticationProvider이 인증을 수행할 수 없는 경우, ProviderManager은 선택 가능한 부모 AuthenticationManager을 구성할 수도 있습니다. 부모는 AuthenticationManager 타입인 경우 어느 것도 될 수 있지만 ProviderManager의 객체가 오는 경우가 많습니다.
사실 다수의 ProviderManager 객체들은 같은 AuthenticationManager 부모를 공유할 수도 있습니다. 이는 공통 인증을 가지는 다수의 SecurityFilterChain 객체(공유 부모 Authentication)가 있을 뿐 아니라 다른 인증 메커니즘(다른 ProviderManager 객체)를 가질 때의 일반적인 현상이라 할 수 있습니다. (의역 - This is somewhat common in scenarios where there are multiple SecurityFilterChain instances that have some authentication in common (the shared parent AuthenticationManager), but also different authentication mechanisms (the different ProviderManager instances).)
기본적으로 ProviderManager은 성공적인 인증 요청에 의해 반환된 Authentication 객체의 민감한 자격 증명 정보를 지우려고 시도합니다. 이러한 결과로 비밀번호와 같은 정보는 HttpSession 내에서 필요한 시간보다 오래 보관되지 않습니다.
이러한 ProviderManager의 특징은 가령 무상태 애플리케이션(stateless application)의 퍼포먼스를 증가시키려 할 때와 같이 사용자 객체의 캐시를 사용해야 할 때 문제가 될 수 있습니다. 만약 Authentication이 캐시 내에 객체의 참조(가령 UserDetails 객체)를 포함하고 있고 객체의 자격 증명이 제거되어 있다면 캐시된 값에 인증하는 것은 더 이상 가능하지 않습니다. 만약 캐시를 사용하고 있다면, 이 점을 고려하십시오. 분명한 해결책 하나는 캐시 구현체 혹은 Authentication 객체를 반환 생성한 AuthenticationProvider 내에 먼저 객체의 복사본을 만드는 것입니다. 대안으로는 ProviderManager의 프로퍼티 eraseCredentialsAfterAuthentication을 비활성화하는 방법도 있습니다.
AuthenticationProvider
다수의 AuthenticationProvider은 ProviderManager 내에 주입될 수 있습니다. 각각의 AuthenticationProvider은 특정 유형의 인증을 수행합니다. 예를 들어, DaoAuthenticationProvider은 사용자명/비밀번호 기반의 인증을, JwtAuthenticationProvider은 JWT 토큰 기반의 인증을 수행합니다.
Request Credentials with AuthenticationEntryPoint
AuthenticationEntryPoint는 클라이언트로부터의 자격 증명을 요청하는 HTTP 응답을 보내기 위해 사용됩니다.
때때로 클라이언트는 리소스를 요청하기 위해 사용자명/비밀번호와 같은 자격 증명을 사전에 포함합니다. 이러한 경우 클라이언트는 이미 자격 증명을 포함하였기 때문에 스프링 시큐리티는 자격 증명을 요청하기 위한 HTTP 응답을 보낼 필요가 없습니다.
클라이언트가 접근 권한이 없는 리소스에 인증되지 않는 요청을 하는 또 다른 경우에는 AuthenticationEntryPoint의 구현체가 클라이언트로부터 자격 증명을 받기 위해 사용됩니다. AuthenticationEntyPoint 구현체는 로그인 페이지로 리다이렉트를 수행하거나 WWW-Authenticate 헤더를 응답하는 등의 행동을 할 수 있습니다.
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter는 사용자의 자격 증명을 인증하는 기본 필터입니다. 자격 증명이 인증되기 전, 스프링 시큐리티는 관례적으로 AuthenticationEntryPoint를 사용해 자격 증명을 요청합니다.
이후, AbstractAuthenticationProcessingFilter가 제출된 인증 요청에 대한 인증을 수행합니다.
- 사용자가 자격 증명을 제출하면 AbstractAuthenticationProcessingFilter은 인증할 HttpServletRequest로부터 Authentication을 생성합니다. 생성된 Authentication의 타입은 AbstractAuthenticationProcessingFilter의 subclass에 의해 결정됩니다. 예를 들어, UsernamePasswordAuthenticationFilter은 httpServletRqeust 내 제출된 사용자명과 비밀번호로부터 UsernamePasswordAuthenticationToken을 생성합니다.
- 다음, Authentication이 AuthenticationManager로 전달되어 인증됩니다.
- 만약 인증을 실패하면,
- SecurityContextHolder이 비워집니다.
- RememberMeServices.loginFail이 호출됩니다. 만약 remember me가 구성되지 않았다면 아무런 동작도 진행되지 않습니다(this is a no-op).
- AuthenticationFailureHandler가 호출됩니다.
- 만약 인증을 성공하면
- SessionAuthenticationStrategy에 새 로그인이 통보됩니다.
- Authentication이 SecurityContextHolder에 설정됩니다. 이후 SecurityContextPersistenceFilter이 HttpSession에 SecurityContext를 저장합니다.
- RememberMeServices.loginSuccess가 호출됩니다. 만약 remember me가 구성되지 않았다면 아무런 동작도 진행되지 않습니다.
- ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent를 publish합니다.
- AuthenticationSuccessHandler이 호출됩니다.