Remember-Me

Remember-Me

주로 자동로그인 등에 사용되는 Remember-Me 기능에 대해 알아봅시다.


Dependencies


이 글의 예제 코드를 적용하기 위해서는 다음과 같은 의존성을 빌드 스크립트에 추가해야만 합니다.

빌드툴은 Gradle을 기준으로 작성하며, Spring Boot 2.5.5 기반입니다.

웹 브라우저는 크롬을 사용하였습니다.


// file: 'build.gradle'
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    
    ...

    runtimeOnly 'com.h2database:h2'
}


✅ Remember-Me


Java Servlet 기반의 WAS를 사용 할 경우 인증 요청 대한 인증 성공 시 발급되는 쿠키의 기본 이름은 JSESSIONID 입니다.

이것은 Java Servlet 표준 스펙에 정의된 부분이기 때문에 Java Servlet을 구현하는 모든 WAS가 동일합니다.

Spring MVC를 사용 할 경우 기본적으로 톰캣을 사용하기 때문에 위의 내용이 적용됩니다.

따라서 Spring Security 적용 후 발급되는 인증 쿠키의 이름은 JSESSIONID가 됩니다.


이 이름은 물론 개발자가 커스터마이징할 수 있습니다.


Remember-Me 기능은 사용자에게 매우 큰 편리성을 제공하는 기능입니다.

사용자가 서버에 인증한 순간 웹 브라우저에 remember-me 쿠키를 저장해 놓음으로써 이후 사용자의 세션이 만료되거나, 불특정한 사유로 JSESSIONID가 없어졌을 경우에도 해당 사용자를 기억할 수 있게 됩니다.


image


즉, Spring Security Filter Chain에서 유효한 remember-me 쿠키를 인식하면 해당 사용자가 현재 인증되지 않은 사용자라고 할지라도 즉시 자동으로 재 인증을 시켜주므로, 사용자는 이 기능을 통해 로그인 시 쿠키가 유효한 시점까지 아이디/패스워드 등의 정보를 다시 입력하지 않아도 되게 됩니다.

단, 사용자에게 편의성을 제공하면 할수록 보안 수준은 반비례하여 점점 떨어질 수 밖에 없기 때문에, 이 부분은 항상 주의해야 합니다.


스프링 시큐리티는 Remember-Me 기능에 대해 기본적으로 두 가지 구현체를 제공합니다.

  • 암호화 시그니처를 사용한 토큰 기반Remember-Me, 구현체의 이름은 TokenBasedRememberMeServices
  • 데이터베이스를 사용한 영구 토큰 기반의 Remember-Me, 구현체의 이름은 PersistentTokenBasedRememberMeServices

그리고 위의 두 구현체는 모두 UserDetailsService를 의존하기 때문에 Remember-Me 기능을 구현하기 위해서는 UserDetailsService를 반드시 설정해주어야만 합니다.


암호화 시그니처를 사용한 토큰 기반


기본적인 적용은 아주 심플합니다.


// file: 'SecurityConfiguration.java'

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("test").password("{noop}test").authorities("ROLE_USER"); // 개발 편의성을 위한 인메모리 유저를 설정합니다.
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
            .authorizeRequests() // 서버로 오는 요청들에 대한 보안 정책
            .anyRequest().authenticated() // 모든 요청에 대해 인증이 필요합니다.

            .and()

            .rememberMe().key("key") // remember-me 토큰 암호화에 사용할 키를 설정합니다. 기본값은 무작위로 설정된 문자열입니다.
            .userDetailsService(userDetailsService()) // Remember-Me 기능 설정에 필요한 필수 옵션

            .and()

            .formLogin()
        ;
    }

}


이후 프로젝트를 실행하고 /login 으로 접근하면 다음과 같은 웹 페이지가 뜹니다.

저는 localhost:8080/login으로 접근했습니다.


기억하기미사용


여기에 위에서 설정한 인메모리 유저정보를 입력하고 로그인을 하면 성공적으로 로그인이 되며, 웹 브라우저의 개발자 도구를 열어 쿠키 정보를 보면 아래와 같은 정보가 뜰 것입니다.

이 때, Value는 이미지와 다를 수 있으며, 쿠키 이름이 JSESSIONID이기만 하면 됩니다.

위 정보는 서버에서 로그인 요청(=인증 요청)을 성공적으로 받아 처리하였으며, 이에 대한 인증 쿠키를 웹 브라우저에 내려주었음을 의미합니다.


기억하기미사용2


이제 Remember-Me 기능을 사용할 것임을 의미하는 아래의 체크박스에 체크를 하고, 동일하게 로그인을 시도해봅니다.

아래의 과정을 진행하기 전 /logout으로 접근하여 서버에 로그아웃 요청을 보내 세션을 지워주어야 합니다.

저는 localhost:8080/logout으로 접근하였습니다.


기억하기사용


기억하기사용2


역시 성공적으로 로그인이 되며 아까와 다르게 remember-me 라는 이름의 새로운 쿠키가 하나 더 생겨있음을 알 수 있습니다.

여기서 한가지 짚고 넘어갈 것이 있는데, 로그인 페이지의 HTML을 살펴보면,


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
  </head>
  <body>
     <div class="container">
      <form class="form-signin" method="post" action="/login">
        <h2 class="form-signin-heading">Please sign in</h2>
        <p>
          <label for="username" class="sr-only">Username</label>
          <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
        </p>
        <p>
          <label for="password" class="sr-only">Password</label>
          <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
        </p>
        <p>
          <input type='checkbox' name='remember-me'/> Remember me on this computer.
        </p>
          <input name="_csrf" type="hidden" value="facf4bbf-4c91-455f-8ee2-ac777f8e901c" />
          <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
      </form>
     </div>
</body>
</html>


체크박스 관련 HTML에 nameremember-me로 돼있음을 볼 수 있습니다.

만약 서버에서 Remember-Me 파라미터 이름을 변경한다면, HTML도 함께 변경되어야만 합니다.

스프링 시큐리티의 기본 설정 파라미터명은 remember-me이며, 이 이름은 기본 쿠키 이름과도 동일합니다.


public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> {

    /**
     * The default name for remember me parameter name and remember me cookie name
     */
    private static final String DEFAULT_REMEMBER_ME_NAME = "remember-me";
  
    ...
}


이제 웹 브라우저 개발자 모드에서 JSESSIONID 쿠키를 강제로 제거한다면 웹 브라우저에서는 인증 쿠키가 사라지는 것이므로, 서버는 당연히 이후 요청에 대해 인증 요청을 강제할 것입니다.

하지만 웹 브라우저에는 아직 remember-me 쿠키가 남아있으므로, 추가적인 로그인과정 없이도 다시 로그인이 될 것임을 예상해볼 수 있습니다.


제거전


JSESSIONID 쿠키를 클릭하여 키보드의 delete키를 입력하면 쿠키가 강제로 제거됩니다.


제거후


그리고 다시 서버에 임의의 요청을 보낸다면, 자동적으로 로그인처리가 될 것이므로 JSESSIONID 쿠키가 다시 생겨야만 합니다.

크롬의 강력 새로고침(SHIFT + CTRL + R)기능을 이용하여 서버에 임의의 요청을 보내면…


image


다시 인증 처리가 되어 새로운 쿠키가 생성돼있음을 확인할 수 있습니다.

이전에 한 설정으로 인해 서버로 오는 모든 요청은 인증이 필요하므로 원래라면 인증 쿠키가 없어 로그인 페이지로 리다이렉트 됐어야 했으나, remember-me 쿠키로 인해 그러한 과정 없이 재 인증이 된 것입니다.


토큰 기반 Remember-Me 기능의 원리


remember-me는 인증된 사용자에 대한 여러 정보를 담아 MD5 방식으로 암호화한 토큰입니다.

최초 인증시 인증에 성공할 경우 해당 사용자의 여러 정보를 모아 설정한 키(Key)를 사용해 암호화 하고 해당 값을 사용자에게 돌려주는 것이죠.

사용자가 받는 쿠키는 아래의 정보들을 담고 있습니다.


image


사용자는 인증 성공 시 인증 성공에 대한 JSESSIONID라는 쿠키와, 자동로그인을 위한 remember-me 라는 이름의 쿠키를 받는 것입니다. (2개)

또한, remember-me 쿠키가 갖고있는 토큰값은 만료날짜, 사용자의 정보등을 추가적으로 집어넣어 암호화함으로써 레인보우 테이블 공격에 대비합니다.

레인보우 테이블

해커들이 수백만개 이상의 임의의 데이터를 단방향 암호화하여 기록한 테이블이 레인보우 테이블입니다. 보통 사용자의 민감정보를 안전하게 보관하기 위해 데이터를 단방향으로 암호화 하는데, 암호화를 하였더라도 해당 정보가 탈취된다면, 이후 레인보우 테이블에서 해당 정보를 검색해보고 일치하는 값이 있을 경우 보안 공격에 노출되게 됩니다.


이후 임의의 사용자로부터 remember-me 쿠키를 이용한 요청이 들어 올 경우, 기본적으로 쿠키는 신뢰할 수 없는 데이터이기 때문에 검증에 들어갑니다.


image


유효한 시그니처를 만들어내기 위해서는 4가지의 정보가 필요합니다.

  • 사용자명
  • 패스워드
  • 만료일


위의 네가지 정보가 모두 올바른 값이라면, 유효한 시그니처가 생성되게 되고, 이 시그니처를 토대로 체크섬하여 유효성을 검증합니다.


웹 브라우저에서 보내는 remember-me 쿠키에는 세가지의 정보가 들어있습니다.

  • 사용자명
  • 만료일
  • 시그니처


웹 브라우저에서 보내오는 쿠키에서 시그니처를 만드는데 필요한 사용자명, 만료일을 얻을 수 있습니다.

그리고 서버는 시그니처를 만들 때 사용한 키(Key)를 이미 알고있죠.

따라서 네가지의 정보 중 패스워드만 더 얻으면 시그니처를 만들어낼 수 있으며, 사용자명을 토대로 데이터베이스를 조회하여 패스워드를 얻어올 수 있습니다.

이렇게 remember-me 쿠키로 인증 요청이 들어오면 위의 과정을 거쳐 유효한 시그니처를 만들어내며, 생성된 유효한 시그니처와 remember-me 쿠키에 들어있는 시그니처를 비교합니다.

만약 두 시그니처가 같다면, 해당 쿠키는 신뢰할 수 있는 쿠키이기 때문에 인증 처리 합니다.


속성


// file: 'SecurityConfiguration.java'
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
            .rememberMe().key("key") // remember-me 토큰 암호화에 사용할 키를 설정합니다. 기본값은 무작위로 설정된 문자열입니다.
            .userDetailsService(userDetailsService()) // Remember-Me 기능 설정에 필요한 필수 옵션입니다.

//            .rememberMeParameter("remember-me") // 클라이언트 뷰에서 설정한 파라미터명과 동일해야 합니다. 기본값은 remember-me
//            .tokenValiditySeconds(86400) // 토큰 유효기간을 설정합니다. 기본값은 14일이며 단위는 초입니다. -1로 설정 할 경우 브라우저가 종료되면 함께 사라집니다.
//            .alwaysRemember(true) // Remember-Me 기능이 활성화 되지 않아도(체크박스에 체크하지 않아도, 혹은 체크박스가 아예 없더라도) 항상 적용하도록 합니다.
//            .rememberMeCookieName("remember-me") // Remember-Me 응답 쿠키이름입니다.. 기본값은 remember-me

            .and()

            .formLogin()
        ;
    }

}


데이터베이스를 사용한 영구 토큰 기반



image


이 방식은 세션-쿠키 방식과 유사하며, 이 방식을 사용하기 위해서는 먼저 두가지 속성을 이해해야 합니다.

  • 시리즈(Series): 사용자가 처음 로그인할 때 생성되는 랜덤한 고유 값이며 불변합니다. 즉, 이후 사용자가 Remember-Me 기능을 이용해 인증을 시도할 때마다 항상 동일한 값을 가집니다. 데이터베이스의 PK(Primary Key)가 됩니다.

  • 토큰(Token): 사용자가 Remember-Me 기능을 이용해 인증을 시도할 때마다 계속해서 변경되는 고유 값입니다.


처음 인증 시 인증 토큰을 데이터베이스에 저장해두고, 이후 사용자가 보내오는 쿠키와 데이터베이스의 쿠키를 비교합니다.

따라서 remember-me 쿠키가 탈취당했는지의 여부를 알 수 있고, 만약 쿠키가 탈취당했다면 해당 토큰을 강제로 정지시켜버리거나, 사용자에게 쿠키가 탈취당했음을 경고하는 등의 작업도 할 수 있게 됩니다.


구현


데이터베이스H2를 사용할것이며, 데이터베이스 접속 방식JPA(Hibernate)를 이용할 것입니다.

먼저, 스프링 시큐리티 팀에서 제공하는 DDL을 적용해야 하는데, 이 포스팅에서는 JPA를 이용 할 것이므로 해당 DDL을 참고하여 엔티티를 설계 및 구현합니다.


create table persistent_logins
(
    username  varchar(64) not null,
    series    varchar(64) primary key,
    token     varchar(64) not null,
    last_used timestamp   not null
)


이를 엔티티로 구현하면 대략 다음과 같습니다. 그리고 나중에 사용하게 될 몇가지 메서드를 함께 추가하였습니다.


@Entity
@Table(name = "persistent_logins")
public class PersistentLogin implements Serializable {

    @Id
    private String series;

    private String username;

    private String token;

    private Date lastUsed;

    // JPA의 한계로 기본생성자가 반드시 필요하지만 private으로는 설정할 수 없다.
    protected PersistentLogin() {
    }

    // 생성자를 외부에 노출하지 않습니다.
    private PersistentLogin(final PersistentRememberMeToken token) {
        this.series = token.getSeries();
        this.username = token.getUsername();
        this.token = token.getTokenValue();
        this.lastUsed = token.getDate();
    }

    // 정적 팩토리 메서드
    public static PersistentLogin from(final PersistentRememberMeToken token) {
        return new PersistentLogin(token);
    }

    public String getSeries() {
        return series;
    }

    public String getUsername() {
        return username;
    }

    public String getToken() {
        return token;
    }

    public Date getLastUsed() {
        return lastUsed;
    }

    public void updateToken(final String tokenValue, final Date lastUsed) {
        this.token = tokenValue;
        this.lastUsed = lastUsed;
    }

}


이후 위 엔티티를 데이터베이스와 결합하게 도와줄 JPA Repository를 작성합니다.


public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {

    Optional<PersistentLogin> findBySeries(final String series);

    List<PersistentLogin> findByUsername(final String username);

}


그리고 위에서 작성한 구현체들을 스프링 시큐리티에서 제공하는 PersistentTokenRepository로 확장해줘야 합니다.

이렇게 하는 이유는, SecurityConfiguration에서 Remember-Me 기능을 데이터베이스 기반 토큰 방식으로 구현 할 경우 데이터베이스에 접근 할 때 사용할 Repository 구현체를 요구하게 되는데, 기본적으로 스프링 시큐리티에서 JPA를 이용한 구현체가 제공되지 않기 때문에 이 구현체를 직접 구현해 확장하는 것입니다.


// PersistentTokenRepository에서 요구하는 네가지 메서드를 재정의(Override)하도록 합니다.
public class JpaPersistentTokenRepository implements PersistentTokenRepository {

    // 스프링 팀에서 권장하는 생성자 DI를 이용합니다
    private final PersistentLoginRepository repository;

    public JpaPersistentTokenRepository(final PersistentLoginRepository repository) {
        this.repository = repository;
    }

    // 새로운 remember-me 쿠키를 발급할 때 담을 토큰을 생성하기 위한 메서드입니다.
    @Override
    public void createNewToken(final PersistentRememberMeToken token) {
        repository.save(PersistentLogin.from(token));
    }

    // 토큰을 변경할때 호출될 메서드입니다.
    @Override
    public void updateToken(final String series, final String tokenValue, final Date lastUsed) {
        repository.findBySeries(series)
            .ifPresent(persistentLogin -> {
                persistentLogin.updateToken(tokenValue, lastUsed);
                repository.save(persistentLogin);
            });
    }

    // 사용자에게서 remember-me 쿠키를 이용한 인증 요청이 들어올 경우 호출될 메서드입니다.
    // 사용자가 보내온 쿠키에 담긴 시리즈로 데이터베이스를 검색해 토큰을 찾습니다.
    @Override
    public PersistentRememberMeToken getTokenForSeries(final String seriesId) {
        return repository.findBySeries(seriesId)
            .map(persistentLogin ->
                new PersistentRememberMeToken(
                    persistentLogin.getUsername(),
                    persistentLogin.getSeries(),
                    persistentLogin.getToken(),
                    persistentLogin.getLastUsed()
                ))
            .orElseThrow(IllegalArgumentException::new);
    }

    // 세션이 종료될 경우 데이터베이스에서 영구 토큰을 제거합니다.
    @Override
    public void removeUserTokens(final String username) {
        repository.deleteAllInBatch(repository.findByUsername(username));
    }

}


그리고 위의 커스텀 구현체들을 SecurityConfiguration에 추가해줍니다.

테스트를 위해 몇가지 설정을 더 추가하였으나, 이 포스팅의 상단에서 설정한 것과 크게 달라진 것은 없을 것입니다.


@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private PersistentTokenRepository tokenRepository;


    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("test").password("{noop}test").authorities("ROLE_USER");
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
            .csrf().disable() // 테스트 편의성을 위해 CSRF 비활성화
            .headers().frameOptions().sameOrigin() // H2-Console에 접속해 영구 토큰을 확인하기 위해 설정

            .and()

            .authorizeRequests() // 서버로 오는 요청들에 대한 보안 정책
            .antMatchers("/h2-console/**").permitAll() // H2-Console에 접속해 영구 토큰을 확인하기 위해 설정
            .anyRequest().authenticated() // 모든 요청에 대해 인증이 필요합니다.

            .and()

            .rememberMe().key("key") // remember-me 토큰 암호화에 사용할 키를 설정합니다. 기본값은 무작위로 설정된 문자열입니다.
            .userDetailsService(userDetailsService()) // Remember-Me 기능 설정에 필요한 필수 옵션
            .tokenRepository(tokenRepository)

//            .rememberMeParameter("remember-me") // 클라이언트 뷰에서 설정한 파라미터명과 동일해야 한다. 기본값은 remember-me
//            .tokenValiditySeconds(86400) // 토큰 유효기간을 설정한다. 기본값은 14일이며 초단위이다. -1로 설정 할 경우 브라우저가 종료되면 함께 사라진다.
//            .alwaysRemember(true) // Remember-Me 기능이 활성화 되지 않아도(체크박스에 체크하지 않아도, 혹은 체크박스가 아예 없더라도) 항상 적용하도록 한다.
//            .rememberMeCookieName("remember-me") // Remember-Me 응답 쿠키이름. 기본값은 remember-me

            .and()

            .formLogin()
        ;
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository(final PersistentLoginRepository repository) {
        return new JpaPersistentTokenRepository(repository);
    }

}


그리고 간단한 테스트를 위해 프로젝트 설정을 조금 추가해줍니다.


# file: 'resources/application.yaml'
spring:
  h2:
    console:
      enabled: true
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
    hibernate:
      ddl-auto: create


이후 서버를 기동하면 서버 콘솔에 로그가 쭉 뜨는데, 그중 다음과 같은 로그를 찾습니다.


2021-10-12 11:37:38.744  INFO 10844 --- [  restartedMain] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:b1152f26-a567-4c05-9bf4-4f4260366b44'


스프링 설정에 H2 콘솔을 사용할 것이라고 설정했기 때문에 스프링에서 인메모리 데이터베이스 콘솔에 접속할 수 있는 수단을 제공해줍니다.

서버가 기동된 후 http://localhost:8080/h2-console/ 로 접속하면 다음과 같은 화면에 들어갈 수 있는데, 위에서 찾은 JDBC URL을 입력합니다.


image


이후 접속하면 다음과 같은 화면이 뜹니다.


image


  1. 화면 좌측 메뉴의 PERSISTENT_LOGINS 를 클릭하면 우측 콘솔에 SELECT 쿼리가 생성됩니다.
  2. 바로 위의 RUN을 누르면 생성된 쿼리가 실행됩니다.
  3. 화면 하단에 쿼리의 결과가 노출됩니다.


현재는 로그인을 단 한번도 하지 않았으므로 데이터베이스에 토큰이 없는것이 당연합니다.

localhost:8080/login으로 접속해 아이디와 비밀번호(test/test)를 입력하고, Remember-Me 기능을 사용할 것임을 체크하고 로그인한 뒤 다시 H2 콘솔을 확인하도록 합니다.


image


우선 로그인 후 역시 remember-me 쿠키가 잘 응답된 것을 확인할 수 있습니다.


image


H2 콘솔에 접속 후 동일한 쿼리를 실행하면 위와 같이 새로운 영구토큰이 데이터베이스에 저장됐음도 확인할 수 있습니다.


이후 로그아웃이 될 경우 세션 종료를 의미하기 때문에, 클라이언트에 설정된 remember-me 쿠키데이터베이스에 저장된 영구 토큰이 모두 제거되어야만 합니다.

localhost:8080/logout으로 접속하여 로그아웃한 후 다시 한번 더 확인해봅니다.


image


image


모두 성공적으로 제거된 것을 확인할 수 있습니다.


확장


여기까지는 remember-me 쿠키영속성 레이어(Persistent Layer)에 저장하고 관리하는 기본적인 방법들에 대해 알아봤습니다.


만약 쿠키에 대한 추가적인 제어가 필요하다면 스프링 시큐리티에서 제공하는 PersistentTokenBasedRememberMeServices를 통해 다음과 같이 간단하게 몇가지 제어를 더 추가할 수 있으며, 더욱 복잡한 구성이 필요 할 경우 PersistentTokenBasedRememberMeServices를 확장하면 되겠습니다.


// file: 'SecurityConfiguration.java'
@Bean
public PersistentTokenBasedRememberMeServices rememberMeServices(final PersistentTokenRepository repository) {
  PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices("key", userDetailsService(), repository);

  services.setAlwaysRemember(true);
  services.setParameter("remember-me-param-name");

  return services;
}


위 방식의 경우 만료된 토큰들에 대한 상세한 제어가 없습니다.

따라서 만료된 토큰들을 주기적으로 데이터베이스에서 제거하는 코드를 추가로 작성합니다.


// Runnable을 구현하여 별도의 스레드로 동작시키도록 합니다.
public class ExpiredTokenJpaRepositoryCleaner implements Runnable {

    private final PersistentLoginRepository repository;
    private final long tokenValidityInMs;

    private ExpiredTokenJpaRepositoryCleaner(final PersistentLoginRepository repository, final long tokenValidityInMs) {
        if (isNull(repository)) {
            throw new IllegalArgumentException("PersistentTokenRepository cannot be null.");
        }

        if (tokenValidityInMs < 1) {
            throw new IllegalArgumentException("tokenValidityInMs must be greater than 0. Got " + tokenValidityInMs);
        }

        this.repository = repository;
        this.tokenValidityInMs = tokenValidityInMs;
    }

    public static ExpiredTokenJpaRepositoryCleaner of(final PersistentLoginRepository repository, final long tokenValidityInMs) {
        return new ExpiredTokenJpaRepositoryCleaner(repository, tokenValidityInMs);
    }

    @Override
    public void run() {
        final long expiredInMs = System.currentTimeMillis() - tokenValidityInMs;
        repository.deleteAllInBatch(repository.findByLastUsedAfter(new Date(expiredInMs)));
    }

}


이제 위의 설정을 스프링에서 제공하는 스케쥴러를 이용해 주기적으로 실행시키도록 하면 됩니다.


@Configuration
@EnableScheduling
public class ScheduleConfigurer {

    private final PersistentLoginRepository persistentLoginRepository;

    public ScheduleConfigurer(final PersistentLoginRepository persistentLoginRepository) {
        this.persistentLoginRepository = persistentLoginRepository;
    }

    @Scheduled(fixedDelay = 10_000) // 단위는 ms. 따라서 1,000=1초. 10초에 한번 실행됨을 의미함.
    public void cleanExpiredTokens() {
        // 토큰의 유효시간이 10초
        new Thread(ExpiredTokenJpaRepositoryCleaner.of(persistentLoginRepository, 10_000L))
            .start();
    }

}


이후 서버를 기동하고 로그인을 하면 remember-me 쿠키가 발급되고, 토큰의 유효기간을 10초로 설정하였으므로 약 10초 후에 다음과 같은 쿼리가 발생합니다.


Hibernate: 
    select
        persistent0_.series as series1_0_,
        persistent0_.last_used as last_use2_0_,
        persistent0_.token as token3_0_,
        persistent0_.username as username4_0_ 
    from
        persistent_logins persistent0_ 
    where
        persistent0_.last_used>?
Hibernate: 
    delete 
    from
        persistent_logins 
    where
        series=?


참고


  • Spring Security - Third Edition: Secure your web applications, RESTful services, and microservice architectures (ISBN 9781787129511)

© 2022. All rights reserved.