package com.takensoft.cms.token.web; import com.takensoft.cms.loginPolicy.service.LoginModeService; import com.takensoft.cms.loginPolicy.service.LoginPolicyService; import com.takensoft.cms.mber.vo.MberVO; import com.takensoft.cms.token.service.RefreshTokenService; import com.takensoft.cms.token.vo.RefreshTknVO; import com.takensoft.common.message.MessageCode; import com.takensoft.common.util.ResponseUtil; import com.takensoft.common.util.SessionUtil; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Set; /** * @author takensoft * @since 2024.04.01 * @modification * since | author | description * 2024.04.01 | takensoft | 최초 등록 * 2025.06.02 | takensoft | 세션 모드 Redis 정보 삭제 추가 * * RefreshToken 정보 관련 컨트롤러 */ @RestController @RequiredArgsConstructor @Slf4j public class RefreshTokenController { private final ResponseUtil resUtil; private final RefreshTokenService refreshTokenService; private final LoginPolicyService loginPolicyService; private final LoginModeService loginModeService; private final SessionUtil sessionUtil; private final RedisTemplate redisTemplate; /** * 로그아웃 - 세션/JWT 모드 통합 처리 */ @PostMapping(value = "/mbr/logout.json") public ResponseEntity logout(HttpServletRequest req, HttpServletResponse res){ String mbrId = null; String loginMode = loginModeService.getLoginMode(); try { // 1. 인증 정보에서 사용자 ID 추출 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getPrincipal() instanceof MberVO) { MberVO mber = (MberVO) auth.getPrincipal(); mbrId = mber.getMbrId(); } // 2. 세션에서 사용자 ID 추출 (인증 정보가 없는 경우) if (mbrId == null) { HttpSession session = req.getSession(false); if (session != null) { mbrId = (String) session.getAttribute("mbrId"); } } // 3. DB에서 Refresh 토큰 삭제 int dbResult = 0; if (mbrId != null) { RefreshTknVO refresh = new RefreshTknVO(); refresh.setMbrId(mbrId); dbResult = refreshTokenService.delete(req, refresh); } // 4. 로그인 모드별 처리 if ("S".equals(loginMode)) { handleSessionLogout(req, res, mbrId); } else { handleJWTLogout(req, res, mbrId); } // 5. 공통 정리 작업 (모든 쿠키 완전 제거) performCompleteCleanup(req, res); return resUtil.successRes(dbResult, MessageCode.LOGOUT_SUCCESS); } catch (Exception e) { // 오류가 발생해도 기본 정리는 수행 performCompleteCleanup(req, res); return resUtil.successRes(0, MessageCode.LOGOUT_SUCCESS); // 클라이언트에는 성공으로 응답 } } /** * 관리자 시스템 설정 변경시 전체 사용자 로그아웃 */ @PostMapping(value = "/mbr/logoutAll.json") public ResponseEntity logoutAll(HttpServletRequest req, HttpServletResponse res) { try { // 1. 모든 세션 무효화 sessionUtil.invalidateAllSessions(); // 2. Redis의 모든 인증 관련 데이터 삭제 clearAllRedisAuthData(); // 3. 모든 Refresh 토큰 삭제 refreshTokenService.deleteAll(); // 4. 현재 요청자도 로그아웃 performCompleteCleanup(req, res); return resUtil.successRes("모든 사용자가 로그아웃되었습니다.", MessageCode.LOGOUT_SUCCESS); } catch (Exception e) { // 오류가 발생해도 현재 요청자는 로그아웃 처리 performCompleteCleanup(req, res); return resUtil.successRes("로그아웃 처리되었습니다.", MessageCode.LOGOUT_SUCCESS); } } /** * 세션 모드 로그아웃 처리 */ private void handleSessionLogout(HttpServletRequest req, HttpServletResponse res, String mbrId) { // 1. 현재 세션 무효화 HttpSession session = req.getSession(false); if (session != null) { session.invalidate(); } // 2. SessionUtil에서 제거 if (mbrId != null) { sessionUtil.removeSession(mbrId); } // 3. Redis에서 세션 관련 정보 삭제 if (mbrId != null) { cleanupSessionRedisData(mbrId); } } /** * JWT 모드 로그아웃 처리 */ private void handleJWTLogout(HttpServletRequest req, HttpServletResponse res, String mbrId) { // 1. Redis에서 JWT 정보 삭제 (중복로그인 관리용) if (mbrId != null && !loginPolicyService.getPolicy()) { redisTemplate.delete("jwt:" + mbrId); } } /** * Redis 세션 데이터 정리 */ private void cleanupSessionRedisData(String mbrId) { // 세션 토큰 키 삭제 String sessionTokenKey = "session_token:" + mbrId; redisTemplate.delete(sessionTokenKey); // 세션 키 삭제 String sessionKey = "session:" + mbrId; redisTemplate.delete(sessionKey); // 기타 사용자별 Redis 키 패턴 삭제 Set userKeys = redisTemplate.keys("*:" + mbrId); if (userKeys != null && !userKeys.isEmpty()) { redisTemplate.delete(userKeys); } } /** * 모든 Redis 인증 데이터 정리 (전체 로그아웃용) */ private void clearAllRedisAuthData() { String[] globalPatterns = { "session:*", "session_token:*", "jwt:*", "user:*", "auth:*", "refresh:*" }; for (String pattern : globalPatterns) { Set keys = redisTemplate.keys(pattern); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } } } /** * 완전한 정리 작업 (모든 쿠키 제거 포함) */ private void performCompleteCleanup(HttpServletRequest req, HttpServletResponse res) { // 1. SecurityContext 제거 SecurityContextHolder.clearContext(); // 2. 모든 쿠키 완전 제거 clearAllCookiesCompletely(req, res); // 3. 응답 헤더에서 인증 정보 제거 res.setHeader("Authorization", ""); res.setHeader("loginMode", ""); // 4. 캐시 무효화 헤더 설정 res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate, private"); res.setHeader("Pragma", "no-cache"); res.setHeader("Expires", "0"); } /** * 모든 쿠키 완전 제거 (확장된 버전) */ private void clearAllCookiesCompletely(HttpServletRequest req, HttpServletResponse res) { // 제거할 쿠키 목록 (확장) String[] cookieNames = { // 일반 인증 쿠키 "refresh", "Authorization", "access_token", "JSESSIONID", "SESSION", // OAuth 관련 쿠키 "oauth_access_token", "oauth_refresh_token", "oauth_state", "OAUTH2_AUTHORIZATION_REQUEST", "oauth2_auth_request", // 카카오 관련 "kakao_login", "_kadu", "_kadub", "_kalt", "_kawlt", "_kawltea", "_karmt", "_karmts", "_tiara", "_dfs", // 네이버 관련 "NID_AUT", "NID_SES", "NID_JKL", "NID_INF", "NID_LOG", // 구글 관련 "SACSID", "APISID", "SSID", "HSID", "SID", "1P_JAR", "__Secure-1PAPISID", "__Secure-1PSID", "__Secure-3PAPISID", "__Secure-3PSID", "ACCOUNT_CHOOSER", "LSID", "GAPS", // 기타 소셜 로그인 "facebook_login", "twitter_login", "github_login", // 시스템 쿠키 "remember-me", "user-session", "auth-token", "login-token", "csrf-token" }; // 다양한 경로와 도메인에서 쿠키 삭제 String[] paths = { "/", "/oauth2", "/login", "/auth", "/api", "/mbr" }; String serverName = req.getServerName(); String[] domains = { null, // 도메인 없음 "." + serverName, serverName, ".localhost", "localhost", ".google.com", ".kakao.com", ".naver.com" }; // 각 쿠키를 모든 경로와 도메인 조합으로 삭제 for (String cookieName : cookieNames) { for (String path : paths) { for (String domain : domains) { Cookie cookie = new Cookie(cookieName, ""); cookie.setMaxAge(0); cookie.setPath(path); cookie.setHttpOnly(true); if (domain != null && !domain.equals("null") && !domain.isEmpty()) { try { cookie.setDomain(domain); } catch (Exception e) { // 도메인 설정 실패해도 계속 진행 } } // HTTPS 환경이면 Secure 설정 if (req.isSecure()) { cookie.setSecure(true); } res.addCookie(cookie); } } } // 기존 요청에 있는 모든 쿠키도 제거 if (req.getCookies() != null) { for (Cookie existingCookie : req.getCookies()) { // 기본 경로로 삭제 Cookie deleteCookie = new Cookie(existingCookie.getName(), ""); deleteCookie.setMaxAge(0); deleteCookie.setPath("/"); deleteCookie.setHttpOnly(true); if (req.isSecure()) { deleteCookie.setSecure(true); } res.addCookie(deleteCookie); // 원본 경로로도 삭제 시도 if (existingCookie.getPath() != null) { Cookie originalPathCookie = new Cookie(existingCookie.getName(), ""); originalPathCookie.setMaxAge(0); originalPathCookie.setPath(existingCookie.getPath()); originalPathCookie.setHttpOnly(true); if (existingCookie.getDomain() != null) { try { originalPathCookie.setDomain(existingCookie.getDomain()); } catch (Exception e) { // 도메인 설정 실패해도 계속 } } if (req.isSecure()) { originalPathCookie.setSecure(true); } res.addCookie(originalPathCookie); } } } } /** * 토큰 재발급 */ @PostMapping("/refresh/tokenReissue.json") public ResponseEntity tokenReissue(HttpServletRequest req, HttpServletResponse res) { try { int result = refreshTokenService.tokenReissueProc(req, res); if(result > 0) { return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); } else { return resUtil.errorRes(MessageCode.JWT_EXPIRED); } } catch (Exception e) { return resUtil.errorRes(MessageCode.JWT_EXPIRED); } } }