[Spring Security] Remember Me 인증

식빵·2022년 8월 7일
0
post-thumbnail

이 시리즈에 나오는 모든 내용은 인프런 인터넷 강의 - [ 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security ] - 에서 기반된 것입니다. 그리고 여기서 인용되는 PPT 이미지 또한 모두 해당 강의에서 가져왔음을 알립니다.




🥝 Remember Me


  • 세션이 만료되고 브라우저를 끈 후에도 사용자를 기억하는 기능

  • HTTP 요청에 있는 Remember-me 쿠키를 확인하여, 만약 쿠키가 있다면
    이전에 배운 토큰 기반 인증 프로세스를 거쳐서 재로그인이 됨

  • 만약에 어떤 이유로든 이후에 인증 프로세스에 실패하거나 로그아웃하면
    쿠키를 무효화한다.


🥥 API

// .... 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 쿠키는 계속해서 존재함.

로그아웃 후

로그아웃 이후에 remember-me 쿠키 삭제





🥝 RememberMeAuthenticationFilter


이제 이런 Remember Me 의 기능을 실제 처리하는 Filter 클래스인
RememberMeAuthenticationFilter 에 대해서 알아보자.


🥥 프로세스

위 그림처럼 동작하기 전에 필터는 기본적으로 필터 기능이 동작할지 말지를 결정한다.
그 결정 조건은 아래와 같다.

  • 인증 객체 부재
    • 세션이 만료되어 세션 내에 SecurityContext 가 없는 경우
    • SecurityContext 내에 인증객체가 없는 경우
  • 요청에 Remember-me 쿠키 존재

위 그림에서 RememberMeServices는 실제 RememberMe 인증처리를 수행한다.
RememberMeServices 의 종류는 2가지이다.

  • TokenBasedRememberMeServices : 메모리에서 실제로 저장한 토큰과 요청할 때 들고온 쿠키를 비교한다.(기본으로 14일 동안 들고 있음)
  • PersistentToeknBasedRememberMeServices : DB 에 저장된 토큰의 값과 요청할 때 들고온 쿠키를 비교한다.

RememberMeServices 객체가 Token Cookie 를 추출하고
사용자가 들고 있는 Token 이 RememberMe Token 인지 확인한다.

존재하면 해당 토큰을 디코딩하고, 서버에 저장되어 있던 Token 을 비교한다.
일치한다면, 해당 토큰 안에 있던 User 계정이 존재하는지 확인한다.
존재하면 새로운 Authentication 을 생성하고 AuthenticationManager 에게 실제 인증 처리를 한다.



🥥 Remember Me 쿠키 생성 코드 추적

일단 필터가 동작되기 전에 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);
}

메소드의 내용을 요약하자면 다음과 같다.

  • SecurityContext 에서 인증객체 확인
  • 인증객체가 없다면 일단 RememberService 를 통해서 인증 토큰를 하나 생성한다.

  • 해당 인증 토큰을 받은 필터는 이것을 통해서 ProviderManager 에게 인증 처리를 위임한다.

  • ProviderManager 는 자신이 들고 있는 여러 Provider 중에서도 RememberMeAuthenticationToken 을 처리해줄 수 있는 Provider(=RememeberMeAuthenticationProvider)를 찾아내고, 해당 Provider 에게 다시 한번 인증 처리를 위임한다.

  • 해당 인증 통과했다면 돌려받는 인증 토큰을 SecurityContext 에 저장한다(Form 인증과 비슷함)





🌎 보충 링크


profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글