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 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.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 트랜잭션 및 타이밍 이슈 해결 * * 통합 로그인 유틸리티 */ @Component @RequiredArgsConstructor 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) { // 로그인 방식 확인 String loginMode = loginModeService.getLoginMode(); res.setHeader("loginMode", loginMode); try { // 로그인 이력 등록 String loginType = (String) req.getAttribute("loginType"); if (!"OAUTH2".equals(loginType)) { saveLoginHistory(mber, req); } if ("S".equals(loginMode)) { // Redis 기반 중복로그인 관리 적용 handleSessionLogin(mber, req, res); } else { // 기존 Redis 기반 관리 유지 handleJwtLogin(mber, req, res); } } catch (IOException ioe) { throw new RuntimeException(ioe); } catch (Exception e) { throw e; } } /** * 세션 모드 로그인 처리 - Redis 트랜잭션 개선 */ private void handleSessionLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException { // JWT 토큰은 생성하되 세션에만 저장 String accessToken = jwtUtil.createJwt("Authorization", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME); // 세션 생성 및 정보 저장 HttpSession session = req.getSession(true); session.setAttribute("JWT_TOKEN", accessToken); session.setAttribute("mbrId", mber.getMbrId()); 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"); // 토큰만 저장 if (!loginPolicyService.getPolicy()) { String tokenKey = "session_token:" + mber.getMbrId(); // 기존 토큰 삭제 후 새 토큰 저장 redisTemplate.delete(tokenKey); redisTemplate.opsForValue().set(tokenKey, accessToken, Duration.ofSeconds(session.getMaxInactiveInterval())); } // OAuth2가 아닌 경우 JSON 응답 전송 String loginType = (String) req.getAttribute("loginType"); if (!"OAUTH2".equals(loginType)) { try { 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(); } catch (Exception e) { throw e; } } } /** * JWT 모드 로그인 처리 */ private void handleJwtLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException { // JWT 토큰 생성 String accessToken = jwtUtil.createJwt("Authorization", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME); String refreshToken = jwtUtil.createJwt("refresh", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_REFRESHTIME); // Refresh 토큰 처리 RefreshTknVO refresh = new RefreshTknVO(); refresh.setMbrId(mber.getMbrId()); // 기존 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 저장 if (!loginPolicyService.getPolicy()) { redisTemplate.delete("jwt:" + mber.getMbrId()); redisTemplate.opsForValue().set("jwt:" + mber.getMbrId(), accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS); } // 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 saveLoginHistory(MberVO mber, HttpServletRequest req) { try { 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); } catch (Exception e) { } } }