이 시리즈에 나오는 모든 내용은 인프런 인터넷 강의 - [ 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security ] - 에서 기반된 것입니다. 그리고 여기서 인용되는 PPT 이미지 또한 모두 해당 강의에서 가져왔음을 알립니다.
세션이 만료되고 브라우저를 끈 후에도 사용자를 기억하는 기능
HTTP 요청에 있는 Remember-me
쿠키를 확인하여, 만약 쿠키가 있다면
이전에 배운 토큰 기반 인증 프로세스를 거쳐서 재로그인이 됨
만약에 어떤 이유로든 이후에 인증 프로세스에 실패하거나 로그아웃하면
쿠키를 무효화한다.
// .... import 생략 ..... //
import org.springframework.security.core.userdetails.UserDetailsService;
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// .... 생략 ..... //
// Remember-Me
http.rememberMe()
.rememberMeParameter("remember") // 기본명은 remember-me 이다
.tokenValiditySeconds(3600) // 만료시간, 기본은 14일
// .alwaysRemember(true) // 로그인하면 Remember-Me 기능 무조건 활성화 여부
.userDetailsService(userDetailsService);
// 안 하면 "java.lang.IllegalStateException: UserDetailsService is required."
// 내부적으로 재인증 처리를 위해서는 이게 필요하다고 한다.
}
}
remember-me 기능 활성화 하고 로그인에 성공하면 remember-me 쿠키 생성된 것을 확인
로그아웃 이후에 remember-me 쿠키 삭제
이제 이런 Remember Me 의 기능을 실제 처리하는 Filter 클래스인
RememberMeAuthenticationFilter
에 대해서 알아보자.
위 그림처럼 동작하기 전에 필터는 기본적으로 필터 기능이 동작할지 말지를 결정한다.
그 결정 조건은 아래와 같다.
위 그림에서 RememberMeServices는 실제 RememberMe 인증처리를 수행한다.
RememberMeServices 의 종류는 2가지이다.
RememberMeServices 객체가 Token Cookie 를 추출하고
사용자가 들고 있는 Token 이 RememberMe Token 인지 확인한다.
존재하면 해당 토큰을 디코딩하고, 서버에 저장되어 있던 Token 을 비교한다.
일치한다면, 해당 토큰 안에 있던 User 계정이 존재하는지 확인한다.
존재하면 새로운 Authentication 을 생성하고 AuthenticationManager 에게 실제 인증 처리를 한다.
일단 필터가 동작되기 전에 Remember Me 쿠키 정보가 정확히 어떤 시점에
생성되는지 부터 알아둘 필요가 있다. 코드를 추적해보자.
인증 성공/실패를 담당하는 추상 클래스인 AbstractAuthenticationProcessingFilter
클래스에 아래와 같이 디버깅 포인트를 잡는다. 이후에 로그인할 때 Remember me 기능을 활성화 하고 로그인해서 메소드를 따라가보자.
loginSuccess 메소드가 주요 관심사인데, 내부 내용은 다음과 같다.
크게 rememberMeRequested
메소드와 onLoginSuccess
메소드의 내용만 알아보자.
rememberMeRequested 메소드
현재 요청의 쿼리 파라미터에 remember-me=on
이 있는지 확인한다.
이건 우리가 앞서 스프링 시큐리티가 기본으로 제공하는 로그인 화면에서
remember-me 기능을 활성화 시키면 같이 보내준다.
onLoginSuccess 메소드
내부적으로 UserDetailsService 라는 객체를 사용하는 것을 확인할 수 있다.
http.rememberMe().userDetailsService(userDetailsService);
API 가 필수인
이유이기도 하다.
아무튼 메소드 내부적으로 쿠키 만료시간을 지정하고,
쿠키의 시그니쳐로 string 을 생성한다. MD5 암호화를 거친 문자열이다.
아무튼 이렇게 생성한 쿠키값을 remember-me 쿠키에 넣고 response 에 넣어준다.
그런데 사실 위의 과정은 TokenBasedRememberMeServices
의 동작이고,
PersistentToeknBasedRememberMeServices
은 조금 다르게 동작한다.
onLoginSuccess 메소드(PersistentToeknBasedRememberMeServices
의 경우)
이전과는 달리 여기서는 Token 객체를 직접 생성하는 것을 확인할 수 있다.
그래서 TokenRepository 라는 저장소에 해당 토큰을 저장하고,
TokenBasedRememberMeServices 마찬가지로 response 에 쿠키를 추가한다.
이제 Remember Me 쿠키가 적재된 상태에서 세션 아이디인 JSESSIONID 를 브라우저에서
지우고 새로고침을 해서 테스트를 해보자.
// RememberMeAuthenticationFilter 클래스의 일부
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// JSESSIONID 를 지웠다면 이게 null 이 된다.
if (SecurityContextHolder.getContext().getAuthentication() != null) {
//... 생략 ... //
chain.doFilter(request, response);
return;
}
// 여기서 쿠키의 값을 복호화하고 그 값들을 이용해서
// RememberMeAuthenticationToken 을 생성한다.
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
// ProviderManager -> RememberMeAuthenticaitonProvider 호출
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(rememberMeAuth);
SecurityContextHolder.setContext(context);
onSuccessfulAuthentication(request, response, rememberMeAuth);
this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
}
catch (AuthenticationException ex) {
this.logger.debug(LogMessage
.format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
+ "rejected Authentication returned by RememberMeServices: '%s'; "
+ "invalidating remember-me token", rememberMeAuth),
ex);
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
}
}
chain.doFilter(request, response);
}
메소드의 내용을 요약하자면 다음과 같다.
해당 인증 토큰을 받은 필터는 이것을 통해서 ProviderManager 에게 인증 처리를 위임한다.
ProviderManager 는 자신이 들고 있는 여러 Provider 중에서도 RememberMeAuthenticationToken
을 처리해줄 수 있는 Provider(=RememeberMeAuthenticationProvider
)를 찾아내고, 해당 Provider 에게 다시 한번 인증 처리를 위임한다.
해당 인증 통과했다면 돌려받는 인증 토큰을 SecurityContext 에 저장한다(Form 인증과 비슷함)