Spring Security RememberMeAuthenticationFilter

RememberMe 기능은 해당 사이트 로그인 시 세션 종료 및 브라우저 종료 하더라도 로그인 상태를 유지 할 수 있는 기능이다.
개요
일반적으로 로그인을 했다는 것은 해당 WAS 서버에 해당 사용자의 세션이 생성 되었고
Spring Security 가 인증 객체 통해 보관 하고 있다.
그럼 사용자는 다른 페이지에 접속해도 해당 세션을 request 시 함께 서버로 전달 하고
서버는 Spring Security 가 인증 객체 통해 보관 하고 있는 세션과 맞물려 있는지 체크 한다.
즉 인증 체크를 한다는 것 이다.
그러나 그 세션이 기한이 끝났거나 소멸될 경우 다시 로그인을 해야 하는데
RememberMe 기능은 소멸된 세션이라 할지라도 remember-me 라는 또 다른 세션을 생성 후 클라이언트에 response 한다.

그럼 일반 세션을 변조 하거나 value 값을 삭제 할 경우

이렇게 일반 세션을 변조 하였다. 그럼 일반적으로 해당 세션값은 서버에 등록된 1234 값이 없기 때문에 인증 처리 되지 않아 다시 로그인 해야 한다.
하지만 새로고침 하면 remember-me 세션 토큰값 통해 Spring Security 에는 파싱 해서 아이디 및 비밀번호 다시 추출해
다시 인증 처리를 하게 된다.

여기까지 기본 정의를 설명 하였고 본격적으로 Spring Security 에서는 rememberMe 를 어떻게 처리 및 Flow 하는지
알아보겠다.
기본 Spring Security 설정
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | @Configuration@EnableWebSecurity
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
 ... 생략 ...
 
 @Override
 protected void configure(HttpSecurity http) throws Exception {
 
 ... 생략 ...
 
 http
 .rememberMe()
 .rememberMeParameter("remember")
 .tokenValiditySeconds(3600)
 .userDetailsService(userDetailsService);
 
 ... 생략 ...
 
 }
 
 ... 생략 ...
 
 }
 
 | 
AbstractAuthenticationProcessingFilter -> successfulAuthentication
최종적으로 로그인 인증 처리가 완료 되면 다시 AbstractAuthenticationProcessingFilter 로 돌아 오게 되는데
successfulAuthentication 메소드를 호출 하게 된다.
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | // AbstractAuthenticationProcessingFilter 클래스protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
 .. 생략 ..
 SecurityContextHolder.getContext().setAuthentication(authResult);
 this.rememberMeServices.loginSuccess(request, response, authResult);
 
 
 .. 생략 ..
 }
 
 | 
rememberMeServices.loginSuccess 호출 해서 해당 remember-me Token 생성 한다.
| 12
 3
 4
 5
 6
 7
 8
 
 | // AbstractRememberMeServices 클래스public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
 if (!this.rememberMeRequested(request, this.parameter)) {
 this.logger.debug("Remember-me login not requested.");
 } else {
 this.onLoginSuccess(request, response, successfulAuthentication);
 }
 }
 
 | 
onLoginSuccess 메소드를 호출 해서
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | // AbstractRememberMeServices 구현체인 TokenBasedRememberMeServices 클래스public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
 String username = this.retrieveUserName(successfulAuthentication);
 String password = this.retrievePassword(successfulAuthentication);
 if (!StringUtils.hasLength(username)) {
 this.logger.debug("Unable to retrieve username");
 } else {
 if (!StringUtils.hasLength(password)) {
 UserDetails user = this.getUserDetailsService().loadUserByUsername(username);
 password = user.getPassword();
 if (!StringUtils.hasLength(password)) {
 this.logger.debug("Unable to obtain password for user: " + username);
 return;
 }
 }
 
 int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
 long expiryTime = System.currentTimeMillis();
 expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
 String signatureValue = this.makeTokenSignature(expiryTime, username, password);
 this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
 
 ... 생략 ...
 }
 }
 
 | 
remember-me token 생성하는 로직이다.
아이디 패스워드 만료시간 등 조합해서 setCookie 하게 되고 response 시 생성한 token 을 클라이언트에게 전달 하게 된다.
RememberMeAuthenticationFilter

Spring Security 에서는 RememberMeAuthenticationFilter 이 객체에 전달하기 위해
조건이 있다. 우선 인증된 세션이 서버에 찾지 못 하거나 기간이 만료되거나 NULL 경우
그리고 클라이언트가 request 시 remember-me 쿠키 토큰값을 가지고 오는 경우
RememberMeAuthenticationFilter 객체가 발동 한다.
클라이언트 측에서 세션값을 제거 하거나 조작하고 다시 해당 페이지 새로고침 해보자!
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | // RememberMeAuthenticationFilter 클래스public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
 HttpServletRequest request = (HttpServletRequest)req;
 HttpServletResponse response = (HttpServletResponse)res;
 
 // SecurityContext 에 있는 인증 객체가 Null 일때 rememberMe Flow 하게 된다.
 if (SecurityContextHolder.getContext().getAuthentication() == null) {
 Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
 if (rememberMeAuth != null) {
 try {
 rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
 SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
 this.onSuccessfulAuthentication(request, response, rememberMeAuth);
 
 .. 생략 ..
 }
 
 chain.doFilter(request, response);
 } else {
 .. 생략 ..
 }
 
 }
 
 | 
this.rememberMeServices.autoLogin(request, response);
기존 세션값이 null 이기 때문에 remember-me token 을 이용해 다시 인증 처리 하게 한다.
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | // AbstractRememberMeServices 클래스public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
 
 // 발급 받는 remember-me token 값이 있는 확인 한다.
 String rememberMeCookie = this.extractRememberMeCookie(request);
 
 if (rememberMeCookie == null) {
 return null;
 } else {
 ... 생략 ...
 return this.createSuccessfulAuthentication(request, user);
 ... 생략 ...
 }
 }
 }
 
 
 | 
return this.createSuccessfulAuthentication(request, user);
인증 객체 생성 메소스 호출 한다.
| 12
 3
 4
 5
 6
 
 | // AbstractRememberMeServices 클래스protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) {
 RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, user, this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
 auth.setDetails(this.authenticationDetailsSource.buildDetails(request));
 return auth;
 }
 
 | 
여기서 인증 객체를 생성 후 RememberMeAuthenticationFilter 클래스 doFilter 메소드에 반환 해서
| 12
 
 | // RememberMeAuthenticationFilter > doFilterrememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
 
 | 
authenticationManager 호출해서 다시 authenticationProvider 인증 처리 위임해 평상시대로 인증 처리를 하게 된다.
| 12
 
 | // RememberMeAuthenticationFilter > doFilterSecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
 
 | 
이상으로 인증이 성공적으로 처리 되었다면 SecurityContext 에 해당 사용자 인증 객체에 저장 하게 된다.
      
       
 Copyright 201- syh8088. 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.