스프링 시큐리티의 큰 구조에 이어서 스프링 시큐리티의 인증 로직과 구조에 대해서 자세히 알아보자. 지난 포스트를 안 본 사람은 아래 링크를 참고하자.
https://brightmango.tistory.com/360
SecurityContextHolder
스프링 시큐리티 인증 모델의 핵심은 누가 인증이 되었는지 저장하는 SecurityContextHolder이다. 만약에 이 holder가 값을 가지고 있다면, 이 값이 어떻게 채워졌는지와 상관없이 현재 인증된 사용자로서 사용될 수 있다.
SecurityContext가 authentication을 저장하는데 왜 SecurityContextHolder가 필요하나 싶을 수 있는데, SecurityContext를
현재 쓰레드와 연결해주는 역할을 한다.
[SecurityContextHolder 예제]
SecurityContext context = SecurityContextHolder.createEmptyContext(); - 1번
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER"); - 2번
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context); - 3번
1. Empty SecurityContext를 생성한다. (SecurityContextHolder.getContext().setAuthentication(authenticaion) 메서드로 생성하는 방법은 경쟁 상태에 빠질 수 있다.)
2. 새로운 Authentication 객체를 생성한다. 스프링은 어떤 타입의 Authentication인지 신경쓰지 않기 때문에 여기서는 가장 쉬운 TestingAuthenticationToekn을 사용한다.
3. 스프링이 권한을 주기 위해 사용할 인증 정보를 제공하기 위해, SecurityContexyHolder에 SecurityContext를 설정한다.
[Access Currently Authenticated User]
인증된 사용자에 접근하는 예제 코드이다.
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는 같은 쓰레드 안에서 항상 이용이 가능하다. 스프링 시큐리티의 FilterChainProxy는 SecurityContext를 항상 명확한 상태로 유지시켜 주기 때문에 ThreadLocal을 사용하는것은 매우 안전하다.
SecurityContext
SecurityContext는 SecurityContextHolder로 부터 얻어지며, Authentication 객체를 가지고 있다.
Authentication
Authentication은 스프링 시큐리티안에서 2가지 주요 목적을 가지고 있다.
- 인증을 위해 유저가 제공한 인증 정보를 가지고 있는 객체로서 AuthenticationManager의 input이다.
- 현재 인증된 유저를 대표한다.
다음과 같은 정보들을 포함하고 있다.
- principal : 유저를 식별한다.
- credentials : 주로 패스워드이며, 이 정보는 유출 방지를 위해 주로 인증이 끝나면 지워진다.
- authorities : GrantedAuthority는 가장 높은 허가이며, 예시로 roles or scopes가 있다.
GrantedAuthority
Authentication.getAuthorities()를 통해 얻어질 수 있으며, principal에 의해 승인된 권한이다. 주로 ROLE_ADMINISTRATOR / ROLE_HR_SUPERVISOR와 같은 roles와 관련된 권한들이다. 이러한 roles들은 나중에 웹 권한을 위해 설정된다. 예시로 username/password로 인증을 할 때, GrantedAuthority는 UserDetailsService로부터 인증 과정을 거친다.
AuthenticationManager
스프링 시큐리티 필터가 Authentication을 어떻게 수행하는지 정의한 API이다. 반환받은 Authentication은 컨트롤러(AuthenticaionManager를 호출한)에 의해 SecurityContextHolder에 설정된다. AuthenticationManager의 구현체는 어떤 것이든 될 수 있고, 가장 흔한 구현체는 ProvierManager이다.
ProviderManager
AuthenticationProvier 리스트를 선택하는 역할을 한다. 각각의 AuthenticationProvider는 input으로 들어온 authentication이 성공인지 실패인지 결정하거나, 구분 불가능하면 이를 다른 AuthenticationProvier가 결정할 수 있도록 실행하는 역할을 한다. 이 때 어떤 AuthenticationProvider도 인증할 수 없다면 ProviderNotFoundException을 발생시킨다.
ProviderManager는 Optional parent authenticationManager를 설정할 수 있으며, 다수의 ProviderManager가 같은 부모를 둘 수도 있다. (다수의 SecurityFilterChain 객체들이 예가 될 수 있다.) 또 한 서로 다른 인증 메커니즘들도 같은 부모를 둘 수 있다.
기본적으로 ProviderManager는 전달받은 민감한 인증 정보를 제거함으로써, HttpSession에 필요 이상으로 민감 정보가 남는 것을 방지한다. 따라서 인증 정보를 캐쉬를 사용해서 활용하는 경우 문제가 발생할 수 있다. 가장 명확한 해결법은 그 유저의 복사 객체를 만드는 것이다. 다른 방법으로는 eraseCredentialsAfterAuthentication을 disable하는 방법도 있다.
AuthenticationProvider
다수의 AuthenticationProvider는 ProviderManager와 연결될 수 있고, 각각은 특정 타입의 인증을 담당한다. 예를 들어 DaoAuthenticationProvider는 username/password 기반의 인증을 담당하고, JwtAuthenticationProvider는 JWT token 인증을 지원한다.
Request Credentials with AuthenticationEntryPoint
AuthenticationEntryPoint는 민감 정보를 요청하는 클라이언트로부터 HTTP response를 보낼 때 사용된다.
사용자는 특정 자원을 요청하기 위해 아이디나 패스워드를 요청에 포함시킬 때가 있는데, 이런 경우 스프링 시큐리티는 민감 정보를 요청하는 HTTP response를 제공할 필요가 없다. (이미 제공되었기 때문에)
만약 유저가 접근 허가되지 않은 자원에 요청을 하는 경우, AuthenticationEntryPoint의 구현체가 클라이언트에게 민감 정보를 요청하는데 사용될 수 있다.
AbstractAuthenticationProcessingFilter
사용자 민감정보를 인증 처리를 하는 필터로 사용된다. 민감 정보가 인증되기 전에 스프링 시큐리티는 AuthenticationEntryPoint를 사용해 민감정보를 요청한다. 그 다음에 AbstractAuthenticationProcessingFilter는 인증 요청을 처리한다.
과정은 다음과 같다.
- 사용자가 민감 정보를 요청했을 때, AbstractAuthenticationProcessingFilter는 HttpServletRequest로부터 authentication을 만든다. Authentication의 타입은 AbstractAuthenticationProcessingFilter의 하위 클래스로부터 정해진다. 예를 들어 UsernamePasswordAuthenticationFilter는 UsernamePasswordAuthenticationToken을 생성한다.
- Authentication이 authenticationManager로 전달된다.
3. 인증에 실패했을 때
- SecurityContextHolder가 비워진다.
- RememberMeServices.loginFail이 호출된다.
- AuthenticationFailureHnadler가 호출된다.
4. 인증에 성공했을 때
- 세션인증전략(SessionAuthenticationStrategy)에 새 로그인이 통보된다.
- 이 인증 정보는 SecurityContextHolder에 보관되며 나중에 SecurityContextPersistenceFilter가 SecurityContext를 HttpSession에 저장한다.
- RememberMeService.loginService가 호출된다.
- ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent를 발생시킨다.
- AuthenticationSuccessHandler가 호출된다.
실습
스프링 시큐리티 폼 로그인은 디폴트 설정으로도 가능하다. 하지만, servlet에서 설정 정보를 구성할 때, 폼 기반 로그인은 명확하게 제시되어야 한다.
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(withDefaults());
// ...
}
위와 같은 설정 정보로는 스프링 시큐리티는 디폴트 로그인 페이지를 렌더링한다. 하지만 대부분의 애플리케이션에서는 커스텀 로그인 페이지를 사용한다. 커스텀 로그인 페이지는 다음과 같이 사용할 수 있다.
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
// ...
}