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)단위로 입력.keyuserDetailsService()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()와 일치해야 함."remember-me"라는 파라미터를 찾아 RememberMe기능을 활성화할지 여부를 판단함."remember-me" 파라미터가 true로 전달되며, RememberMe 기능이 동작함.
RememberMeAuthenticationFilter는 세션에 SecurityContext가 Null인 경우(즉, 인증 객체가 없는 경우), or 사용자의 요청 헤더에 remember-me 쿠키가 있을 경우 동작함.RemembeMeService는 인터페이스.TokenBasedRememberMeServices, PersistentTokenBasedRememberMeServices 두 개의 구현체가 있음.TokenBasedRememberMeServices는 메모리에 있는 토큰과 요청 헤더에 담아서 보낸 토큰을 비교하여 인증을 수행.PersistentTokenBasedRememberMeServices는 DB에 저장된 토큰과 요청 헤더에 담아서 보낸 토큰을 비교하여 인증을 수행.rememberMe 인증 처리를 하는 구현체.RememberMeServices에서 수행되는 로직)Authentication을 생성하고 AuthenticationManager를 통해 인증을 수행.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을 통해 해당 유저의 아이디, 비밀번호, 권한을 확인.