스프링과 JPA 기반 웹 애플리케이션 개발 #25 로그인 기억하기 (RememberMe)

Jake Seo·2021년 5월 31일
0

스프링과 JPA 기반 웹 애플리케이션 개발 #25 로그인 기억하기 (RememberMe)

해당 내용은 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발의 강의 내용을 바탕으로 작성된 내용입니다.

강의를 학습하며 요약한 내용을 출처를 표기하고 블로깅 또는 문서로 공개하는 것을 허용합니다 라는 원칙 하에 요약 내용을 공개합니다. 출처는 위에 언급되어있듯, 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발입니다.

제가 학습한 소스코드는 https://github.com/n00nietzsche/jakestudy_webapp 에 지속적으로 업로드 됩니다. 매 커밋 메세지에 강의의 어디 부분까지 진행됐는지 기록해놓겠습니다.


로그인 기억의 이론적 배경

서버의 세션

  • 브라우저에는 세션 스토리지(Session storage)라는 Key, Value 저장소가 있음
    • 이 저장소는 클라이언트가 얼마든지 접근해서 변경, 훼손 등이 가능하다.
    • 이건 서버의 세션과는 다른 것이다.
  • 서버에는 세션(Session) 이라는 저장소가 있음
    • 이 저장소는 클라이언트가 독단적으로 접근할 수 없다.
    • 서버사이드에 있는 만큼 서버에서 지원하는 언어가 제공하는 다양한 자료구조로 데이터를 저장해놓을 수도 있다.

로그인에서 세션이 쓰이는 이유는 HTTP가 Stateless 프로토콜인 것과 관련이 깊은데, Stateless 프로토콜은 상태를 기억하지 않는다. 그래서 한번 로그인이 됐다고 해서 모든 페이지에서 내가 로그인한 사실을 알지는 못한다.

이러한 방법의 해결책으로 서버에서는 인식할 수 있는 Session Key를 클라이언트 사이드(브라우저)에 저장해놓고, 해당 Session Key를 쿠키 등에 저장해 매번 서버로 같이 보내서 모든 페이지에서 내가 로그인한 유저임을 증명한다.

참고 블로그1 - 클라이언트 사이드 저장소와 서버 사이드 저장소
참고 블로그2 - Session Management - Client Side vs Server Side
참고 블로그3 - 웹 서버의 Session 전략

세션 정보의 유효기간

세션 정보는 보통 특정한 시간을 기점으로 초기화됨. 초기화하지 않으면, 현재는 사이트를 이용하고 있지 않은 유저들의 세션까지 보유하고 있어서 메모리의 낭비가 발생함.

위와 같이 세션 보유 기간 설정이 가능한데, 기본값은 30m(30분)이다.

그렇다면 세션을 유지하는 방법은?

  • 세션을 초기화하지 않는 방법은 권장되지 않는다.
  • 쿠키에 세션을 생성하기 위한 키를 저장한다.
    • ex) RememberMe라는 이름의 쿠키를 저장한다.
    • 서버는 세션이 만료되었을 때 RememberMe 쿠키의 값을 이용하여 다시 세션을 생성한다.

로그인 기억하기 (RememberMe)

세션이 만료되더라도 로그인을 유지하고 싶다면?

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

해시 기반의 쿠키

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

조금 더 안전한 방법은?

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

조금 더 개선한 방법

  • https://www.programering.com/a/MDO0MzMwATA.html
  • Username, 토큰(랜덤, 매번 바뀜), 시리즈(랜덤, 고정)
  • 쿠키를 탈취 당한 경우
    • 해커는 유효한 Username, 토큰, 시리즈를 이용하여 로그인한다.
    • 희생자는 유효하지 않은 토큰과 유효한 시리즈와 Username으로 접속하게 된다.
    • 이 경우, 토큰만 유효하지 않으므로 시스템은 해커가 해당 유저의 인증정보를 해킹했다고 가정하고, 해당 유저를 위해 저장된 모든 토큰을 삭제하여 해커가 더이상 탈취한 쿠키를 사용하지 못하도록 방지할 수 있다.

스프링 시큐리티 설정: 해시 기반 설정

http.rememberMe().key("랜덤한 키 값");

스프링 시큐리티 설정: 보다 안전한 영속화 기반 설정

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

SecurityConfig 클래스 수정하기

configure(HttpSecurity http) 메소드 수정

@Override
protected void configure(HttpSecurity http) throws Exception {
...

http.rememberMe()
                // .key("asdfasdf"); // 이 방법이 해시 기반의 쿠키 인증이다.
    .userDetailsService(accountService)
    .tokenRepository(tokenRepository());
}

http.rememberMe() 메소드를 수행함으로써, 위에 설명한 조금 더 개선한 방법으로 로그인 기억을 할 수 있다. 메소드 체인으로 userDetailsService()tokenRepository()를 불러주고, 올바른 파라미터를 주어야 한다. userDetailsServiceuserDetailsService를 상속하여 구현했던 클래스의 빈을 넣어주면 되고, tokenRepository에는 PersistentTokenRepository 타입을 반환하는 빈을 넣어주면 된다.

tokenRepository() 메소드 생성

    @Bean
    public PersistentTokenRepository tokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // JPA에서 DataSource는 자동으로 빈으로 등록된 항목이기 때문에 가져다 쓰기만 하면 된다.
        // JdbcTokenRepositoryImpl 의 소스를 보면 내부적으로 인증을 위한 DB 테이블을 사용하는 것을 알 수 있다.
        // 인메모리 디비를 쓸 때는 엔티티 정보를 보고 테이블을 알아서 만들어준다.
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

빈으로 생성하여, 컨테이너에 띄운다. 이 빈은 위에 언급된 메소드인 configure() 메소드 내부에서 http.rememberMe().tokenRepository()에 주입된다.

PersistentTokenRepository 인터페이스의 구현체를 JdbcTokenRepositoryImpl 클래스를 이용하여 생성하는데 세팅해줘야 하는 것은 딱 하나 .setDataSource()밖에 없다. dataSource의 경우, JPA를 사용하면 설정파일에 입력된 접속정보를 이용하여 컨테이너에 항상 띄워져있기 때문에, 클래스 레벨에서 의존성 주입으로 갖다 쓰면 된다.

여기서 주의할 점은 JdbcTokenRepositoryImpl 클래스는 내부적으로 DB 테이블을 이용하는데, 이 테이블은 우리가 수동으로 만들어주어야 한다. 우리는 JPA를 사용하기 때문에 이 테이블을 직접 만들지 않고, JPA 객체로 정의하면 된다.

위와 같이 쓰는 쿼리들이 정의되어 있는데, 우리가 볼 곳은 CREATE_TABLE_SQL이다.

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)";

위와 같은 형식으로 만들어주면 된다.

PersistentLogins 클래스 생성

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

위에서 본 CREATE_TABLE_SQL의 내용과 정확히 동일하게 만들어주면 된다. 그러면 스프링 시큐리티에서 이 테이블을 잘 갖다 쓸 것이다.

여기 공식문서에 자세한 내용이 나와있다.

login.html 수정

...
      <!-- 이 체크박스로 remember-me 파라미터에 true 값이 들어와야 remember-me 쿠키 값을 만들어준다. -->
      <div class="form-group form-check">
        <input id="rememberMe" type="checkbox" name="remember-me" class="form-check-input" checked/>
        <label class="form-check-label" for="rememberMe" aria-describedby="rememberMeHelp">로그인 유지</label>
      </div>
...

위와 같이, 체크 박스를 하나 만들어서 remember-me라는 파라미터에 true값을 보내주어야 스프링 시큐리티의 rememberMe가 정상적으로 동작한다.

설정이 끝나면?

위와 같이 모든 설정이 끝나고, 로그인 유지 상태로 로그인을 하면

쿠키를 봤을 때, JSESSIONIDremember-me 항목이 생긴다.

이제 JSESSIONID의 쿠키 값을 아무리 삭제해도 새로 생성된다.
또한, JSESSIONID의 쿠키 값이 새로 생성되면서 remember-me의 쿠키 값도 변한다.

그런데 여기서 만약 해커에게 remember-me 토큰이 탈취된 상태에서 기존 이용자가 잘못된 remember-me 토큰으로 로그인하는 순간 모든 토큰은 삭제되어 더이상 인증에 이용이 불가능해진다. 그러면 처음부터 다시 로그인을 해야 한다.

테스트 결과 JSESSIONID는 만료되지 않고, remember-me 쿠키만 만료된다.

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글