Remember Me : 로그인 기억하기
http.rememberMe().rememberMeParameter("new-remember-me");
개요
페이지 이동간의 로그인을 유지하기 위해서 세션과 세션을 찾을 수 있는 쿠키가 사용된다. 하지만 쿠키가 만료되거나 삭제되면 로그인을 다시 해주어야하는 번거로움이 있다.
SpringSecurity에서는 Remember Me라는 기능을 제공하여 이를 해결한다. 이번 포스팅에서는 Spring에서 제공하는 Remember Me의 2가지 방식을 알아보려한다.
목차
- 로그인 쿠키
- 해시 기반의 쿠키 방식 : TokenBasedRememberMeServices
- PersistentTokenBasedRememberMeServices
로그인 쿠키
로그인 쿠키란, 세션 쿠키의 만료기간과 관계없이 로그인을 유지하기 위한 자격 증명 쿠키이다. 로그인 쿠키는 최초 로그인 시 서버로 부터 발급받게 되고, 직접 username과 password를 입력해서 로그인 할 때마다 로그인 쿠키를 발급받는다.
만약 내가 노트북과 데스크탑으로 하나의 서비스에 같은 계정으로 로그인한다면 로그인 쿠키는 각 컴퓨터에 하나씩 생성되어 두 개가 생긴다. 정상적인 상황에서라면 쿠키를 직접 입력하여 인증을 시도하지 않기 때문에, 하나의 로그인 쿠키로 로그인이 유지되는 클라이언트(브라우저)는 하나여야 한다.
정리하면 하나의 계정에 로그인 쿠키는 여러개가 있을 수 있다. 하지만 하나의 로그인 쿠키를 사용하여 로그인이 유지되는 브라우저는 하나여야 한다.
해시 기반의 쿠키 방식 : TokenBasedRememberMeServices
RememberMe를 구현하는 첫번째 방식은 간단한 해시 기반의 쿠키 방식이다. 진행 순서를 요약하면 다음과 같다.
- Remember Me를 체크한 상태에서 사용자가 로그인을 성공하면 표준 세션 관리 쿠키(ex. JSESSIONID)와 함께 로그인 쿠키가 발급된다.
- 로그인 쿠키에는 (username, password, 만료시간, key)가 인코딩되어 들어있다.
- key는 임의의 랜덤한 값이고, 애플리케이션마다 달라야 한다.
- 로그인하지 않은 사용자가 사이트를 방문하여 로그인 쿠키를 제공하면 서버에서 로그인 쿠키를 디코딩하여 사용자의 로그인 정보를 알아내 인증절차를 거친 후, 해당 세션 쿠키를 돌려준다.
Spring 공식 문서에 따르면 로그인 쿠키를 인코딩하는 방식은 다음과 같다고 한다.
단점
- 로그인 쿠키는 지정된 기간 동안만 유효하며 사용자 이름, 비밀번호 및 키가 변경되지 않는다면 로그인 쿠키의 값은 항상 같다. 따라서 이 값만 알아낸다면 몇명이든 자격 증명이 가능하다. 이것은 하나의 로그인 쿠키로 여러명이 인증되어 있는 비정상적인 상황을 만들 수 있다.
- 이 방식은 클라이언트가 토큰을 관리하기 때문에 탈취당하기 쉽고 탈취 당하면 토큰이 만료될 때 까지 해커가 계속 사용할 수 있다는 보안 문제가 있다.
- 시스템은 쿠키가 탈취당한 피해자가 있다는 사실을 알 수가 없다.
- 로그인 쿠키에 username과 password가 모두 들어있어서 위험성이 더 크다.
Spring boot 적용 방법
1. Remember Me 체크 박스를 만든다.
<form th:action="@{/login}" method="post">
// .. username, password, etc input
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="rememberMe" name="remember-me" checked/>
<label class="form-check-label" for="rememberMe" aria-describedby="rememberMeHelp">로그인 기억하기</label>
</div>
</form>
RememberMe의 파라미터 이름은 기본적으로 remember-me이며, Web Application Security를 설정할 때 remember-me의 remember-me-parameter 속성을 설정함으로써 변경할 수 있다. 스프링 부트에서는 다음과 같이 변경 가능하다.
http.rememberMe().rememberMeParameter("new-remember-me");
2. WebSecurityConfigurer 구현 클래스에 Remember Me 설정하기
@Override
protected void configure(HttpSecurity http) throws Exception {
// login 과 그 밖의 URI 접근 권한 설정...
// RememberMe
http.rememberMe()
.userDetailsService(userDetailsService())
.key("random private secret key");
}
쿠키에 포함될 username, password 정보를 얻고, RememberMe 토큰이 올바른 권한을 포함할 수 있도록 userDetailsService가 필요하다. UserDetailsService는 대부분 빈으로 직접 구현하게되는데, 이때는 userDetailsService() 대신에 그 빈을 넣으면 된다.
시리즈와 토큰을 추가한 방식 : PersistentTokenBasedRememberMeServices
토큰
앞서 말한 해시 기반의 방식의 문제점을 요약하면 다음과 같다.
- 나의 로그인 쿠키로 여러명이 인증상태로 로그인이 유지가 가능하다. (비정상)
- 해커가 로그인 쿠키를 잘 관리하면 계속해서 사용할 수 있다.
- 시스템이 쿠키 도난 피해자가 발생했다는 것을 판단할 수 없다.
여러명이 인증 가능한 것을 해결하기 위한 아이디어는 로그인 쿠키를 만들 때 랜덤한 값(이하 토큰)을 넣고 로그인 쿠키를 사용하여 인증할 때마다 로그인 쿠키를 새로 발급해주는 것이다. 그러면 로그인 쿠키는 매번 새로운 값을 갖게 되고 여러 사용자가 하나의 로그인 쿠키를 사용하여 로그인 유지 하는 것이 불가능 해진다. 인증을 시도해도 이미 새롭게 발급된 로그인 쿠키가 있기 때문에 유효하지 않기 때문이다.
하지만 이 방식도 해커에 의해 로그인 쿠키가 탈취된다면 오히려 피해자가 로그인 쿠키로 인증이 불가능해지고 해커는 인증이 가능해진다. 물론 사용자는 로그인 유지가 풀림으로써 로그인 쿠키가 해킹당했다는 것을 알 수도 있지만, 대부분의 사람은 무언가 착오가 있나보다라고 넘기고 새롭게 로그인하여 새로운 로그인 쿠키를 발급받을 것이다. 그렇게 되면 하나의 계정을 두 개의 로그인 쿠키로 공유하게되는 셈이다.
문제를 해결하기 위해서는 시스템이 로그인 쿠키가 도난당한 피해자가 있다는 사실을 판단할 수 있어야 한다. 해답은 유효하지 않은 로그인 쿠키를 사용하면 해당 쿠키가 해킹당했다는 증거로 처리하는 것이다. 그러기 위해서는 우선 DB에 토큰을 저장해야 한다.
우선 username과 token을 쌍으로 저장한다고 가정하여 시나리오를 짜보자.
- 피해자는 T_0 로그인 쿠키를 해킹당한다.
- 해커는 T_0로 인증을 하고 T_1을 새로 발급받는다.
- 피해자가 T_0로 인증을 시도하면 시스템은 T_0를 디코딩하여 username과 token을 DB에서 찾는다.
- T_0와 T_1은 username은 동일하지만 token이 다르기 때문에 인증이 실패한다.
- 시스템은 사용자에게 보안 경고를 표시하고 username으로 기억한 모든 로그인 세션을 자동으로 무효화할 수 있다.
이렇게 해서 해시 기반 방식의 문제점을 모두 해결할 수 있다. 하지만 한가지 문제점이 남아있다. 바로 시스템이 username을 기준으로 판별한다는 것인데, username은 쉽게 알 수 있거나 추측하기가 쉽기 때문에 해커가 일부러 username과 잘못된 토큰으로 가짜 로그인 쿠키를 만들어 제출하여 의도적으로 로그인 세션을 무효화 시킬 수 있다. 이렇게 모든 사용자에 대해 잘못된 로그인 쿠키를 제출하여 전체 시스템을 비활성화할 수 있다.
해결하기 위한 방법은 username이 아니라 추측하기 힘든 난수가 필요하다. 이 난수가 바로 시리즈이다.
시리즈
이제부터 본론이다. Spring Security에서 Remember Me를 제공하는 두번째 방식은 token, series, username으로 로그인 쿠키를 만들어 제공하고 세개의 플랫을 DB에 저장하는 방식이다. 진행 순서는 다음과 같이 진행된다.
- Remember Me를 체크한 상태에서 사용자가 성공적으로 로그인하면 표준 세션 관리 쿠키와 함께 로그인 쿠키가 발급된다.
- 로그인 쿠키에는 (username, series, token)으로 이루어져 있다.
- 시리즈와 토큰은 난수이고, 세 가지 모두 데이터베이스 테이블에 함께 저장된다.
- 시리즈는 username을 식별할 수 있는 식별자의 역할을 하고, 토큰은 인증할 때마다 새롭게 생성된다.
- 로그인하지 않은 사용자가 사이트를 방문하여 로그인 쿠키를 제공하면 username, series, token이 DB에서 조회된다.
- 만약 셋 모두 일치하는 데이터가 있는 경우 사용자는 인증 된 것으로 간주한다.
- 사용된 토큰은 제거되고 새 토큰으로 업데이트 된 후 세 가지를 모두 포함하여 인코딩한 새 로그인 쿠키가 사용자에게 발급된다.
- username과 series가 유효하지만 토큰이 일치하지 않으면 도난으로 간주한다. 시스템은 사용자에게 강력한 경고를 보낼 수 있으며 사용자의 기억된 모든 세션을 삭제할 수 있다.
- 해커가 로그인 쿠키를 탈취하여 인증을 한다면, 사용자는 유효한 시리즈, username과 유효하지 않은 토큰을 갖게 된다. 시리즈는 추측하기 힘든 난수이기 때문에 시스템은 해커가 무차별로 공격하는 것이 아니라 쿠키가 탈취당했다는 것을 확신할 수 있게된다.
- username과 series가 없으면 로그인 쿠키가 무시된다.
이로써 토큰만 사용할 때의 장점과 추가적으로 해커는 세션의 전체 수명 대신 피해자가 다음에 웹 사이트에 액세스할 때까지만 탈취한 쿠키를 사용할 수 있게된다. 또한 피해자는 웹 사이트에 액세스하면 도난이 발생했음을 경고로 확실히 알 수 있게된다.
Spring Boot 적용 방법
1. Remember Me 체크 박스를 만든다.
- 해시 기반 쿠키 방식과 동일
2. persistent_logins 테이블을 만든다.
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null)
3. WebSecurityConfigurer 구현 클래스에 Remember Me 설정하기
@Override
protected void configure(HttpSecurity http) throws Exception {
// login 과 그 밖의 URI 접근 권한 설정...
// RememberMe
http.rememberMe()
.userDetailsService(userDetailsService())
.tokenRepository(tokenRepository());
}
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
토큰을 저장하기 위해서는 PersistentTokenRepository를 추가로 구성해야 한다. 두 가지 표준 구현이 존재한다.
- InMemoryTokenRepositoryImpl : 메모리에 토큰을 저장하는 것으로 테스트용으로 사용되어야한다.
- JdbcTokenRepositoryImpl : DB의 persistent_logins 테이블을 대상으로 한 sql들이 들어있는 구현체이다. 이것을 통해 DB에 토큰을 저장하고 갱신하고 삭제한다.
Reference
- https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-rememberme
- https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice
- https://www.baeldung.com/spring-security-remember-me
'Spring' 카테고리의 다른 글
[ Spring ] enum의 원하는 상태 값을 DB에 저장하기 (0) | 2022.03.23 |
---|---|
[ Spring ] Request Param을 enum으로 받기 (0) | 2022.03.23 |
NPM으로 FrontEnd 라이브러리 관리하기 (feat. Springboot & Gradle) (0) | 2021.09.14 |
SMTPSendFailedException: 555 5.5.2 Syntax error (0) | 2021.09.05 |
[ Spring Boot ] 데이터베이스를 Session 저장소로 사용하기 (0) | 2021.03.18 |