
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;
/**
* @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<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);
}
}
/**
* 세션 관련 쿠키 제거
*/
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);
}
}
}