세션 관리 고도화 시리즈 (2/4)
왜 권한 변경 시 세션을 무효화해야 할까?
사용자의 권한을 변경했더라도 기존 세션이 살아 있으면 변경 전 권한으로 계속 요청이 가능하다.
예시:
- ADMIN이 user1을 CHANNEL_MANAGER로 강등
- user1은 이미 로그인 중 → 세션에는 아직 이전 권한 정보가 남아 있음
- user1은 강등됐음에도 ADMIN 권한 API를 계속 사용 가능
이를 막으려면 권한 변경 시 해당 사용자의 세션을 강제로 만료시켜야 한다.
SessionRegistry로 세션 무효화하는 방법
List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false);
sessions.forEach(SessionInformation::expireNow);
sessionRegistry.getAllSessions(principal, false): 해당 사용자의 모든 활성 세션 조회- 첫 번째 인자 :
principal—DiscodeitUserDetails객체 - 두 번째 인자 :
includeExpiredSessions—false면 만료된 세션 제외
- 첫 번째 인자 :
expireNow(): 세션을 즉시 만료 처리
[실수] principal 객체가 equals()를 오버라이딩하지 않으면 조회 안 됨
getAllSessions()는 내부 Map에서 equals()로 사용자를 찾는다. 이전 문서에서 오버라이딩한 이유가 여기서도 적용된다.
권한 변경 서비스에 적용
@Service
@RequiredArgsConstructor
public class BasicUserService implements UserService {
private final UserRepository userRepository;
private final SessionRegistry sessionRegistry;
public UserDto updateRole(UUID userId, Role newRole) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
user.updateRole(newRole);
userRepository.save(user);
// 세션 무효화
invalidateUserSessions(user.getUsername());
return UserMapper.toDto(user);
}
private void invalidateUserSessions(String username) {
sessionRegistry.getAllPrincipals().stream()
.filter(p -> p instanceof DiscodeitUserDetails)
.map(p -> (DiscodeitUserDetails) p)
.filter(p -> p.getUsername().equals(username))
.flatMap(p -> sessionRegistry.getAllSessions(p, false).stream())
.forEach(SessionInformation::expireNow);
}
}
sessionRegistry.getAllPrincipals(): 현재 로그인된 모든 사용자 목록- username으로 해당 사용자를 찾아 세션을 만료한다
AuthService에서 처리하는 방법도 있다
요구사항에 따라 BasicAuthService에 SessionRegistry를 주입해 처리할 수도 있다. 권한 변경 로직이 어느 서비스에 있는지에 따라 위치를 결정하면 된다.
전체 흐름
sequenceDiagram
participant Admin
participant Service as UserService
participant Registry as SessionRegistry
participant User as 대상 사용자
Admin->>Service: updateRole(userId, CHANNEL_MANAGER)
Service->>Service: user.updateRole() + save()
Service->>Registry: getAllSessions(userPrincipal)
Registry-->>Service: [session1, session2]
Service->>Registry: expireNow() x2
Service-->>Admin: UserDto 반환
User->>Registry: 다음 요청 시 세션 확인
Registry-->>User: 세션 만료 → 재로그인 필요