본문 바로가기

Backend/Spring

[Spring/SpringBoot] Spring Security - Remember Me

안녕하세요. 이번 포스팅에서는 Spring의 Remember-me 즉 로그인 기억하기 기능에 대해 살펴보겠습니다.

 

Session

보통 일반적인 웹 서비스에서, Client가 로그인을 하게 되면 세션 Id가 서버로부터 부여됩니다. 이는 이 사용자에 대한 인증이며, 서버에서 따로 관리하며 이 인증정보로 서버의 리소스들을 접근할 수 있게 됩니다.

 

하지만 당연하게도 이 세션 Id는 취약합니다. Client가 지워버릴 수도 있고, Server가 꺼졌다가 재시동되면 당연히 Client들의 세션 Id를 보관하던 저장소도 사라지게 됩니다. 이렇게 되면 로그인 정보가 사라지게 됩니다.

 

Spring tomcat은 기본적으로 session timeout을 30분 정도로 가지고 있습니다. 이 말은 로그인을 해도 30분이 지나면 로그인이 사라지게 됩니다. 

 

Cookie

다른 방법으로는 쿠키를 사용하는 방법이 있습니다. 세션이 만료되었으면 이 쿠키 값을 사용하여 재 인증을 시도하고, 재 인증되었으면 새로운 세션 Id를 발급받는 방식입니다. 물론 이 쿠키에는 Username과 Password기반으로 Hashing 된 인증 정보가 있을 것입니다.

 

이 방법의 문제는 쿠키값이 해커에게 탈취당하면 거의 계정 자체를 해킹당한 것과 같은 의미입니다. 당연하게도 이 쿠키는 Username, Password기반으로 받은 값이고, 이 값을 통해 인증정보를 받아오는 용도로 사용하는데, 해커는 이 값으로 사용자의 인증정보를 가져올 수 있기 때문입니다. 

 

Username + Token + Series

오늘 소개해드리는 기능은 이 로그인 정보를 유지하기 위해 위의 문제점들을 개선한 Spring에서 제공하는 하나의 방법입니다. 

 

Token은 인증을 시도할 때마다 랜덤 값으로 변경되고, Series는 변경되지 않습니다.

 

항상 인증을 시도할 때에 Username + Token + Series로 인증을 시도합니다. 이 중 Username과 Series는 고정된 값입니다.

 

해커가 Cookie를 탈취하여 인증을 시도하면, 처음에는 인증이 됩니다. 하지만 Token값이 변경되게 됩니다. 그렇게 되면 그 이후부터는 해커는 인증을 할 수 없게 됩니다.

 

물론 해커가 처음 인증을 시도하면, 유저도 인증을 할 수 없습니다. 이미 해커가 인증을 시도했기 때문에 Token값이 변경되었기 때문입니다.

 

이 경우 Spring에서 Token을 삭제하여, 해커 유저 모두 사용할 수 없게 만들어 버립니다. 이렇게 되면 유저는 로그인 창으로 이동하여 다시 로그인을 해서 새로운 Token을 정상적으로 받을 수 있습니다.

 

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final AccountService accountService;
    private final DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/", "/sign-up", "/login").permitAll()
                .mvcMatchers(HttpMethod.GET, "/profile/*").permitAll()
                .anyRequest().authenticated();

        http.formLogin()
                .loginPage("/login").permitAll();
        http.logout()
                .logoutSuccessUrl("/");
        http.rememberMe()
                .userDetailsService(accountService)
                .tokenRepository(tokenRepository());
    }

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

 

구현을 하기 위해 Security에 http.rememberMe() 메소드를 활용합니다. 

userDetailsService는 위 코드의 경우 accountService에 구현을 해 놓았기 때문에 accountService를 전달했습니다.

그리고 앞서 말했던 세 가지 정보로 만든 token을 db에 저장하는 tokenRepository 만들어서 넣어줍니다.

 

tokenRepository는 JdbcTokenRepositoryImpl이라는 class로 만들어 줍니다. datasource를 주입받아 전달해 주시면 됩니다.

 

그리고 JdbcTokenRepository가 사용하기 위한 table을 선언해 주셔야 합니다.

public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
			+ "token varchar(64) not null, last_used timestamp not null)";

 

보통 Jpa를 사용하시며 자동으로 엔티티가 table에 매핑되는 전략을 사용하시는 프로젝트가 많은데, 이 경우 별도로 JdbcTokenRepository가 사용하는 table에 상응하는 Entity를 생성하셔서, Spring 실행 시 자동으로 table이 생성되게 만들겠습니다.

 

@Table(name = "persistent_logins")
@Entity
@Getter @Setter
public class PersistentLogins {

    @Id @Column(length = 64)
    private String series;

    @Column(nullable = false, length = 64)
    private String username;

    @Column(nullable = false, length = 64)
    private String token;

    @Column(name = "last_used", nullable = false, length = 64)
    private LocalDateTime lastUsed;
}

 

혹시 모르니 명시적으로 table명과 column명을 명시해주시는 것이 좋습니다.

 

정상적으로 구현이 되었다면, 

 

이 SessionId를 삭제하고 새로고침 했을 때에 

 

 

이런 식으로 로그인 정보가 유지되셨다면, 원하는 기능 구현이 완료되었습니다.

 

*저의 글에 대한 피드백이나 지적은 언제나 환영합니다.