예를 들어 우리가 어떤 어플리케이션에 로그인을 한다. 그러면 jsessionid가 생긴다. 이것은 웹브라우저와 서버의 연결고리이다. 이 아이디에 해당하는 서버 쪽에 세션이라는 메모리가 있다. 그것과 이 id가 연결고리이다. 서버쪽에 있는 세션의 키 값이라고 생각하면 된다.
그런데 이것에 해당하는 서버쪽에 객체가 없거나 또는 클라이언트가 키값을 보내지 않거나 그럼 현재 이 요청과 관련있는 세션을 찾지 못한다. 지금 이 요청에 대한 세션을 찾지 못할 때 해당되는 쿠키가 있으면 쿠키에 들어가 있는 인증 정보로 시도한다.
이 세션은 무한대로 들고 있지 않는다. 스프링 부트는 기본 타임 아웃은 30분으로 들고 있게 되있다. 물론 이 값을 변경할 수도 있다. 매우 큰 값을 주면 현재 이 서버를 사용하고 있지 않은 클라이언트에 대한 세션 정보까지 너무 오래 보유하고 있기 때문에 메모리 낭비가 발생할 수 있다. (실제 서버를 사용하는 사용자를 위해 할당해야 할 메모리가 부족해지는 문제 유발 가능성)따라서 이 값을 지나치게 늘리는 것은 권장하지 않는다.
그럼 어떻게 해야 로그인이 안풀릴까? remember me를 사용하는 것이다.
즉 쿠키를 하나 더 쓰는 것이다. 인증을 했을 때 jsessionid(세션 id) 말고도 쿠키에다가 인증정보를 담아서 넣어두는 것이다. 암호화를 하고 해싱을 한 다음에 username, password 등을 담아놓는 것이다. 이를 세션이 만료되거나 없을 때 사용한다.
지금 이 요청에 해당하는 세션을 찾지 못할 때 같이 보내온 rememberme 쿠키가 있으면 그 쿠기에 들어있는 인증 정보로 인증을 시도하는 것이다. 그 쿠키에 username, password 가 들어있으므로 가능하다. 이로 인증이 되면 다시 또 새로운 jsessionid(세션 id)와 쿠키가 발급된다. 이런 식으로 동작하는 게 remember me쿠키이다. 하지만 이 방법에 치명적인 단점이 있다. 다른 사람이 이 remember me 쿠키값을 가져가면 계정을 뺏긴 것과 같다. 따라서 로그아웃을 꼭 해야 한다. 그래야 쿠키가 없어진다.
유저도 희생자도 쿠키로 로그인을 시도한다. 희생자가 갖고 있던 쿠키는 유효하지 않다. Username과 시리즈는 일치하지만 해커가 이미 토큰값을 바꿔버렸기 때문에 db에 저장되어있던 토큰값과 유저가 들고 있는 토큰값과 매칭이 안된다. 즉 유효하지 않은 토크와 유효한 시리즈와 Username으로 속하게 된다. 이 경우는 오로지 쿠키가 탈취 당해서 누군가에 의해 사용되었다는 뜻이다.
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는 쿠키 관리자이다. 이것을 이용하여 쿠키를 추가하고, 삭제하고, 편집하고, 찾고, 보호하거나 막을 수 있다.
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