package com.takensoft.common.util; import com.fasterxml.jackson.databind.ObjectMapper; import com.takensoft.cms.loginPolicy.service.LoginModeService; import com.takensoft.cms.loginPolicy.service.LoginPolicyService; import com.takensoft.cms.mber.service.LgnHstryService; import com.takensoft.cms.mber.vo.LgnHstryVO; import com.takensoft.cms.mber.vo.MberVO; import com.takensoft.cms.token.service.RefreshTokenService; import com.takensoft.cms.token.vo.RefreshTknVO; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import java.io.IOException; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @author takensoft * @since 2025.03.21 * @modification * since | author | description * 2025.03.21 | takensoft | 최초 등록 * 2025.05.28 | takensoft | 통합 로그인 적용, 문제 해결 * 2025.05.29 | takensoft | Redis 통합 중복로그인 관리 * 2025.06.04 | takensoft | Redis 트랜잭션 및 타이밍 이슈 해결 * 2025.06.09 | takensoft | 중복로그인 처리 로직 개선 * * 통합 로그인 유틸리티 - 중복로그인 처리 개선 */ @Component @RequiredArgsConstructor @Slf4j public class LoginUtil { private final LgnHstryService lgnHstryService; private final HttpRequestUtil httpRequestUtil; private final LoginModeService loginModeService; private final RefreshTokenService refreshTokenService; private final LoginPolicyService loginPolicyService; private final JWTUtil jwtUtil; private final SessionUtil sessionUtil; private final RedisTemplate redisTemplate; @Value("${jwt.accessTime}") private long JWT_ACCESSTIME; @Value("${jwt.refreshTime}") private long JWT_REFRESHTIME; @Value("${cookie.time}") private int COOKIE_TIME; /** * 통합 로그인 성공 처리 */ public void successLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException { String loginMode = loginModeService.getLoginMode(); res.setHeader("loginMode", loginMode); // 로그인 이력 등록 String loginType = (String) req.getAttribute("loginType"); if (!"OAUTH2".equals(loginType)) { saveLoginHistory(mber, req); } if ("S".equals(loginMode)) { handleSessionLogin(mber, req, res); } else { handleJwtLogin(mber, req, res); } } /** * 세션 모드 로그인 처리 - 중복로그인 개선 */ private void handleSessionLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException { String mbrId = mber.getMbrId(); // 중복로그인 비허용 시 기존 세션 정리 if (!loginPolicyService.getPolicy()) { forceLogoutExistingSession(mbrId); } // JWT 토큰은 생성하되 세션에만 저장 String accessToken = jwtUtil.createJwt("Authorization", mbrId, mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME); // 세션 생성 및 정보 저장 HttpSession session = req.getSession(true); session.setAttribute("JWT_TOKEN", accessToken); session.setAttribute("mbrId", mbrId); session.setAttribute("mbrNm", mber.getMbrNm()); session.setAttribute("lgnId", mber.getLgnId()); session.setAttribute("roles", mber.getAuthorList()); session.setAttribute("loginType", req.getAttribute("loginType") != null ? req.getAttribute("loginType") : "S"); // Redis에 세션 토큰 저장 (중복로그인 관리용) if (!loginPolicyService.getPolicy()) { saveSessionTokenToRedis(mbrId, accessToken, session.getMaxInactiveInterval()); } // SessionUtil에 등록 sessionUtil.registerSession(mbrId, session); // OAuth2가 아닌 경우 JSON 응답 전송 String loginType = (String) req.getAttribute("loginType"); if (!"OAUTH2".equals(loginType)) { sendSessionLoginResponse(res, mber); } } /** * JWT 모드 로그인 처리 - 중복로그인 개선 */ private void handleJwtLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException { String mbrId = mber.getMbrId(); // 중복로그인 비허용 시 기존 토큰 정리 if (!loginPolicyService.getPolicy()) { forceLogoutExistingJWT(mbrId); } // JWT 토큰 생성 String accessToken = jwtUtil.createJwt("Authorization", mbrId, mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME); String refreshToken = jwtUtil.createJwt("refresh", mbrId, mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_REFRESHTIME); // Refresh 토큰 처리 RefreshTknVO refresh = new RefreshTknVO(); refresh.setMbrId(mbrId); // 기존 refresh 토큰 삭제 if (refreshTokenService.findByCheckRefresh(req, refresh)) { refreshTokenService.delete(req, refresh); } refresh.setToken(refreshToken); // 응답 헤더 및 쿠키 설정 res.setHeader("Authorization", accessToken); res.addCookie(jwtUtil.createCookie("refresh", refreshToken, COOKIE_TIME)); // Redis에 JWT 저장 (중복로그인 관리용) if (!loginPolicyService.getPolicy()) { saveJWTTokenToRedis(mbrId, accessToken); } // Refresh 토큰 저장 refreshTokenService.saveRefreshToken(req, res, refresh, JWT_REFRESHTIME); // OAuth2가 아닌 경우만 상태 코드 설정 String loginType = (String) req.getAttribute("loginType"); if (!"OAUTH2".equals(loginType)) { res.setStatus(HttpStatus.OK.value()); } } /** * 기존 세션 강제 로그아웃 */ private void forceLogoutExistingSession(String mbrId) { // 1. SessionUtil에서 기존 세션 제거 sessionUtil.removeSession(mbrId); // 2. Redis에서 기존 세션 토큰 삭제 String sessionTokenKey = "session_token:" + mbrId; String existingToken = redisTemplate.opsForValue().get(sessionTokenKey); if (existingToken != null) { redisTemplate.delete(sessionTokenKey); } // 3. 기타 세션 관련 키 정리 cleanupSessionRedisKeys(mbrId); } /** * 기존 JWT 강제 로그아웃 */ private void forceLogoutExistingJWT(String mbrId) { String jwtKey = "jwt:" + mbrId; String existingToken = redisTemplate.opsForValue().get(jwtKey); if (existingToken != null) { redisTemplate.delete(jwtKey); } } /** * Redis에 세션 토큰 저장 */ private void saveSessionTokenToRedis(String mbrId, String accessToken, int sessionTimeout) { String tokenKey = "session_token:" + mbrId; redisTemplate.opsForValue().set(tokenKey, accessToken, Duration.ofSeconds(sessionTimeout)); } /** * Redis에 JWT 토큰 저장 */ private void saveJWTTokenToRedis(String mbrId, String accessToken) { String jwtKey = "jwt:" + mbrId; redisTemplate.opsForValue().set(jwtKey, accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS); } /** * 세션 관련 Redis 키 정리 */ private void cleanupSessionRedisKeys(String mbrId) { Set sessionKeys = redisTemplate.keys("session*:" + mbrId); if (sessionKeys != null && !sessionKeys.isEmpty()) { redisTemplate.delete(sessionKeys); } } /** * 세션 로그인 응답 전송 */ private void sendSessionLoginResponse(HttpServletResponse res, MberVO mber) throws IOException { Map result = new HashMap<>(); result.put("mbrId", mber.getMbrId()); result.put("mbrNm", mber.getMbrNm()); result.put("roles", mber.getAuthorList()); res.setContentType("application/json;charset=UTF-8"); res.setCharacterEncoding("UTF-8"); res.setStatus(HttpStatus.OK.value()); String jsonResponse = new ObjectMapper().writeValueAsString(result); res.getWriter().write(jsonResponse); res.getWriter().flush(); } /** * 로그인 이력 저장 */ private void saveLoginHistory(MberVO mber, HttpServletRequest req) { String userAgent = httpRequestUtil.getUserAgent(req); LgnHstryVO lgnHstryVO = new LgnHstryVO(); lgnHstryVO.setLgnId(mber.getLgnId()); lgnHstryVO.setLgnType(mber.getAuthorities().stream() .anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1"); lgnHstryVO.setCntnIp(httpRequestUtil.getIp(req)); lgnHstryVO.setCntnOperSys(httpRequestUtil.getOS(userAgent)); lgnHstryVO.setDeviceNm(httpRequestUtil.getDevice(userAgent)); lgnHstryVO.setBrwsrNm(httpRequestUtil.getBrowser(userAgent)); lgnHstryService.LgnHstrySave(lgnHstryVO); } }