Remember-ME?
세션이 만료되고 웹 브라우저가 종료된 후에도 어플리케이션이 사용자를 기억하는기능이다.
보통 로그인 창에는 체크박스로 로그인 기억(로그인 저장) 등을 지원하는데, 사용자는 일반적으로 로그인 후에 서버에서 Remember-me 관련 쿠키로(cookie) 보내주면 다음 요청시, 명시적으로 로그아웃 하지 않는이상 계속 로그인 되있는 것처럼 쿠키 기반으로 인증을 하는 방식이다.
Remember-Me가 무엇인지 정리하자면 다음과 같다
- 일반적으로 로그인 한 후에 브라우저를 끄거나, 일정 시간이 지나 세션이 만료되는 경우 명시적으로 로그아웃 하지 않더라도 로그아웃이 된다.
- 이 때 재 로그인 하지 않더라도 세션이 만료되고 웹 브라우저가 종료된 후에도 어플리케이션이 사용자를 기억하는기능이다.
- 인증되지 않은 사용자의 HTTP 요청이 remember-me 쿠키(Cookie)를 갖고 있다면, 사용자를 자동으로 인증처리한다
페이지에 접속하면 서버에서 세션이 생성되고 웹 브라우저 쿠키에 세션 아이디 정보(JSessionID)가 담긴다.
로그인을 하면 서버는 해당 세션을 인증된 세션으로 취급.
JSESSIONID 쿠키가 동작하는 경우, 세션 필터에 의해 먼저 인증되어서 rememberme는 동작하지 않는다.
세션 기능을 끌라면 http.sessionManagement().disable()
를 사용 하자
Remember-Me 기능 활성화
일반적으로 RememberMeAuthenticationFilter가 처리한다.
SecurityConfig에서 기능을 활성화 하지 않으면, RememberMeAuthenticationFilter
가 filter chain에 등록되지 않는다
- 또한, DefaultLoginPageGeneratingFilter에서도 체크박스를 만들지 않는다.
SecurityConfig Remember-me 활성화
@Configuration
@EnableWebSecurity
public class WebSecurityConfigure {
private final Logger log = LoggerFactory.getLogger(WebSecurityConfigure.class);
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
...생략
.rememberMe()
.rememberMeParameter(“remember”) // 기본 파라미터명은 remember-me
.tokenValiditySeconds(3600) // Default 는 14일
.alwaysRemember(true) // 리멤버 미 기능이 활성화되지 않아도 항상 실행
.userDetailsService(userDetailsService)
}
}
- rememberMe() : 리멤버 미 활성화, 설정 시작
- rememberMeParameter(파라미터 명) : RememberMe 쿠키의 이름을 지정한다. default Name은 remember-me 이다.
- RememberMe 쿠키 이름은 AbstractRememberMeServices클래스에 default로 정의 되어 있으며, 이 메소드로 파라미터 이름을 바꾸게 된다면 내부적으로 쿠키 이름이 바뀌게 된다
- tokenValiditySeconds(seconds) : 초 단위로 RememberMe 쿠키의 유효 기간을 정한다. default는 14일 (3600초) 이다
- alwaysRemember(boolean) : remember-me 매개변수가 설정되지 않은 경우에도 쿠키를 항상 생성해야 하는지 여부
- default는 false이다
- userDetailsService(userDetailsService 구현체) : Remember-Me 토큰이 유효한 경우 User를 조회하는데 사용되는 UserDetailsService를 지정한다. 설정이 없다면 기본으로 InMemoryUserDetailsManager를 사용한다.
- default : InMemoryUserDetailsManager. (UserDetailsService 빈이 필요.)
- TokenBasedRememberMeServices.processAutoLoginCookie() 에서 loadUserByUserName으로 사용자 정보를 가져오기 때문이다.
화면을 개발자가 임의로 개발한다면 여러 형태로 바뀌게 될 것인데, 이 때 Remember Me를 제대로 사용하기 위해서는 rememberMeParameter() 메소드의 파라미터와 HTML 요소의 name과 동일하게 맞추어 줘야한다.
- ex)
<input type="checkbox" name="remember">
Remember-Me의 쿠키의 Life Cycle
- 인증 성공 : 사용자가 로그인 기억 체크박스를 체크하고, 인증에 성공하면 Remeber-Me쿠키를 헤더에 설정 (Remember-Me쿠키 설정)
- 인증 실패 : 로그인에 실패하거나 쿠키가 time-out 된 경우(쿠키가 존재하면 쿠키 무효화)
- 즉, 로그인이 성공했어도 사용자가 임의로 로그인 페이지로 돌아간 후 인증에 실패하면, 있는 쿠키도 무효화 시킨다.
- 로그아웃 : 로그아웃시 (쿠키가 존재하면 쿠키 무효화)
- 만료시간이 지날 경우에도 무효화
Remember-Me 인증과정
먼저, RememberMeAuthenticationFilter에서 인증이 진행되는데 이 필터가 존재하려면 Remember-Me 기능이 활성화 되어야 한다.
- SecuritiyConfig 설정
RememberMeAuthenticationFilter는 default로 DefaultLoginPageGeneratingFilter나 UsernamePasswordAuthenticationFilter보다 뒤에 존재한다
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
this.logger.debug(LogMessage
.of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
chain.doFilter(request, response);
return;
}
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
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() + "'"));
this.securityContextRepository.saveContext(context, request, response);
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);
}
- RememberMeAuthenticationFilter의 doFilter()에 요청이 도착한다.
- 현재 인증된 객체가 없어야 인증을 시작한다
(SecurityContextHolder.getContext().getAuthentication() != null)
- 세션이 동작하는 경우, 세션 필터에 의해 JSESSIONID 쿠키가 동작하여 먼저 인증되어서 rememberme는 동작하지 않는다.
세션 기능을 끌라면 http.sessionManagement().disable()
를 사용
- 실제 사용자 인증은 RememberMeServices 인터페이스 구현체를 통해 처리된다.
TokenBasedRememberMeServices
와PersistentTokenBasedRememberMeServices
구현체가 있다TokenBasedRememberMeServices
는 메모리에 있는 토큰과 사용자가 request header에 담아서 보낸 토큰을 비교하여 인증을 한다. (기본적으로 14일만 토큰을 유지한다. http.tokenValiditySeconds(seconds)로 변경 가능 )PersistentTokenBasedRememberMeServices
는 DB에 저장된 토큰과 사용자가 request header에 담아서 보낸 토큰을 비교하여 인증을 한다. (영구적인 방법)
- RememberMeServices.authLogin(request, response)를 통해 인증과정이 진행된다.
- request로부터 rememberMeCookie를 가져온다.
extractRememberMeCookie(request)
- rememberMeCookie가 없는경우 그냥 다음 필터로 넘어간다.
- 쿠키의 길이가 0인 경우에도 다음 필터로 넘어가는데, 지정되어 있던 쿠키를 삭제한다
- 쿠키가 존재한다면 decode한다 (
decodeCookie(rememberMeCookie)
)- 일반적으로 Base64로 인코딩 되어있으며 다음과 같다
- 인코딩 된 값 : dXNlcjoxNjcxMzU3MDQyMjE3OmU4ZGQ3MDUwYjYyMjk1M2E1ZWE3OTcxYTljNTczNzQ1
- 디코딩 한 값 : user:1671357042217:e8dd7050b622953a5ea7971a9c573745
- userId, 만료시간, signatureValue
- 일반적으로 Base64로 인코딩 되어있으며 다음과 같다
- TokenBasedRememberMeService에게 인증을 위임하여 processAutoLoginCookie() 메소드를 통해 인증을 진행한다.
- 파싱하여 token 으로 쪼갠 길이는 무조건 3이여야 한다
- 토큰으로부터 토큰 만료시간을 가져온다
- UserDetailsService로부터 loadUserByUserName(username) 으로 유저 정보를 가져온다
- 시큐리티 Config에서 UserDetailsService를 설정하지 않으면 default UserDetailsService는 InemoryUserDetailsManager이다
- signatureValue를 검증한다.
- signatureValue는
username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
형태로 MD5 해시 알고리즘을 이용하여 만들어진다. - 즉, signatureValue의 형식은 username, 만료시간, 비밀번호, 키 값인데 이 값들이 다르면 signatureValue값도 달라지니까 이걸로 검증할 수 있는것이다
- signatureValue가 다르면 예외를 던져 RememberMe 인증을 중지한다
- signatureValue는
- 정상적으로 UserDetails 객체가 반환되면 createSuccessfulAuthentication()을 통해 Authentication 객체를 반환한다.
- RememberMeAuthenticationToken (Authentication 구현체)이다.
- Authentication 객체를 AuthenticationManager(ProviderManager,
RememberMeAuthenticationProvider
를 사용한다.) 에게 인증처리를 위임하고 SecurityContext에 저장하고 리턴한다.
참조
- 인프런 정수원님 스프링 시큐리티 강의
'Spring > Spring Security' 카테고리의 다른 글
SecurityContextPersistenceFilter - SecurityContext 영속화 필터, SecurityContextRepository (0) | 2022.12.18 |
---|---|
RememberMeAuthenticationFilter, RememberMeSevices (0) | 2022.12.18 |
BasicAuthenticationFilter (0) | 2022.12.18 |
AbstractAuthenticationProcessingFilter (a.k.a UsernamePasswordAuthenticationFilter) 와 인증과정 (0) | 2022.12.18 |
DefaultLogoutPageGeneratingFilter, LogOutFilter (0) | 2022.12.17 |