스프링 시큐리티(Spring Security) - rememberMe.

하쮸·2025년 3월 18일

1. rememberMe

  • rememberMe는 세션이 만료되고 웹 브라우저가 종료된 상황에서도 애플리케이션이 사용자를 기억하는 기능임.
    • 로그인 시 Remember-Me 쿠키를 서버에서 발급해서 사용자가 쿠키를 갖고 있다면 토큰 기반 인증을 사용해서 유효성을 검증하고 사용자를 로그인 시켜주는 방식.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
	private final UserSecurityService userSecurityService;

	
	@Bean
	SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
						
                        .........
                        
				
				.formLogin((formLogin) -> formLogin
						.loginPage("/user/login").defaultSuccessUrl("/"))
				
				.logout((logout) -> logout
						.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout")).logoutSuccessUrl("/").invalidateHttpSession(true).deleteCookies("JSESSIONID", "remember-me"))
				.rememberMe((rememberMe) -> rememberMe.key("uniqueAndSecretKey").rememberMeCookieName("rememberUser").tokenValiditySeconds(604800).userDetailsService(userSecurityService));
                
		return httpSecurity.build();
	}
.rememberMe((rememberMe) -> rememberMe
.key("uniqueAndSecretKey")
.rememberMeCookieName("rememberUser")
.tokenValiditySeconds(604800)
.userDetailsService(userSecurityService));
  • rememberMeParameter()
    • 위 코드에는 없지만, 기본값은 remember-me로, 체크박스 체크 시 넘겨주는 이름값을 설정.
  • tokenValiditySeconds()
    • 기본값은 14일, 초(second)단위로 입력.
  • key
    • 토큰을 생성할 때 사용되는 키.
  • userDetailsService()
    • rememberMe기능을 수행할 때 사용자의 계정을 조회하는 기능을 담당.
    • 현재 코드에서는 UserDetailsService를 구현한 구현체(userSecurityService)를 넣었음.
  • rememberMeCookieName()
    • 클라이언트에 저장할 쿠키 이름 설정.

login_form.html

		<form th:action="@{/user/login}" method="post">
			<div th:if="${param.error}">
				<div class="alert alert-danger">ID 또는 비밀번호를 다시 확인.</div>
			</div>
			<div class="mb-3">
				<label for="username" class="form-label">아이디</label>
				<input type="text" name="username" id="username" class="form-control">
			</div>
			<div class="mb-3">
				<label for="password" class="form-label">비밀번호</label>
				<input type="password" name="password" id="password" class="form-control">
			</div>
			<label for="remember-me" class="form-label">자동로그인</label>
			<input type="checkbox" name="remember-me" id="remember-me"/>
			<button type="submit" class="btn btn-primary">로그인</button>
		</form>	
  • name="remember-me"로 설정한 부분이 rememberMeParameter()와 일치해야 함.
    • 이렇게 설정하면 로그인 요청 시, Spring Security는 "remember-me"라는 파라미터를 찾아 RememberMe기능을 활성화할지 여부를 판단함.
    • 사용자가 체크하면 "remember-me" 파라미터가 true로 전달되며, RememberMe 기능이 동작함.

1-1. 내부 동작.

  • RememberMeAuthenticationFilter세션SecurityContext가 Null인 경우(즉, 인증 객체가 없는 경우), or 사용자의 요청 헤더에 remember-me 쿠키가 있을 경우 동작함.
  • RemembeMeService는 인터페이스.
    • TokenBasedRememberMeServices, PersistentTokenBasedRememberMeServices 두 개의 구현체가 있음.
      • TokenBasedRememberMeServices는 메모리에 있는 토큰과 요청 헤더에 담아서 보낸 토큰을 비교하여 인증을 수행.
        (기본적으로 14일만 토큰을 유지.)
      • PersistentTokenBasedRememberMeServices는 DB에 저장된 토큰과 요청 헤더에 담아서 보낸 토큰을 비교하여 인증을 수행.
        (영구적인 방법)
    • 위 두 구현체가 실제로 rememberMe 인증 처리를 하는 구현체.
  • 요청에서 토큰을 추출.
  • Token이 존재하는지 확인. (RememberMeServices에서 수행되는 로직)
    • 토큰이 없다면 다음 filter로 넘어감.
    • 토큰이 옳바른 형태이면서 해당 토큰 값과 서버의 값의 일치하는지, 사용자의 정보가 일치하는지 확인 후 Authentication을 생성하고 AuthenticationManager를 통해 인증을 수행.

1-2. 디버깅으로 확인.

package org.springframework.security.web.authentication.rememberme;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
    private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
    private ApplicationEventPublisher eventPublisher;
    private AuthenticationSuccessHandler successHandler;
    private AuthenticationManager authenticationManager;
    private RememberMeServices rememberMeServices;
    private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
    private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();

    public RememberMeAuthenticationFilter(AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) {
        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
        Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
        this.authenticationManager = authenticationManager;
        this.rememberMeServices = rememberMeServices;
    }

    public void afterPropertiesSet() {
        Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
        Assert.notNull(this.rememberMeServices, "rememberMeServices must be specified");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (this.securityContextHolderStrategy.getContext().getAuthentication() != null) {
            this.logger.debug(LogMessage.of(() -> {
                return "SecurityContextHolder not populated with remember-me token, as it already contained: '" + this.securityContextHolderStrategy.getContext().getAuthentication() + "'";
            }));
            chain.doFilter(request, response);
        } else {
            Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
            if (rememberMeAuth != null) {
                try {
                    rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
                    this.sessionStrategy.onAuthentication(rememberMeAuth, request, response);
                    SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
                    context.setAuthentication(rememberMeAuth);
                    this.securityContextHolderStrategy.setContext(context);
                    this.onSuccessfulAuthentication(request, response, rememberMeAuth);
                    this.logger.debug(LogMessage.of(() -> {
                        return "SecurityContextHolder populated with remember-me token: '" + this.securityContextHolderStrategy.getContext().getAuthentication() + "'";
                    }));
                    this.securityContextRepository.saveContext(context, request, response);
                    if (this.eventPublisher != null) {
                        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(this.securityContextHolderStrategy.getContext().getAuthentication(), this.getClass()));
                    }

                    if (this.successHandler != null) {
                        this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
                        return;
                    }
                } catch (AuthenticationException var6) {
                    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), var6);
                    this.rememberMeServices.loginFail(request, response);
                    this.onUnsuccessfulAuthentication(request, response, var6);
                }
            }

            chain.doFilter(request, response);
        }
    }

    protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
    }

    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
    }

    public RememberMeServices getRememberMeServices() {
        return this.rememberMeServices;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
        Assert.notNull(successHandler, "successHandler cannot be null");
        this.successHandler = successHandler;
    }

    public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
        Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
        this.securityContextRepository = securityContextRepository;
    }

    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
        Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
        this.securityContextHolderStrategy = securityContextHolderStrategy;
    }

    public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
        Assert.notNull(sessionStrategy, "sessionStrategy cannot be null");
        this.sessionStrategy = sessionStrategy;
    }
}
  • doFilter()에 브레이크 포인트를 걸고 확인.

  • 시큐리티 설정파일에서 설정한 부분들을 확인.

  • 커스텀 시큐리티 필터체인에 등록되어 있는 체인들을 확인.

  • rememberMeAuth에 담긴 principle을 통해 해당 유저의 아이디, 비밀번호, 권한을 확인.

2. 참고.

profile
Every cloud has a silver lining.

0개의 댓글