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; /** * @param req - HTTP 요청 객체 * @param res - HTTP 응답 객체 * @return ResponseEntity - 로그아웃 응답 결과 * * 로그아웃 - 세션/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"); } } log.info("로그아웃 시작 - 사용자: {}, 모드: {}", mbrId, loginMode); // 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. 공통 정리 작업 performCommonCleanup(req, res); log.info("로그아웃 완료 - 사용자: {}", mbrId); return resUtil.successRes(dbResult, MessageCode.LOGOUT_SUCCESS); } catch (Exception e) { log.error("로그아웃 처리 중 오류 발생 - 사용자: {}, 오류: {}", mbrId, e.getMessage(), e); // 오류가 발생해도 기본 정리는 수행 try { performCommonCleanup(req, res); } catch (Exception cleanupError) { log.error("정리 작업 중 오류: {}", cleanupError.getMessage()); } return resUtil.successRes(0, MessageCode.LOGOUT_SUCCESS); // 클라이언트에는 성공으로 응답 } } /** * 세션 모드 로그아웃 처리 */ private void handleSessionLogout(HttpServletRequest req, HttpServletResponse res, String mbrId) { try { // 1. 현재 세션 무효화 HttpSession session = req.getSession(false); if (session != null) { try { session.invalidate(); log.debug("세션 무효화 완료: {}", session.getId()); } catch (IllegalStateException e) { log.debug("이미 무효화된 세션: {}", e.getMessage()); } } // 2. SessionUtil에서 제거 if (mbrId != null) { sessionUtil.removeSession(mbrId); } // 3. Redis에서 세션 관련 정보 삭제 if (mbrId != null) { cleanupSessionRedisData(mbrId); } // 4. 세션 쿠키 제거 removeSessionCookies(res); } catch (Exception e) { log.error("세션 모드 로그아웃 처리 중 오류: {}", e.getMessage(), e); } } /** * JWT 모드 로그아웃 처리 */ private void handleJWTLogout(HttpServletRequest req, HttpServletResponse res, String mbrId) { try { // 1. Redis에서 JWT 정보 삭제 (중복로그인 관리용) if (mbrId != null && !loginPolicyService.getPolicy()) { redisTemplate.delete("jwt:" + mbrId); log.debug("Redis JWT 토큰 삭제: jwt:{}", mbrId); } // 2. JWT 관련 쿠키 제거 removeJWTCookies(res); } catch (Exception e) { log.error("JWT 모드 로그아웃 처리 중 오류: {}", e.getMessage(), e); } } /** * Redis 세션 데이터 정리 */ private void cleanupSessionRedisData(String mbrId) { try { // 세션 토큰 키 삭제 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); log.debug("사용자별 Redis 키 삭제: {}", userKeys); } } catch (Exception e) { log.error("Redis 세션 데이터 정리 중 오류: {}", e.getMessage(), e); } } /** * 세션 관련 쿠키 제거 */ private void removeSessionCookies(HttpServletResponse res) { String[] sessionCookies = {"JSESSIONID", "SESSION"}; for (String cookieName : sessionCookies) { // 기본 경로 Cookie cookie = new Cookie(cookieName, null); cookie.setMaxAge(0); cookie.setPath("/"); res.addCookie(cookie); // 도메인별 쿠키도 삭제 시도 Cookie domainCookie = new Cookie(cookieName, null); domainCookie.setMaxAge(0); domainCookie.setPath("/"); // domainCookie.setDomain(".example.com"); // 필요시 도메인 설정 res.addCookie(domainCookie); } } /** * JWT 관련 쿠키 제거 */ private void removeJWTCookies(HttpServletResponse res) { String[] jwtCookies = {"refresh", "Authorization", "access_token"}; for (String cookieName : jwtCookies) { Cookie cookie = new Cookie(cookieName, null); cookie.setMaxAge(0); cookie.setHttpOnly(true); cookie.setPath("/"); res.addCookie(cookie); } } /** * OAuth2 관련 쿠키 제거 */ private void removeOAuth2Cookies(HttpServletResponse res) { String[] oauthCookies = { "oauth_access_token", "oauth_refresh_token", "oauth_state", "OAUTH2_AUTHORIZATION_REQUEST", "oauth2_auth_request" }; for (String cookieName : oauthCookies) { Cookie cookie = new Cookie(cookieName, null); cookie.setMaxAge(0); cookie.setPath("/"); res.addCookie(cookie); } } /** * 공통 정리 작업 */ private void performCommonCleanup(HttpServletRequest req, HttpServletResponse res) { try { // 1. SecurityContext 제거 SecurityContextHolder.clearContext(); // 2. OAuth2 쿠키 제거 removeOAuth2Cookies(res); // 3. 응답 헤더에서 인증 정보 제거 res.setHeader("Authorization", ""); res.setHeader("loginMode", ""); // 4. 캐시 무효화 헤더 설정 res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); res.setHeader("Pragma", "no-cache"); res.setHeader("Expires", "0"); } catch (Exception e) { log.error("공통 정리 작업 중 오류: {}", e.getMessage(), e); } } /** * @param req - HTTP 요청 객체 * @param res - HTTP 응답 객체 * @return ResponseEntity - 토큰 재발급 응답 결과 * * 토큰 재발급 */ @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) { log.error("토큰 재발급 중 오류: {}", e.getMessage(), e); return resUtil.errorRes(MessageCode.JWT_EXPIRED); } } }