본문 바로가기
카테고리 없음

로그인 기억하기 - remember me

by 찐세 2021. 6. 13.

rememeber me

 

 

 

사용자에 대한 인증이 필요한 서비스는 로그인 기능이 필요하다.

로그인은 일반적으로 DB에 저장되어 있는 사용자의 정보와 로그인 시 입력한 값을 비교해서 일치하는 경우, 해당 사용자의 정보를 세션에 등록하고 해당 세션 ID를 담은 쿠키를 사용자에게 발급한다.

이 세션 ID를 통해서 이 사용자가 세션에 등록되어 있는(인증이 된) 사용자인지 판단을 한다.

 

 

하지만 세션이라는 것은 서버측의 자원이고, 이를 무한정으로 할당하는 것은 서버측의 불필요한 리소스의 낭비를 발생시킬 수 있다.

그렇기 때문에 각 사용자의 세션 공간은 주어진 시간 동안만 할당 된다. 톰캣의 경우 기본 세션 타임 아웃이 30분으로 설정이 되어 있다.

이 말은 결국 30분이 지나면 사용자의 세션이 만료되어 더이상 인증된 사용자로 식별이 될 수 없고, 새로 로그인을 해야한다는 것을 의미한다. 

각종 은행 사이트나 우리 학교의 학생지원시스템의 경우에도 30분이 지나면 로그인이 풀리게 되고, 연장하고 싶은 경우에는 별도의 버튼을 누르는 등 조치를 취해야한다.

이렇게 타임 아웃이 지나도 로그인을(인증된 상태) 유지하고 싶은 경우에는 별도의 조치를 취해야한다. 

 

 

 

어떤 원리로 로그인을 유지할 수 있는지 한번 알아보자.

로그인을 유지하는 원리는 간단하다.( 간단한거 맞나~? )

사용자가 인증된 사용자임을 식별하기 위해서(세션 ID를 가지고 있는) 사용하는 쿠키말고 별도의 쿠키를 하나 더 사용하는 것이다.

이 쿠키에 사용자의 인증에 필요한 정보를 담아서 세션이 만료가 되어도 이를 통해서 인증을 할 수 있게 하는 것이다.

이것이 로그인을 유지하는 기본적인 원리지만, 여기에는 문제점이 존재한다.

 

 

 

 

1. 해싱 기반의 쿠키 (문제점 존재)

가장 일반적인 방법은 로그인시 사용할 usernamepassword 정보를 해싱해서 쿠키에 담고 이를 가지고 인증을 하는 방식이다. 

하지만 이는 치명적인 단점이 존재하는데, 바로 쿠키가 탈취당하는 경우다.

해커가 쿠키를 탈취하게 되면 이 계정이 탈취당한 것과 같고, (그럴일은 드물지만) 탈취한 계정의 비밀번호 마저 바꿀 수 있는 상황이라면 문제가 심각해질 수 있다.

그렇기 때문에 단순히 인증 정보를 해싱해서 쿠키에 담아서 사용하는 것은 안전하지 못한 방법이다.

 

 

 

2. 문제점을 개선한 쿠키 방식 with username & random token ( 결론적으로 이것도 문제점이 존재!)

첫번째 쿠키 방식을 개선하기 위해서 다른 방식을 사용할 수 있다.

쿠키에 usernamerandom token 값이 저장이 되고, 이 정보들은 비교를 위해서 마찬가지로 DB에도 저장이 된다.

사용자가 요청을 보내면 쿠키의 username과 token의 값을 확인하고 DB에 저장되어 있는 값과 일치하는지 확인한다.

이때 정보가 일치하다면 사용자는 인증된 것으로 간주하고, 사용한 token은 DB에서 제거한다.

그리고 새로운 random token 값을 생성해서 DB에 저장하고 사용자의 쿠키에도 반영을 해서 돌려준다.

즉, 쿠키를 통해서 인증이 될 때마다 random token 값을 새로 갱신해준다는 것이다.

이 방법을 사용하면 사용자가 쿠키를 통해서 인증을 할 때, 해커가 이전에 탈취한 쿠키를 통해서는 더이상 인증이 불가능하기 때문에 이전의 문제점을 개선했다고 볼 수 있다.

 

하지만 이 방식에도 문제점이 존재한다. 만약 사용자가 해당 쿠키를 통해서 요청을 보내고 있는데, 해커가 이 정보를 탈취해서 사용자보다 먼저 요청을 한다면??

당연히 해커가 먼저 인증이 될 것이고, DB에는 새로 변경된 random token값이 갱신이 될 것이다.  그렇기 때문에 사용자는 쿠키를 통해서 인증을 할 수 없게 된다.

즉, 아직까지 완전하게 쿠키 탈취에 대해서 문제점이 해결되지 않았다고 볼 수 있다.

 

 

 

3. 더 개선된 방법 with username, random token, random fixed series

위의 문제점은 사용자보다 해커가 탈취한 쿠키를 사용해서 먼저 인증을 하는 경우였다. 이 경우에는 해커는 인증에 성공하고, 사용자는 변경된 random token 값에 의해서 인증에 실패하게 되는 것이다.

그래서 이를 해킹의 단서로 사용하는 것이다.

쿠키에 username, random token 과 더불어서 fixed random series( 랜덤하게 생성되지만 token 처럼 계속 갱신하지 않는 고정값의 series)를 사용한다.

2번 방식의 매커니즘에 series 값만 하나 더 존재한다고 생각하면 된다. 해커가 사용자보다 먼저 인증을 하게 되면, random token 값은 새로 변경이 되었을 것이고, series 값은 그대로 일 것이다.

그렇다면 사용자 쿠키안의 정보 상태는 일치하는 username, 다른 token 값, 일치하는 series값이 될 것이다. 

이 경우를 계정이 도난당했다고 간주한다. 강력한 경고를 발생시키고 서버 측에서는 해당 사용자와 관련된 모든 세션 정보를 삭제한다. 그렇기 때문에 사용자는 새로 로그인을 해야한다.

 

반대로 생각해볼 수도 있다. 만약 사용자가 먼저 인증하고 해커가 뒤늦게 인증을 했다면??

당연히 위와 마찬가지의 경우다. (해커 요청의 쿠키 정보 : 일치하는 username, 다른 token 값, 일치하는 series값)

그렇기 때문에 이때도 마찬가지로 해당 사용자와 관련된 세션 정보를 모두 삭제하고 사용자는 다시 로그인을 해야한다.

사용자는 다시 로그인을 할 수 있지만, 해커는 더이상 인증을 할 방법이 없다. (사용자의 인증만 자꾸 제거 될 뿐?)

 

이런 방식으로 쿠키의 인증 정보를 구성하면, 쿠키 탈취에 의한 계정 도난 문제를 해결할 수 있다. 

스프링 시큐리티에서는 3번째 방식인 rememberMe 쿠키를 통해서 로그인 유지 기능을 사용할 수 있다.

 

 

시큐리티 설정 - rememberMe

@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    private final AccountService accountService;
    private final DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.rememberMe().userDetailsService(accountService).tokenRepository(tokenRepository()); // remember-me 설정
    }

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

 

 

 

"create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)" 에 해당하는 테이블이 존재해야한다.  :  쿠키의 정보를 비교할 테이블에 해당.

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
    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 Entity 매핑

package com.jingeore.account;

import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.time.LocalDateTime;

@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;
}