package com.takensoft.common.util; import jakarta.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * @author takensoft * @since 2025.03.21 * @modification * since | author | description * 2025.03.21 | takensoft | 최초 등록 * 2025.05.29 | takensoft | Redis 통합 중복로그인 관리 * 2025.06.09 | takensoft | 세션 관리 로직 개선, 안정성 향상 * * 세션 로그인 방식의 유틸리티 */ @Slf4j @Component public class SessionUtil { private final Map sessionMap = new ConcurrentHashMap<>(); private final RedisTemplate redisTemplate; public SessionUtil(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } /** * 세션 등록 - 중복로그인 처리 개선 */ public synchronized void registerSession(String mbrId, HttpSession newSession) { // 1. 기존 세션 확인 및 무효화 HttpSession oldSession = sessionMap.get(mbrId); if (oldSession != null && !oldSession.getId().equals(newSession.getId())) { oldSession.invalidate(); } // 2. 새 세션을 메모리에 등록 sessionMap.put(mbrId, newSession); // 3. Redis에 세션 정보 저장 (세션 ID 저장) String sessionKey = "session:" + mbrId; redisTemplate.opsForValue().set(sessionKey, newSession.getId(), Duration.ofSeconds(newSession.getMaxInactiveInterval())); } /** * 세션 ID로 세션 무효화 */ public void invalidateSessionById(String sessionId) { // 메모리에서 해당 세션 ID를 가진 세션 찾아서 무효화 String targetMbrId = null; sessionMap.entrySet().removeIf(entry -> { HttpSession session = entry.getValue(); if (session != null && session.getId().equals(sessionId)) { session.invalidate(); return true; } return false; }); } /** * 사용자별 세션 제거 - Redis 연동 개선 */ public void removeSession(String mbrId) { // 1. 메모리 세션 무효화 HttpSession session = sessionMap.get(mbrId); if (session != null) { session.invalidate(); } sessionMap.remove(mbrId); // 2. Redis에서 모든 관련 키 제거 cleanupRedisSessionData(mbrId); } /** * 전체 세션 무효화 - Redis 연동 개선 */ public void invalidateAllSessions() { // 1. 모든 메모리 세션 무효화 int invalidatedCount = 0; for (Map.Entry entry : sessionMap.entrySet()) { HttpSession session = entry.getValue(); if (session != null) { session.invalidate(); invalidatedCount++; } } sessionMap.clear(); // 2. Redis에서 모든 세션 관련 키 삭제 cleanupAllRedisSessionData(); } /** * 특정 사용자의 Redis 세션 데이터 정리 */ private void cleanupRedisSessionData(String mbrId) { // 세션 관련 키들 삭제 String sessionKey = "session:" + mbrId; String sessionTokenKey = "session_token:" + mbrId; redisTemplate.delete(sessionKey); redisTemplate.delete(sessionTokenKey); // 패턴 매칭으로 사용자 관련 모든 세션 키 찾아서 삭제 Set userSessionKeys = redisTemplate.keys("session*:" + mbrId); if (userSessionKeys != null && !userSessionKeys.isEmpty()) { redisTemplate.delete(userSessionKeys); } } /** * 모든 Redis 세션 데이터 정리 */ private void cleanupAllRedisSessionData() { // 모든 세션 관련 키 패턴들 String[] sessionKeyPatterns = { "session:*", "session_token:*" }; for (String pattern : sessionKeyPatterns) { Set keys = redisTemplate.keys(pattern); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } } } /** * 활성 세션 수 조회 */ public int getActiveSessionCount() { return sessionMap.size(); } /** * 특정 사용자의 세션 활성 상태 확인 */ public boolean isSessionActive(String mbrId) { HttpSession session = sessionMap.get(mbrId); if (session == null) { return false; } try { // 세션 접근을 통한 유효성 확인 session.getLastAccessedTime(); return true; } catch (IllegalStateException e) { // 무효화된 세션이면 맵에서 제거 sessionMap.remove(mbrId); return false; } } }