인증이 어떻게 수행되는지 알아보는 챕터이다. 스프링 시큐리티 버전은 5.7.6 버전을 참고했다. Spring Security Servlet Application Authenctication Architecture
스프링 시큐리티는 인증에 대해 포괄적인 지원을 제공한다. 여러 인증의 구조를 추상적인 Authentication Architecture에 대해 알아보고, 좀더 각 유형의 인증 구조는 Authentication Mechanisms을 알아보자.
SecurityContextHolder
인증된 사용자의 세부정보를 저장한 스프링 시큐리티를 가지고 있다. 즉 SecurityContext
를 가지고 있는 것이다.
SecurityContext
SecurityContextHolder
에서 꺼내오고, 현재 인증된 사용자의 정보를 가지고 있다.
Authentication
사용자가 인증을 위해 제공한 자격 증명(credentias) 또는 SecurityContext
로부터 현재 유저 AuthenticationManager
에 입력으로 넣어 줄 수 있다. 그니까, Authentication
를 AuthenticationManager
에 넣고 인증 수행을 한다.
GrantedAuthority
위에서 기술한 Authentication
에 부여된 권한. (roles, scopes, 등)
AuthenticationManager
스프링 시큐리티의 필터들이 어떻게 실제로 인증을 수행하는 방법을 정의한 API
ProviderManager
위의 AuthenticationManager
을 구현한 가장 흔한 구현체
AuthenticationProvider
ProviderManager
에서 특정 유형의 인증을 수행하는데 사용
Request Credentials with AuthenticationEntryPoint
:
클라이언트에게 자격 증명을 요청하기 위해 사용
AbstractAuthenticationProcessingFilter
인증에서 사용되는 기본 필터, 인증과 각 구성들이 어떻게 작동하는지에 대한 높은 수준의 흐름을 파악할 수 있다.
스프링 시큐리티 인증의 모델의 중심에는 SecurityContextHolder
가 있다.
SecurityContextHolder
에 스프링 시큐리티가 인증된 사용자의 세부사항을 저장한 내용이 있다. 즉, SecurityContext
를 가지고 있다는 것이다.
스프링 시큐리티는 SecurityContextHolder
가 어떻게 내부를 채우는지 신경쓰지 않고, 오로지 값이 포함되어있다면, 그것을 현재 인증된 사용자로서 인식하고 사용한다.
사용자가 인증되었음을 나타내는 가장 간단한 방법은 SecurityContextHolder
를 직접 설정하는 것이다.
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
SecurityContext
를 생성한다. 여러 스레드에서의 경합 조건(race condition)을 피하려면 SecurityContextHolder.getContext().setAuthentication(authentication) 대신에 새로운 SecurityContext
를 생성하는 것이 중요하다.
새로운 Authentication
객체를 생성한다. 스프링 시큐리티는 SecurityContext
에서 Authentication
구현의 타입 설정이 무엇이든 신경쓰지 않는다. 여기서는 매우 간단한 TestingAuthenticationToken
을 사용한다. 일반적으로는 UsernamePasswordAuthenticationToken(userDetails, password, authorities)
를 사용한다.
SecurityContext
에 Authentication
을 담아 SecurityContextHolder
에 저장하고 스프링 시큐리티는 authorization
의 정보를 사용할 것이다.
만약 인증된 유저에 대한 정보를 얻고 싶은 경우, 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
가 해당 메서드에 대한 인수로 명시적으로 전달되지 않더라도 동일한 스레드의 메서드에서 항상 SecurityContext
를 사용할 수 있다.
ThreadLocal
의 사용은 현재 주체의 요청이 처리된 후에 스레드가 제거되는 것이 염려된다면 아주 좋은 방법일 수 있다. 스프링 시큐리티의 FilterChainProxy
는 항상 SecurityContext
가 지워지도록 한다.
일부 애플리케이션에는 스레드를 사용하는 특정 방법 떄문에 완전히 사용하기엔 적합하지 않다. 예를 들어, Swing 클라이언트는 자바 가상 머신에 있는 모든 스레드가 같은 시큐리티 컨텍스트에서 사용하길 원할지도 모른다.
시작 시점에 SecurityContextHolder
를 구성하여 컨텍스트를 저장할 방법을 지정할 수 있다. 독립 실행형 애플리케이션인 경우, SecurityContextHolder.MODE_GLOBAL
전략을 사용할 수 있다.
다른 애플리케이션에서는 보안 스레드에의해 생성된 스레드도 동일한 보안 주체로서 판단하는 경우도 있을 수 있다. 이것은 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
을 사용하여 수행할 수 있다.
기본 SecurtiyContextHolder.MODE_THREADLOCAL
에서 다른 모드로 바꾸는 방법은 두가지가 있다.
SecurityContextHolder
에서 static 메서드를 호출한다.하지만 대부분의 애플리케이션들은 기본 모드에서 변경할 필요가 없다. 그렇지만 변경하고 싶다면, Javadoc의 SecurityHolder
를 살펴보자.
SpringContextHolder
에서 꺼내오고, Authentication
객체를 포함하고 있다.
Authentication
가 스프링 시큐리티에서 제공하는 중요한 목적은 두가지가 있다.
AuthenticationManager
에 대한 입력으로 사용자가 인증을 위해 넘겨준 자격 증명(credentials)를 넘겨준다. 이러한 과정이 진행될 때, isAuthenticated()
는 false
를 반환한다.
현재 인증된 사용자를 보여준다. 현재 Authentication
은 SecurityContext
에서 얻을 수 있다.
principal
사용자를 식별한다. username/password를 사용하여 인증할 때, UserDetails
의 인스턴스가 principal
이 된다.
credentials
패스워드로 사용되기도 한다. 보통 사용자가 유출되지 않도록 인증한 후 삭제'
authorities
사용자에게 부여된 상위 수준 레벨(역할, 스코프 등)
GrantedAuthority
는 사용자게에 부여되는 상위 수준 권한이다(roles, scopes 등)
GrantedAuthority
는 Authentication.getAuthorities()
에서 얻을 수 있다. 이 메서드는 GrantedAuthority
객체들을 컬렉션으로 반환한다.
여기서 GrantedAuthority
는 principal
에 부여된 권한이다. 이러한 권한은 보통 ROLE_ADMINISTRATOR
, ROLE_HR_SUPERVISOR
와 같은 ROLE이다. ROLES는 나중에 웹 권한 부여, 메서드 권한 부여, 도메인 개체 권한 부여를 위해 구성된다.
스프링 시큐리티의 다른 부분에서 이러한 권한을 알고 있고, 해석할 수 있다. username/password 인증을 사용할 때 GrantedAuthority
는 보통 UserDetailsService
에서 로드 된다.
보통 GrantedAuthority
개체는 특정 도메인에 한해서가 아닌 애플리케이션 전반에서의 사용 권한을 말한다. 대신 스프링에서는 특정 도메인 보안 기능을 제공한다.
AuthenticationManager
는 Spring Security의 필터들이 인증을 실제 어떻게 수행하는지에 대해 정의한 API이다. 반환된 Authentication
은 AuthenticationManager
를 통해 호출된 컨트롤러(예: 스프링 시큐티의 필터들)에 의해 SecurityContextHolder
에 설정된다.
만약 스프링 시큐리티의 필터들을 통합하지 않는다면, 직접 SecurityContextHolder
를 설정할 수 있고, AuthenticationManager
를 사용하지 않아도 된다.
AuthenticationManager
의 구현체는 무엇이든 될 수 있지만, 가장 흔한 구현체는 ProvideManager
이다.
ProviderManager
는 가장 일반적으로 사용되는 AuthenticationManager
의 구현체이다. ProviderManager
는 AuthenticationProvider
들의 리스트에 역할을 위임한다.
각각의 AuthenticationProvider
는 인증이 성공, 실패 또는 후속의 ProviderManager
가 결정할 수 없음을 표시해야한다. 구성된 AuthenticationProvider
들 중 어느 것도 인증을 수행 할 수 없을 경우,
전달된 Authentication
의 유형을 지원하는 ProviderManager
가 없음을 나타내는 AuthenticationException
인 ProviderNotFoundException
가 발생하며 실패할 것이다.
각 AuthenticationProvider
는 특정 유형의 인증을 수행하는 방법을 알고 있다. 예를 들어, 하나의 AuthenticationProvider
는 username/password 검증을 할수 있고, 다른 AuthenticationProvider
는 SAML assertion 인증을 할수 있다.
각 AuthenticationProvider
는 여러 유형의 인증을 제공하고, 단일 AuthenticationManager
을 bean으로 보내면서 매우 특정한 유형의 인증을 수행할 수 있다.
또한 ProviderManager
는 AuthenticationProvider
가 인증을 수행할 수 없는 경우 참조되는 상위 AuthenticationManager
를 선택사항으로 설정할 수 있다.
상위 AuthenticationManager
의 타입은 무엇이 되도 상관은 없지만, 보통 ProviderManager
의 인스턴스이다.
사실, 여러 ProviderManager
인스턴스들은 같은 부모 AuthencticationManager
를 공유한다.
그래서 보통 상위 AuthenticationManager
는 공통으로 같지만, 실제 ProviderManager
의 인스턴스는 모두 다른 인증 메커니즘을 가지고있고 이러한 ProviderManager
인스턴스들을 여러 SecurityFilterChain
인스턴스가 가지고있는 방식이다.
기본적으로, ProviderManager
는 인증 요청에 성공하여 반환된 Authentication
객체의 민감한 자격 증명 정보를 지우려고 할것이다. 이는 HttpSession에서 필요 이상으로 패스워드 같은 정보가오래 유지되는 것을 막기 위해서다.
stateless 애플리케이션의 성능을 개선하기 위해 사용자 객체의 캐시를 사용할 때 문제를 일으 킬 수 있다. 만약 Authentication
이 포함한다면 UserDetails
인스턴스와 같은 캐시에서 객체 참조를 갖고 있고, 해당 객체의 자격 증명을 삭제한다면 캐시된 값에 대해 더이상 인증할 수 없다.
만약 캐시를 사용한다면 고려해야할 필요가 있다. 확실한 해결책은 반환된 Authentication
을 생성하는 AuthenticationProvider
또는 캐시 구현에서 객체의 복사본을 만드는 것이다.
다른 방법으로는, ProviderManager
에서 eraseCredentialsAfterAutheication
속성을 사용하지 않게 할 수 있다. 자세한 내용은 Javadoc 참조.
여러 AuthenticationProvider
를 ProviderManager
에 주입 할 수 있다. 각 AuthenticationProvider
는 특정 유형의 인증을 수행한다.
예를 들어 DaoAuthenticationProvider
는 Username/Password 기반 인증을 지원하고, JwtAuthenticationProvider
는 JWT 토큰 인증 방식을 지원한다.
클라이언트에게 자격 증명을 요청하라는 HTTP 응답을 보내는데 사용한다. 가끔 클라이언트가 username/password
와 같은 자격 정보를 리소스 요청을 위해 사전에 포함하기도 하는데, 이런 경우에는 이미 자격 정보가 포함되어있기 때문에 스프링 시큐리티는 클라이언트에게 자격 증명 요청 정보 HTTP Response를 보내지 않아도 된다.
하지만 대부분, 클라이언트는 접근 권한이 없는 리소스에 인증되지 않은 요청을 한다. 이럴 때, AuthenticationEntryPoint
의 구현체는 클라이언트에게 자격 증명 요청으로 사용된다.
AuthenticationEntryPoin
구현체는 로그인 페이지로 리디렉트를 수행하거나, WWW-Authenticate 헤더 응답을 보낸다.
AbstractAuthenticationProcessingFilter
는 사용자의 자격 증명을 위한 기본 필터로 사용된다. 자격 증명(credentials)가 인증되기 전에, 스프링 시큐리티는 일반적으로 AuthenticationEntryPoint
를 사용하여 자격 증명(credentials)를 요청한다.
AbstractAuthenticationProcessingFilter
은 Submit 된 모든 유형의 인증 요청을 수행할 수 있다.
사용자가 자격 증명(credentials)를 Submit 할때, AbstractAuthenticationProcessingFilter
는 인증할 HttpServletRequest
에서 Authentication
을 생성한다.
생성된 Authentication
의 타입은 AbstractAuthenticationProcessingFilter
의 하위 클래스에 의존한다. 예를 들어, UsernamePasswordAuthenticationFilter
는 HttpServletRequest
에서 넘어온 username과 password를 통해 UsernamePasswordAuthenticationToken
을 생성한다. 이 구현 객체가 Authenctication
이 된다.
다음으로, Authentication
은 인증을 위해 AuthenticationManager
로 전달 된다.
만약 인증 실패라면, Failure 단계로 진행된다.
SecurityContextHolder
를 초기화한다.RememberMeService.loginFail
이 호출된다. 만약 Remember me
가 설정되어 있지 않다면, 작동하지 않는다.AuthenticationFailureHandler
를 호출한다.인증에 성공할 경우. Success 단계로 진행된다.
SessionAuthenticationStrategy
는 새 로그인에 대한 알림을 받는다.
Authentication
은 SecurityContextHolder
에서 설정되고, 나중에 SecurityContextPersistenceFilter
는 SecurityContext
를 HttpSession
에 저장
RememberMeService.loginSuccess
가 호출, Remember Me
가 설정되어 있지 않다면 작동되지 않음.
ApplicationEeventPublisher
는 InteractiveAuthenticationSuccessEvent
를 올린다.
AuthenticationSuccessHandler
가 호출
SecurityContextHolder
는 SecurityContext
를 가지고 있는 인증의 핵심 부분이다.
SecurityContext
는 인증된 사용자의 정보를 가지고 있는 데, 이 정보가 Authentication
이다.
Authentication
은 두가지의 사용 목적이 있다.
AutheticationManager
에게 사용자에게 받은 자격 증명 정보를 넘겨주어 인증을 수행한다.
현재 인증된 유저의 정보를 확인 할 수 있다. 현재 Authentication
은 SecurityContext
에서 꺼낼 수 있다.
GrantedAuthority
는 사용자에게 부여하는 권한이다. 권한은 애플리케이션 전반에 걸쳐 적용된다.
각 도메인 마다 사용자에게 여러권한을 주면 한 사람당 여러 권한이 들어오게 되고 사용자가 많아진다면 메모리에 부하가 걸린다.
그래서 각 도메인마다 특정 제약을 걸어 접근 제한을 할수 있다고한다.
AuthenticationManager
는 실제 스프링 시큐리티 필터가 인증을 수행하는 API이다.
ProviderManager
는 AuthenticationManager
의 가장흔한 구현체이다. 그리고 여러 유형의 ProviderManager
가 있고, 시큐리티의 필터체인 인스턴스는 각 유형의 ProviderManager
를 사용한다.
AuthenticationProvider
를 ProviderManager
에 주입할 수 있다. jwt 방식의 인증, username/password 방식의 인증 등
AuthenticationEntryPoint
인증되지 않은 요청이 올때, 서버가 클라이언트로부터 사용자의 자격 증명 정보를 요청하기위해 로그인 페이지로 리디렉트하거나, 헤더에 WWW-Authenticate 담아 응답한다.
AbstractAuthenticationProcessingFIlter
가 이제 실제 인증을 수행하는 필터이다.
그래서 AbstarctAuthenticationProcessingFilter
가 실제 인증을 수행하기 위한 부분이고, 여기서 AuthenticationManager 등으로 인증 수행을 해야하는 것같다. AbstractAuthenticationPorocessingFilter
의 구현체도 찾아보고 상속받아서 내가 원하는 유형의 인증 필터를 만들어보자, 그리고 ProviderManager
를 상속받아 JwtProviderManager
로 만들어 커스텀 해보자.