자동 로그인 기능 구현 with remember-me

Shinny·2022년 8월 16일
0

자동 로그인 기능을 구현하기 위해 filter 와 interceptor 를 활용해서 자동 로그인 체크를 한 사용자와 그렇지 않은 사용자를 걸러내려고 했습니다. 하지만 이 과정 자체를 일일이 로직으로 쳐내는 것이 복잡했고, 이미 Spring Security 를 사용하고 있었기 때문에 Spring Security 의 기능 중 하나인 remember me를 사용하기로 결정했습니다.

아래는 제가 Remember Me 기능을 구현한 방식을 정리해둔 것입니다.

전체코드

.rememberMe(rememberMe -> rememberMe
                                .key("rememberMeKey")
                                .tokenRepository(persistentTokenRepository())
                                .rememberMeServices(rememberMeServices(persistentTokenRepository()))
                                .userDetailsService(new UserDetailsServiceImpl(memberRepository)))

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
    repo.setDataSource(dataSource);
    return repo;
}

@Bean
public PersistentTokenBasedRememberMeServices rememberMeServices(PersistentTokenRepository tokenRepository) {
    PersistentTokenBasedRememberMeServices rememberMeServices = new
            PersistentTokenBasedRememberMeServices("rememberMeKey", new UserDetailsServiceImpl(memberRepository), tokenRepository);
    rememberMeServices.setParameter("remember-me");
    rememberMeServices.setAlwaysRemember(true);
    return rememberMeServices;
}

@Bean
public PasswordEncoder passwordEncoder() {
    String idForEncode = "bcrypt";
    Map encoders = new HashMap<>();
    encoders.put(idForEncode, new BCryptPasswordEncoder());
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}

🔑 Remember Me Key 값

저는 임의로 Remember Me의 key 값을 rememberMeKey 로 대충 지었지만, 사실 이 값은 조금 더 신경써서 지어야 할 것 같긴 합니다… 😂 왜냐하면 애플리케이션 내에서 토큰이 생성될 때마다 항상 사용되는 private 한 value이기 때문입니다.

👛 Token Repository 가 필요한 이유는?

RememberMeServices 를 구현하는 방식은 2개가 있는데, TokenBasedRememberMeServices 와 PersistentTokenBasedRememberMeServices 입니다.

TokenBasedRememberMeServices 은 토큰을 브라우저에 저장하는 반면 PersistentTokenBasedRememberMeServices 은 서버에 토큰을 저장해서 이용합니다. 또한 전자의 방식은 유저네임, 쿠키만료시간, 비밀번호의 값이 토큰에 포함되어 있기 때문에, 토큰 탈취시 해당 정보들이 노출되는 취약점을 안고 있습니다. 그에 비해 후자의 방식은 series 값과 token 값을 따로 만들어서 저장하기 때문에 유저네임과 만료시간이 노출되지 않습니다. 또한 다른 브라우저에 로그인 할 때마다 series 값이 바뀌는데, 최근 로그인 한 계정의 series 값을 기준으로 token 값을 검사하기 때문에 보안상 좀 더 안전하다고 할 수 있습니다.

그래서 결과적으로 저는 후자의 방식 즉 PersistentTokenBasedRememberMeServices 을 통해 remember me 기능을 구현하였습니다.

그렇기 때문에 이 Token Repository가 필요했고, 이 Token을 저장할 테이블을 먼저 생성해주었습니다. 그리고 이 테이블의 이름은 고정이 되어있기 때문에, persistentLogins 라는 이름의 Entity를 만들어주었습니다. 그리고 아래는 해당 Table이 생성될 때 나가는 SQL DDL문입니다.

create table if not exists persistent_logins ( 
  username varchar_ignorecase(100) not null, 
  series varchar(64) primary key, 
  token varchar(64) not null, 
  last_used timestamp not null 
);

💡 JdbcTokenRepositoryImpl 과 dataSource의 연결

그런 다음, JdbcTokenRepositoryImpl 객체를 만들어서 MySQL 데이터베이스와 연결해주었습니다. 이미 application.properties 파일에 dataSource에 관한 정보를 기입해두었기 때문에 스프링은 이미 해당 dataSource를 Bean으로 등록한 상태입니다. 그래서 SecurityConfiguration 클래스에 dataSource 의존성을 주입해주었고, 해당 객체에 기존에 연결되어있는 dataSource 를 연결해 준 것입니다.

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
    repo.setDataSource(dataSource);
    return repo;
}

그 후 위에서 언급한대로 PersistentTokenBasedRememberMeServices 를 생성하여 빈으로 만들어 주었는데, 이렇게 랜덤하게 생성된 series 와 token 값의 조합은 브루트 포스(brute force) 공격에 굉장히 강하다고 합니다.

@Bean
public PersistentTokenBasedRememberMeServices rememberMeServices(PersistentTokenRepository tokenRepository) {
    PersistentTokenBasedRememberMeServices rememberMeServices = new
            PersistentTokenBasedRememberMeServices("rememberMeKey", new UserDetailsServiceImpl(memberRepository), tokenRepository);
    rememberMeServices.setParameter("remember-me");
    rememberMeServices.setAlwaysRemember(true);
    return rememberMeServices;
}

또한 Spring Security 5.0부터 사용하는 Password Encoder 형식에 맞춰 Custom DelegatingPasswordEncoder 를 생성해주었습니다. DelegatingPasswordEncoder Storage Format 은 다음과 같은데, 회원가입 시 사용자가 비밀번호를 입력하면 아래와 같은 방식으로 저장되게 됩니다.

password 형식 : {bcrypt}2a$10bVcGLHvjjOGC8qs7Bfr7bupA4NFyZPv8n3h2tgokZsTpe3gdm7XQ

@Bean
public PasswordEncoder passwordEncoder() {
    String idForEncode = "bcrypt";
    Map encoders = new HashMap<>();
    encoders.put(idForEncode, new BCryptPasswordEncoder());
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}

위 과정을 다 잘 마친 후에, 이 passwordEncoder 방식을 정의할 때 DelegatingPasswordEncoder 저장 포맷에 맞게 해야하는 것을 몰라서 한참을 헤매었습니다. 결과적으로는 위와 같이 설정하여 remember-me cookie 생성을 잘 완료할 수 있었습니다!

Reference

https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html
https://www.baeldung.com/spring-security-remember-me
https://www.baeldung.com/spring-security-persistent-remember-me

profile
비즈니스 성장을 함께 고민하는 개발자가 되고 싶습니다.

0개의 댓글