
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
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<String, String> 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");
}
}
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. 공통 정리 작업 (모든 쿠키 완전 제거)
performCompleteCleanup(req, res);
log.info("로그아웃 완료 - 사용자: {}", mbrId);
return resUtil.successRes(dbResult, MessageCode.LOGOUT_SUCCESS);
} catch (Exception e) {
log.error("로그아웃 처리 중 오류 발생 - 사용자: {}, 오류: {}", mbrId, e.getMessage(), e);
// 오류가 발생해도 기본 정리는 수행
try {
performCompleteCleanup(req, res);
} catch (Exception cleanupError) {
log.error("정리 작업 중 오류: {}", cleanupError.getMessage());
}
return resUtil.successRes(0, MessageCode.LOGOUT_SUCCESS); // 클라이언트에는 성공으로 응답
}
}
/**
* 관리자 시스템 설정 변경시 전체 사용자 로그아웃
*/
@PostMapping(value = "/mbr/logoutAll.json")
public ResponseEntity<?> logoutAll(HttpServletRequest req, HttpServletResponse res) {
try {
log.info("전체 사용자 로그아웃 시작");
// 1. 모든 세션 무효화
sessionUtil.invalidateAllSessions();
// 2. Redis의 모든 인증 관련 데이터 삭제
clearAllRedisAuthData();
// 3. 모든 Refresh 토큰 삭제
try {
refreshTokenService.deleteAll();
} catch (Exception e) {
log.warn("전체 Refresh 토큰 삭제 실패: {}", e.getMessage());
}
// 4. 현재 요청자도 로그아웃
performCompleteCleanup(req, res);
log.info("전체 사용자 로그아웃 완료");
return resUtil.successRes("모든 사용자가 로그아웃되었습니다.", MessageCode.LOGOUT_SUCCESS);
} catch (Exception e) {
log.error("전체 로그아웃 처리 중 오류", e);
// 오류가 발생해도 현재 요청자는 로그아웃 처리
performCompleteCleanup(req, res);
return resUtil.successRes("로그아웃 처리되었습니다.", 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);
}
} 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);
}
} 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<String> 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);
}
}
/**
* 모든 Redis 인증 데이터 정리 (전체 로그아웃용)
*/
private void clearAllRedisAuthData() {
try {
String[] globalPatterns = {
"session:*",
"session_token:*",
"jwt:*",
"user:*",
"auth:*",
"refresh:*"
};
for (String pattern : globalPatterns) {
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
log.info("전체 Redis 키 삭제: {} 개 (패턴: {})", keys.size(), pattern);
}
}
} catch (Exception e) {
log.error("전체 Redis 데이터 정리 실패", e);
}
}
/**
* 완전한 정리 작업 (모든 쿠키 제거 포함)
*/
private void performCompleteCleanup(HttpServletRequest req, HttpServletResponse res) {
try {
// 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");
} catch (Exception e) {
log.error("완전한 정리 작업 중 오류: {}", e.getMessage(), e);
}
}
/**
* 모든 쿠키 완전 제거 (확장된 버전)
*/
private void clearAllCookiesCompletely(HttpServletRequest req, HttpServletResponse res) {
try {
// 제거할 쿠키 목록 (확장)
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);
}
}
}
log.info("모든 쿠키 완전 제거 완료");
} catch (Exception e) {
log.error("쿠키 완전 제거 실패", e);
}
}
/**
* 토큰 재발급
*/
@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);
}
}
}