로그인 기억하기

Yuri Lee·2020년 11월 11일
0

세션이 만료 되더라도 로그인을 유지하고 싶을 때 사용하는 방법

  • 쿠키에 인증 정보를 남겨두고 세션이 만료 됐을 때에는 쿠키에 남아있는 정보로 인증한다.

예를 들어 우리가 어떤 어플리케이션에 로그인을 한다. 그러면 jsessionid가 생긴다. 이것은 웹브라우저와 서버의 연결고리이다. 이 아이디에 해당하는 서버 쪽에 세션이라는 메모리가 있다. 그것과 이 id가 연결고리이다. 서버쪽에 있는 세션의 키 값이라고 생각하면 된다.

그런데 이것에 해당하는 서버쪽에 객체가 없거나 또는 클라이언트가 키값을 보내지 않거나 그럼 현재 이 요청과 관련있는 세션을 찾지 못한다. 지금 이 요청에 대한 세션을 찾지 못할 때 해당되는 쿠키가 있으면 쿠키에 들어가 있는 인증 정보로 시도한다.

이 세션은 무한대로 들고 있지 않는다. 스프링 부트는 기본 타임 아웃은 30분으로 들고 있게 되있다. 물론 이 값을 변경할 수도 있다. 매우 큰 값을 주면 현재 이 서버를 사용하고 있지 않은 클라이언트에 대한 세션 정보까지 너무 오래 보유하고 있기 때문에 메모리 낭비가 발생할 수 있다. (실제 서버를 사용하는 사용자를 위해 할당해야 할 메모리가 부족해지는 문제 유발 가능성)따라서 이 값을 지나치게 늘리는 것은 권장하지 않는다.

그럼 어떻게 해야 로그인이 안풀릴까? remember me를 사용하는 것이다.

remember me

즉 쿠키를 하나 더 쓰는 것이다. 인증을 했을 때 jsessionid(세션 id) 말고도 쿠키에다가 인증정보를 담아서 넣어두는 것이다. 암호화를 하고 해싱을 한 다음에 username, password 등을 담아놓는 것이다. 이를 세션이 만료되거나 없을 때 사용한다.

지금 이 요청에 해당하는 세션을 찾지 못할 때 같이 보내온 rememberme 쿠키가 있으면 그 쿠기에 들어있는 인증 정보로 인증을 시도하는 것이다. 그 쿠키에 username, password 가 들어있으므로 가능하다. 이로 인증이 되면 다시 또 새로운 jsessionid(세션 id)와 쿠키가 발급된다. 이런 식으로 동작하는 게 remember me쿠키이다. 하지만 이 방법에 치명적인 단점이 있다. 다른 사람이 이 remember me 쿠키값을 가져가면 계정을 뺏긴 것과 같다. 따라서 로그아웃을 꼭 해야 한다. 그래야 쿠키가 없어진다.

해시 기반의 쿠키

  • Username
  • Password
  • 만료 기간
  • Key (애플리케이션 마다 다른 값을 줘야 한다.)
  • 치명적인 단점, 쿠키를 다른 사람이 가져가면... 그 계정은 탈취당한 것과 같다.

조금 더 안전한 방법은?

  • 쿠키안에 랜덤한 문자열(토큰)을 만들어 같이 저장하고 매번 인증할 때마다 바꾼다.
  • Username, 토큰
  • 문제는, 이 방법도 취약하다. 쿠키를 탈취 당하면, 해커가 쿠키로 인증을 할 수 있고, 희생자는 키로 인증하지 못한다.

조금 더 개선한 방법

  • Username, 토큰(랜덤, 매번 바뀜), 시리즈(랜덤, 고정) (3가지 정보)
  • 쿠키를 탈취 당한 경우, 희생자는 유효하지 않은 토큰과 유효한 시리즈와 Username으로
    접속하게 된다.
  • 경우, 모든 토큰을 삭제하여 해커가 더이상 탈취한 쿠키를 사용하지 못하도록 방지할 수
    있다.

유저도 희생자도 쿠키로 로그인을 시도한다. 희생자가 갖고 있던 쿠키는 유효하지 않다. Username과 시리즈는 일치하지만 해커가 이미 토큰값을 바꿔버렸기 때문에 db에 저장되어있던 토큰값과 유저가 들고 있는 토큰값과 매칭이 안된다. 즉 유효하지 않은 토크와 유효한 시리즈와 Username으로 속하게 된다. 이 경우는 오로지 쿠키가 탈취 당해서 누군가에 의해 사용되었다는 뜻이다.

code

SecurityConfig.java

        http.rememberMe()
                .key("sdafsefasdafds")

키값만 설정한 경우, 해싱 알고리즘을 적용한 경우이다. 이 방법은 제일 안전하지 않다. 해커가 이 쿠키를 가져가면 비밀번호를 바꿔버릴 수 있다.

        http.rememberMe()
                .userDetailsService(accountService)
                .tokenRepository(tokenRepository());

따라서 tokenRepository 를 활용하는 방법을 사용한다. tokenRepository 를 사용할 때는 userDetailsService 도 같이 설정을 해줘야 한다. userDetailsService 는 AccountService에 구현해놓은 상태이다. 가져와서 주입해주면 된다.

Username, 토큰(랜덤, 매번 바뀜), 시리즈(랜덤, 고정) (3가지 정보) 를 조합해서 만든 토큰 정보가 필요하다. 사용자 보낸 쿠키(remember쿠키값)와 일치하는지 확인해야 하므로 db에 저장해야 한다. db에서 토큰값을 읽어와 저장하는 인터페이스에 객체, 구현체를 주입해줘야 한다.

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

tokenRepository의 구현체인 JdbcTokenRepositoryImpl, 너무 구체적인 타입이니까 인터페이스 타입으로 지정해주고 이 안에서 구체적인 타입을 사용하자. Jdbc 기반의 토큰 Repository 의 구현체이다. Jdbc 기반으로 당연히 DataSource 를 필요로 한다. 우리는 JPA 를 사용하고 있으므로 DataSource는 당연히 등록되어있다. 가져다가 사용하면 된다.

JdbcTokenRepositoryImpl 가 사용하는 table이 있다.

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)";
    public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
    public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
    public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
    public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
    private String tokensBySeriesSql = "select username,series,token,last_used from persistent_logins where series = ?";
    private String insertTokenSql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
    private String updateTokenSql = "update persistent_logins set token = ?, last_used = ? where series = ?";
    private String removeUserTokensSql = "delete from persistent_logins where username = ?";
    private boolean createTableOnStartup;

사용하고 있는 db에 persistent_logins에 해당하는 스키마가 있어야 한다. 하지만 현재 jpa를 사용하고 있고, 아무런 기본 설정없이 인메모리 db를 사용하고 있을 때는 엔티티 정보를 보고 table을 알아서 만들어 준다. 따라서 스키마가 생성될 수 있도록 맵핑이 되는 엔티티를 추가한다.

package com.goodmoim.domain;

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") //@Table 어노테이션은 맵핑할 테이블을 지정
@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;

}

맵핑이 되는 엔티티 클래스를 작성해주면 persistent_logins이 생성되게 된다.

EditThisCookie : chorme extension

EditThisCookie는 쿠키 관리자이다. 이것을 이용하여 쿠키를 추가하고, 삭제하고, 편집하고, 찾고, 보호하거나 막을 수 있다.

remember-me 쿠키값은 username + 토큰 + 시리즈의 조합

회원가입 후 로그인.
쿠키가 있기 때문에 세션이 만료되었다고 생각하고 세션 아이디를 삭제해보자. 로그인이 풀리지 않는다. 오히려 인증을 했기 때문에 세션id가 새로 생긴다. remember-me 쿠기값도 변경이 된다.

가입후 로그인 했을 때의 remember-me 값: V09ZcWM0Uk1lNmFyM2p2THRpWFBMZyUzRCUzRDo4Z05jN3dGRm9nSVhjSXBXWHpWJTJGMGclM0QlM0Q

jsessionID값을 삭제 했을 때의 remember-me 값(세션 강제로 지우고 reload 할 경우):
V09ZcWM0Uk1lNmFyM2p2THRpWFBMZyUzRCUzRDp2WXIlMkI4SnBFWTZ2WGVWOFMlMkJqTmklMkZBJTNEJTNE

값이 바뀐 것을 확인할 수 있다. 랜덤한 값이기 때문!


출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발
https://chrome.google.com/webstore/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg?hl=ko

profile
Step by step goes a long way ✨

0개의 댓글