spring-session-jdbc 을 사용하여 사용자의 중복 로그인을 방지하되
이메일 계정 하나에 웹 1개, 앱 1개씩만 로그인이 가능하도록 하고 싶었다.
어떻게 해야 될까?
public class PrincipalDetails implements UserDetails, OAuth2User {}
SecurityContextHolder 에 인증 객체가 저장되어 있어야 세션에 사용자 정보까지 DB에 저장되므로 앱은 UserDetails , 웹은 OAuth2User를 상속받아 구현하였다.
DB에 저장될 때는 위 객체가 SESSION_ATTRIBUTES 테이블에 직렬화되어 저장된다.
하고 있는 프로젝트에서는 소셜로그인으로만 인증하기 때문에
앱인지 웹인지 어디서 로그인을 했는지 쉽게 구분할 수 있었다.
그렇다면 PrincipalDetails 클래스 필드에 어느 플랫폼으로 로그인했는지 구분하는 값을 넣어준다면 쉽게 해결할 수 있을 거라고 생각했다.
공식 문서에 아주 친절하게 예제 코드까지 나와있다.
객체를 역.직렬화 할 수 있도록 Serializable 를 상속받은 클래스를 만들면 Session에 추가로 데이터를 넣을 수 있다.
그래서 세션에 추가로 데이터를 넣을 클래스를 만들어주고
public class PrincipalDetails extends SessionDetails implements UserDetails, OAuth2User {}
이를 상속받으면 DB에 추가한 데이터까지 직렬화되어 저장된다.
이제 앱 1개, 웹 1개만 사용할 수 있도록 중복로그인을 막아보자.
- FindByIndexNameSessionRepository: SessionRepository에 지정된 인덱스 이름과 인덱스 값으로 세션을 찾을 수 있도록 하는 인터페이스
- SessionRepository: 세션을 관리하기 위한 저장소 인터페이스
우선 spring-session-jdbc 에서 제공하는 FindByIndexNameSessionRepository와 SessionRepository를 사용해야 한다.
그리고 처리하는 단계를 설명하자면
- FindByIndexNameSessionRepository 를 통해 사용자 이메일에 해당하는 세션을 모두 가져온다.
- 세션에서 사용자 인증 객체를 추출한다.
- 인증 객체에서 어느 플랫폼(앱 or 웹) 에서 로그인했는지 추출한다.
- 현재 로그인한 플랫폼의 경로는 알고 있으므로 인증 객체에서 추출한 플랫폼과 현재 플랫폼이 같다면 중복 로그인이므로 SessionRepository 를 사용해서 이전 세션을 삭제한다.
다음과 같이 처리할 수 있게 된다.
findByIndexNameSessionRepository.findByPrincipalName(email) 를 통해 사용자의 이메일에 해당하는 세션을 모두 가져오고 세션을 순회하면서 인증 객체를 추출하고 같은 플랫폼 경로인지 비교하여 같다면 세션을 삭제하는 식으로 구현하였다.
extractPrincipalDetails(session) 은 인증 객체를 추출하는 것으로 세션 속성에 "SPRING_SECURITY_CONTEXT" 로 객체를 추출할 수 있다.
인증 객체 추출에 대한 설명은 위 이미지를 보면 쉽게 파악할 수 있을 것이다.
그리고 이전 글에서 설명했지만 익명 객체는 세션 속성이 "SPRING_SECURITY_SAVED_REQUEST" 이므로 참고하길 바란다.
https://docs.spring.io/spring-session/reference/guides/boot-findbyusername.html