요즘에는 대부분 시큐리티 + JWT 조합을 사용하지만 아직도 많이들 세션을 이용하고 있다.
JWT보다 세션이 나은 점은 뭘까??
- 서버에서 세션 데이터를 관리하기에 JWT보다 안전하다.
- 데이터 만료가 JWT보다 수월하다.
크게 보면 위 2가지라고 생각한다.
우선 JWT는 서버에서 생성만 하고 관리는 클라이언트에게 넘겨버린다. 이 때문에 AccessToken, RefreshToken이 탈취당하는 경우가 발생하게 되고, 서버에서는 탈취 자체를 막을 방법이 존재하지 않게 된다.
또 JWT는 생성 시간을 초기에 설정하고 토큰을 생성하기에 만료가 될 때까지 서버에서 토큰 삭제가 불가능하다.
아래 사진을 보면 하나의 클라이언트에게 3번의 로그인 요청이 들어왔고 3개의 JWT가 생성되었다.
이 3개의 토큰들은 만료시간이 되기 전까지는 전부 사용이 가능하다는 말이 된다.
(서버에서는 블랙리스트 기능을 통해 발급된 JWT를 사용 불가능하도록 만들 수도 있는데 결국 서버에서 정보를 기억해야 한다. 그러면 세션이랑 뭐가 달라(?))
그래서 세션을 이용하기도 하는데 세션 중복 로그인을 방지하고 싶을 때가 있을 수 있다.
이때 스프링 시큐리티를 이용해서 동일 세션의 로그인을 방지하는 방법을 알아보자.
SecurityConfig
.sessionManagement(sessionManagement -> sessionManagement
.sessionFixation().changeSessionId()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/Login.do"))
sessionFixation() -> 세션 고정 공격을 방어하기 위한 정책 설정(4개의 정책이 존재)
- newSession() -> 로그인 시 새로운 세션을 생성. 기존 세션 속성 유지X
- migrateSession() -> 로그인 시 새로운 세션을 생성. 새로운 세션에 속성 값을 이주시킨다.
- changeSessionId() -> 세션의 ID만 변경하고 세션의 속성은 그대로 유지한다.
- none() -> 세션 고정 보호 비활성화
maximumSession('숫자') -> '숫자'만큼의 동일 유저 세션을 생성할 수 있다.
maxSessionsPreventsLogin(true or false) -> 세션 최대 개수를 초과했을 경우의 정책이다.
- true -> 초과하게 되면 현재 시도를 차단한다.
- false -> 초과하게 되면 이전 세션을 만료시킨다.
위 코드를 요약하면 동일 계정에 대해 1개의 세션만 가능하고 이미 세션이 생성되어 있다면 추가 로그인은 불가능하게 된다.
이 상황에서 중복 로그인을 시도한다고 가정해 보자.
1. 중복 로그인 시도
2. 시큐리티 필터 체인에서 AbstractAuthenticationProcessingFilter의 doFilter메서드를 실행하여 this.sessionStrategy의 onAuthentication을 실행하게 된다.
this.sessionStrategy는 CompositeSessionAuthenticationStrategy을 주입받은 상태이다.
3. 이후 CompositeSessionAuthenticationStrategy의 onAuthentication을 수행해 ConcurrentSessionControlAuthenticationStrategy를 호출해서 아래 코드를 수행한다.
getAllSessions(인자 1, 인자 2) 메서드를 수행하는데 이때 2가지 인자를 넘기게 되어있다.
인자 1 : 유저의 정보 or 객체
인자 2 : 만료 여부
authentication.getPricipal()을 통해 인증받은 객체를 넘기고 false로 만료되지 않은 세션을 찾게 된다.
-> 즉, 만료되지 않은 모든 세션을 찾아서 List로 결과를 받게 된다.
아래는 기본적으로 구현된 getAllSessions()이다.
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
Set<String> sessionsUsedByPrincipal = (Set)this.principals.get(principal);
if (sessionsUsedByPrincipal == null) {
return Collections.emptyList();
} else {
List<SessionInformation> list = new ArrayList(sessionsUsedByPrincipal.size());
Iterator var5 = sessionsUsedByPrincipal.iterator();
while(true) {
SessionInformation sessionInformation;
do {
do {
if (!var5.hasNext()) {
return list;
}
String sessionId = (String)var5.next();
sessionInformation = this.getSessionInformation(sessionId);
} while(sessionInformation == null);
} while(!includeExpiredSessions && sessionInformation.isExpired());
list.add(sessionInformation);
}
}
}
여기서 주목할 부분은 첫 번째 줄인 아래 로직이다.
Set<String> sessionsUsedByPrincipal = (Set)this.principals.get(principal);
이때 principals에서 principal과 동일한 객체를 꺼내서 Set에 저장하는 것을 볼 수 있다.(중복 제거)
그런데 principals라는 것은 아래와 같이 ConcurrentHashMap으로 초기화를 진행한다.
public SessionRegistryImpl() {
this.principals = new ConcurrentHashMap();
this.sessionIds = new ConcurrentHashMap();
}
결국은 Hash를 사용하는 구현체이기 때문에 인자로 넘겨주는 principal과 동일한 객체를 찾으려고 시도할 때 hashCode(), equals() 메서드를 사용하게 된다.
이러한 이유 때문에 principal 클래스에 hashCode(), equals() 메서드 오버라이드를 진행해주어야 한다.
UserDetails를 구현한 CustomUserDetails에서 equals()랑 hashCode() 오버라이드를 통해 해시 구현체를 사용했을 때 정확하게 동일성 판단이 가능하도록 만들어야 한다.
@Getter
public class CustomUserDetails implements UserDetails {
@Override
public boolean equals(Object obj) {
if(obj instanceof CustomUserDetails){
return getId().equals(((CustomUserDetails) obj).getId());
}
return false;
}
@Override
public int hashCode() {
return getId().hashCode();
}
이렇게 유저 객체에 대한 동일성을 판단해서 현재 활성화된 세션의 최대 개수를 판단하고 이후 중복 로그인을 막을 수 있게 되는 것이다.
SessionRegistry (Spring Security 3.1.7.RELEASE API)
getAllSessions List getAllSessions(Object principal, boolean includeExpiredSessions) Obtains all the known sessions for the specified principal. Sessions that have been destroyed are not returned. Sessions that have expired may be returned, depending o
docs.spring.io
'CS지식' 카테고리의 다른 글
자바에서 가시성을 보장하는 volatile (0) | 2024.08.24 |
---|---|
자바 8과 11에서 String 연산을 처리하는 방법 (0) | 2024.08.10 |
CSRF 공격을 막기 위한 CSRF 토큰 (0) | 2024.07.31 |
spring-oauth-client 라이브러리의 동작 흐름 정리 (0) | 2024.06.27 |