hmkim 06-04
250603 김혜민 다중로그인설정 수정
@e291fe083780a3b68d7153f7140184ff00628705
src/main/java/com/takensoft/cms/token/service/impl/RefreshTokenServiceImpl.java
--- src/main/java/com/takensoft/cms/token/service/impl/RefreshTokenServiceImpl.java
+++ src/main/java/com/takensoft/cms/token/service/impl/RefreshTokenServiceImpl.java
@@ -211,7 +211,8 @@
                 result += saveRefreshToken(req, res, refresh, JWT_REFRESHTIME);
 
                 // 응답설정 RefreshToken
-                //            res.setHeader("refresh", newRefreshToken);
+                // res.setHeader("refresh", newRefreshToken);
+
                 // 쿠키 방식
                 res.addCookie(jwtUtil.createCookie("refresh", newRefreshToken, COOKIE_TIME));
             }
src/main/java/com/takensoft/cms/token/web/RefreshTokenController.java
--- src/main/java/com/takensoft/cms/token/web/RefreshTokenController.java
+++ src/main/java/com/takensoft/cms/token/web/RefreshTokenController.java
@@ -1,6 +1,5 @@
 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;
@@ -27,12 +26,14 @@
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import java.nio.charset.Charset;
+
 /**
  * @author takensoft
  * @since 2024.04.01
  * @modification
  *     since    |    author    | description
  *  2024.04.01  |  takensoft   | 최초 등록
+ *  2025.06.02  |  takensoft   | 세션 모드 Redis 정보 삭제 추가
  *
  * RefreshToken 정보 관련 컨트롤러
  */
@@ -47,12 +48,13 @@
     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){
@@ -61,29 +63,45 @@
         if (auth != null && auth.getPrincipal() instanceof MberVO) {
             MberVO mber = (MberVO) auth.getPrincipal();
             String mbrId = mber.getMbrId();
-            String loginType = loginModeService.getLoginMode(); // J or S
+            String loginMode = loginModeService.getLoginMode(); // J or S
 
             // Refresh 토큰 삭제 (DB)
             RefreshTknVO refresh = new RefreshTknVO();
             refresh.setMbrId(mbrId);
-            int result =  refreshTokenService.delete(req, refresh);
+            int result = refreshTokenService.delete(req, refresh);
 
-            if (loginType.equals("S")) {
-                // 세션 방식: 세션 만료 + SessionMap에서 제거
+            if (loginMode.equals("S")) {
                 HttpSession session = req.getSession(false);
-                if (session != null) session.invalidate();
+                if (session != null) {
+                    session.invalidate();
+                }
+
+                // SessionUtil에서 제거
                 sessionUtil.removeSession(mbrId);
 
+                // Redis에서 세션 정보 삭제 (중복로그인 관리용)
+                String sessionKey = "session:" + mbrId;
+                try {
+                    redisTemplate.delete(sessionKey);
+                } catch (Exception e) {
+                }
+
+                // JSESSIONID 쿠키 제거
                 Cookie cookie = new Cookie("JSESSIONID", null);
                 cookie.setMaxAge(0); // 삭제
                 cookie.setPath("/");
                 res.addCookie(cookie);
+
             } else {
                 // JWT 방식: Redis에서 삭제
                 if (!loginPolicyService.getPolicy()) {
-                    redisTemplate.delete("jwt:" + mbrId);
+                    try {
+                        redisTemplate.delete("jwt:" + mbrId);
+                    } catch (Exception e) {
+                    }
                 }
-                // 쿠키 제거
+
+                // refresh 쿠키 제거
                 Cookie cookie = new Cookie("refresh", null);
                 cookie.setMaxAge(0);
                 cookie.setHttpOnly(true);
@@ -91,11 +109,12 @@
                 res.addCookie(cookie);
             }
 
-            //  SecurityContext 제거
+            // SecurityContext 제거
             SecurityContextHolder.clearContext();
+
             return resUtil.successRes(result, MessageCode.LOGOUT_SUCCESS);
         }
-            return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
+        return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR);
     }
 
     /**
@@ -122,4 +141,4 @@
             return new ResponseEntity<>(responseData, headers, HttpStatus.INTERNAL_SERVER_ERROR);
         }
     }
-}
+}
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/common/config/SecurityConfig.java
--- src/main/java/com/takensoft/common/config/SecurityConfig.java
+++ src/main/java/com/takensoft/common/config/SecurityConfig.java
@@ -35,6 +35,7 @@
 
 import jakarta.servlet.http.HttpServletRequest;
 import java.util.Collections;
+import java.util.List;
 
 /**
  * @author takensoft
@@ -145,7 +146,8 @@
                         configuration.setAllowedHeaders(Collections.singletonList("*")); // 허용할 헤더
                         configuration.setAllowCredentials(true); // 프론트에서 credentials 설정하면 true
                         configuration.setMaxAge(3600L); // 허용을 물고 있을 시간
-                        configuration.setExposedHeaders(Collections.singletonList("Authorization")); // 서버에서 JWT를 Authorization에 담아 보내기 위해 허용을 함
+                 //       configuration.setExposedHeaders(Collections.singletonList("Authorization")); // 서버에서 JWT를 Authorization에 담아 보내기 위해 허용을 함
+                        configuration.setExposedHeaders(List.of("Authorization", "loginMode"));
                         return configuration;
                     }
                 })
@@ -189,15 +191,15 @@
         // Context Path 검증 필터
         http.addFilterBefore(new ContextPathFilter(cntxtPthService), SecurityContextPersistenceFilter.class);
 
-        // JWT 토큰 검증 필터
-        http.addFilterBefore(new JWTFilter(jwtUtil, appConfig, loginModeService, loginPolicyService, redisTemplate), LoginFilter.class);
-
         // 접근(아이피) 검증 필터
-        http.addFilterBefore(new AccesFilter(accesCtrlService, httpRequestUtil, appConfig), JWTFilter.class);
+        http.addFilterBefore(new AccesFilter(accesCtrlService, httpRequestUtil, appConfig), UsernamePasswordAuthenticationFilter.class);
 
         // 로그인 필터
         http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), emailServiceImpl, loginUtil, email2ndAuth, unifiedLoginService), UsernamePasswordAuthenticationFilter.class);
 
+        // JWT 토큰 검증 필터
+        http.addFilterAfter(new JWTFilter(jwtUtil, appConfig, loginModeService, loginPolicyService, redisTemplate), UsernamePasswordAuthenticationFilter.class);
+
         return http.build();
     }
 }
(파일 끝에 줄바꿈 문자 없음)
src/main/java/com/takensoft/common/filter/AccesFilter.java
--- src/main/java/com/takensoft/common/filter/AccesFilter.java
+++ src/main/java/com/takensoft/common/filter/AccesFilter.java
@@ -118,7 +118,8 @@
     private boolean isOAuth2Request(String requestURI) {
         return requestURI.startsWith("/oauth2/") ||
                 requestURI.startsWith("/login/oauth2/") ||
-                requestURI.startsWith("/oauth/");
+                requestURI.startsWith("/oauth/") ||
+                requestURI.contains("oauth_success=true");
     }
 
     /**
src/main/java/com/takensoft/common/filter/JWTFilter.java
--- src/main/java/com/takensoft/common/filter/JWTFilter.java
+++ src/main/java/com/takensoft/common/filter/JWTFilter.java
@@ -11,6 +11,8 @@
 import io.jsonwebtoken.ExpiredJwtException;
 import io.jsonwebtoken.JwtException;
 import jakarta.servlet.http.HttpSession;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
@@ -25,6 +27,7 @@
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.time.Duration;
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -35,9 +38,12 @@
  *     since    |    author    | description
  *  2024.04.01  |  takensoft   | 최초 등록
  *  2025.05.30  |  takensoft   | 모드별 명확한 분기 처리, Redis 통합
+ *  2025.06.02  |  takensoft   | 세션 모드 중복로그인 검증 개선, 401 에러 통일
+ *  2025.06.04  |  takensoft   | 세션에서 JWT 토큰 추출하여 통합 처리
  *
- * JWT 토큰 검증 Filter - 모드별 명확한 분기 처리
+ * JWT 토큰 검증 Filter
  */
+@Slf4j
 public class JWTFilter extends OncePerRequestFilter {
 
     private static final String AUTHORIZATION_HEADER = "Authorization";
@@ -61,8 +67,8 @@
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         String requestURI = request.getRequestURI();
 
-        // OAuth2 관련 경로는 검증 제외
-        if (isOAuth2Request(requestURI)) {
+        // OAuth2 관련 경로 및 로그인 요청은 검증 제외
+        if (isLoginRequest(requestURI) || isOAuth2Request(requestURI)) {
             filterChain.doFilter(request, response);
             return;
         }
@@ -71,10 +77,8 @@
             String loginMode = loginModeService.getLoginMode();
 
             if ("S".equals(loginMode)) {
-                // 세션 모드: 세션 기반 검증 및 Redis 중복로그인 체크
                 handleSessionMode(request, response, filterChain);
             } else {
-                // JWT 모드: 토큰 기반 검증
                 handleJwtMode(request, response, filterChain);
             }
 
@@ -90,7 +94,7 @@
     }
 
     /**
-     * 세션 모드 처리 - Redis 기반 중복로그인 체크
+     * 세션 모드 처리 - 세션에서 JWT 토큰을 꺼내서 JWT 검증 로직 재사용
      */
     private void handleSessionMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         HttpSession session = request.getSession(false);
@@ -106,43 +110,53 @@
             return;
         }
 
-        // Redis 기반 중복로그인 체크 (세션 모드)
-        if (!loginPolicyService.getPolicy()) {
-            String sessionKey = "session:" + mbrId;
-            String storedSessionId = redisTemplate.opsForValue().get(sessionKey);
+        // 세션에서 JWT 토큰 꺼내기
+        String sessionToken = (String) session.getAttribute("JWT_TOKEN");
+        if (sessionToken == null) {
+            sendSessionExpiredResponse(response, request);
+            return;
+        }
 
-            if (storedSessionId == null || !storedSessionId.equals(session.getId())) {
-                // 다른 곳에서 로그인했거나 세션이 만료됨
+
+        if (!loginPolicyService.getPolicy()) {
+            String tokenKey = "session_token:" + mbrId;
+            String storedToken = redisTemplate.opsForValue().get(tokenKey);
+
+            if (storedToken != null && !storedToken.equals(sessionToken)) {
+                // 토큰이 다르면 바로 차단
                 try {
                     session.invalidate();
                 } catch (IllegalStateException e) {
-                    // 이미 무효화된 세션
+                    // 이미 무효화됨
                 }
                 sendSessionExpiredResponse(response, request);
                 return;
             }
         }
 
-        // 세션 정보로 Authentication 설정
+        // 세션에서 꺼낸 JWT 토큰을 헤더에 설정하고 JWT 검증 로직 재사용
         try {
-            List<MberAuthorVO> roles = (List<MberAuthorVO>) session.getAttribute("roles");
-            String lgnId = (String) session.getAttribute("lgnId");
-            String mbrNm = (String) session.getAttribute("mbrNm");
+            // Authorization 헤더 설정
+            HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(request) {
+                @Override
+                public String getHeader(String name) {
+                    if (AUTHORIZATION_HEADER.equals(name)) {
+                        return "Bearer " + sessionToken;
+                    }
+                    return super.getHeader(name);
+                }
+            };
 
-            MberVO mber = new MberVO(mbrId, lgnId, roles);
-            mber.setMbrNm(mbrNm);
+            // 기존 JWT 검증 로직 재사용
+            handleJwtMode(wrappedRequest, response, filterChain);
 
-            Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities());
-            SecurityContextHolder.getContext().setAuthentication(authToken);
-
-            filterChain.doFilter(request, response);
         } catch (Exception e) {
             sendSessionExpiredResponse(response, request);
         }
     }
 
     /**
-     * JWT 모드 처리 - 기존 로직 유지
+     * JWT 모드 처리
      */
     private void handleJwtMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         String accessToken = request.getHeader(AUTHORIZATION_HEADER);
@@ -152,27 +166,42 @@
             return;
         }
 
+        // JWT 토큰 만료 체크
         if ((Boolean) jwtUtil.getClaim(accessToken, "isExpired")) {
             sendTokenExpiredResponse(response, request);
             return;
         }
 
+        // 토큰에서 사용자 정보 추출
         MberVO mber = new MberVO(
                 (String) jwtUtil.getClaim(accessToken, "mbrId"),
                 (String) jwtUtil.getClaim(accessToken, "lgnId"),
                 (List<MberAuthorVO>) jwtUtil.getClaim(accessToken, "roles")
         );
 
-        // JWT 모드에서 중복로그인 체크
-        if (!loginPolicyService.getPolicy() && !isTokenValid(mber.getMbrId(), accessToken)) {
+        // 중복로그인 체크
+        String loginMode = loginModeService.getLoginMode();
+        if ("J".equals(loginMode) && !loginPolicyService.getPolicy() && !isTokenValid(mber.getMbrId(), accessToken)) {
             sendTokenExpiredResponse(response, request);
             return;
         }
 
+        // Authentication 설정
         Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities());
         SecurityContextHolder.getContext().setAuthentication(authToken);
 
         filterChain.doFilter(request, response);
+    }
+
+    /**
+     * 로그인 요청인지 확인
+     */
+    private boolean isLoginRequest(String requestURI) {
+        return requestURI.equals("/mbr/loginProc.json") ||
+                requestURI.startsWith("/mbr/login") ||
+                requestURI.startsWith("/refresh/") ||
+                requestURI.startsWith("/sys/") ||
+                requestURI.contains("loginProc");
     }
 
     /**
@@ -181,7 +210,8 @@
     private boolean isOAuth2Request(String requestURI) {
         return requestURI.startsWith("/oauth2/") ||
                 requestURI.startsWith("/login/oauth2/") ||
-                requestURI.startsWith("/oauth/");
+                requestURI.startsWith("/oauth/") ||
+                requestURI.contains("oauth_success=true");
     }
 
     /**
@@ -209,11 +239,11 @@
     }
 
     /**
-     * 세션 만료 응답
+     * 세션 만료 응답 - JWT와 동일한 메시지로 통일
      */
     private void sendSessionExpiredResponse(HttpServletResponse response, HttpServletRequest request) throws IOException {
         ErrorResponse errorResponse = new ErrorResponse();
-        errorResponse.setMessage("Session expired or duplicate login detected");
+        errorResponse.setMessage("Token expired"); // JWT와 동일한 메시지
         errorResponse.setPath(request.getRequestURI());
         errorResponse.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase());
         errorResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
src/main/java/com/takensoft/common/filter/LoginFilter.java
--- src/main/java/com/takensoft/common/filter/LoginFilter.java
+++ src/main/java/com/takensoft/common/filter/LoginFilter.java
@@ -10,7 +10,6 @@
 import com.takensoft.common.verify.service.Impl.EmailServiceImpl;
 import com.takensoft.common.verify.vo.EmailVO;
 import lombok.SneakyThrows;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -38,7 +37,6 @@
  *
  * 사용자 로그인 요청을 처리하는 Filter - 통합 로그인 시스템 적용
  */
-@Slf4j
 public class LoginFilter extends UsernamePasswordAuthenticationFilter {
 
     private final AuthenticationManager authenticationManager;
@@ -67,7 +65,7 @@
         String lgnId = login.getLgnId();
         String pswd = login.getPswd();
         req.setAttribute("lgnReqPage", login.getLgnReqPage());
-        req.setAttribute("loginType", "S"); // 시스템 로그인 표시
+        req.setAttribute("loginMode", "S"); // 시스템 로그인 표시
 
         // 통합 로그인 시스템을 통한 사용자 검증
         try {
@@ -118,7 +116,22 @@
             return;
         }
 
-        loginUtil.successLogin(mber, req, res);
+        try {
+            // 응답이 이미 커밋되었는지 확인
+            if (res.isCommitted()) {
+                return;
+            }
+            loginUtil.successLogin(mber, req, res);
+
+        } catch (Exception e) {
+            // 예외 발생 시
+            if (!res.isCommitted()) {
+                res.setContentType("application/json;charset=UTF-8");
+                res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+                result.put("message", "로그인 처리 중 오류가 발생했습니다.");
+                new ObjectMapper().writeValue(res.getOutputStream(), result);
+            }
+        }
     }
 
     @Override
src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
--- src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
@@ -29,8 +29,9 @@
  *  2025.05.22  |  takensoft   | 최초 등록
  *  2025.05.28  |  takensoft   | 통합 로그인 적용
  *  2025.05.29  |  takensoft   | OAuth2 통합 문제 해결
+ *  2025.06.02  |  takensoft   | 세션 모드 중복로그인 처리 개선
  *
- * OAuth2 로그인 성공 핸들러 - 통합 로그인 시스템 적용 (문제 해결)
+ * OAuth2 로그인 성공 핸들러 - 세션 모드 중복로그인 처리 개선
  */
 @Slf4j
 @Component
@@ -64,13 +65,16 @@
                     request
             );
 
+            // OAuth2 로그인 이력 저장
             saveLoginHistory(request, mber, oAuth2User.getProvider());
+
+            request.setAttribute("loginType", "OAUTH2");
 
             // LoginUtil을 통한 통합 로그인 처리
             loginUtil.successLogin(mber, request, response);
 
-            String redirectUrl = String.format("%s/login.page?oauth_success=true&loginMode=%s",
-                    frontUrl, currentLoginMode);
+            // OAuth2 성공 후 프론트엔드로 리다이렉트
+            String redirectUrl = String.format("%s/login.page?oauth_success=true&loginMode=%s",frontUrl, currentLoginMode);
             getRedirectStrategy().sendRedirect(request, response, redirectUrl);
 
         } catch (Exception e) {
@@ -79,7 +83,7 @@
     }
 
     /**
-     * 로그인 이력 저장
+     * 로그인 이력 저장 - OAuth2 전용
      */
     private void saveLoginHistory(HttpServletRequest request, MberVO mber, String provider) {
         try {
@@ -96,7 +100,6 @@
 
             lgnHstryService.LgnHstrySave(loginHistory);
         } catch (Exception e) {
-            log.error("로그인 이력 저장 실패: {}", e.getMessage());
         }
     }
 
src/main/java/com/takensoft/common/util/LoginUtil.java
--- src/main/java/com/takensoft/common/util/LoginUtil.java
+++ src/main/java/com/takensoft/common/util/LoginUtil.java
@@ -3,7 +3,6 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.takensoft.cms.loginPolicy.service.LoginModeService;
 import com.takensoft.cms.loginPolicy.service.LoginPolicyService;
-import com.takensoft.cms.loginPolicy.service.StorageModeService;
 import com.takensoft.cms.mber.service.LgnHstryService;
 import com.takensoft.cms.mber.vo.LgnHstryVO;
 import com.takensoft.cms.mber.vo.MberVO;
@@ -13,7 +12,6 @@
 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;
@@ -34,12 +32,12 @@
  *  2025.03.21  |  takensoft   | 최초 등록
  *  2025.05.28  |  takensoft   | 통합 로그인 적용, 문제 해결
  *  2025.05.29  |  takensoft   | Redis 통합 중복로그인 관리
+ *  2025.06.04  |  takensoft   | Redis 트랜잭션 및 타이밍 이슈 해결
  *
- * 통합 로그인 유틸리티 - Redis 통합 중복로그인 관리
+ * 통합 로그인 유틸리티
  */
 @Component
 @RequiredArgsConstructor
-@Slf4j
 public class LoginUtil {
     private final LgnHstryService lgnHstryService;
     private final HttpRequestUtil httpRequestUtil;
@@ -49,7 +47,6 @@
     private final JWTUtil jwtUtil;
     private final SessionUtil sessionUtil;
     private final RedisTemplate<String, String> redisTemplate;
-    private final StorageModeService storageModeService;
 
     @Value("${jwt.accessTime}")
     private long JWT_ACCESSTIME;
@@ -59,19 +56,19 @@
     private int COOKIE_TIME;
 
     /**
-     * 통합 로그인 성공 처리 - Redis 기반 중복로그인 관리
+     * 통합 로그인 성공 처리
      */
     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);
             }
-
-            // 로그인 방식 확인
-            String loginMode = loginModeService.getLoginMode();
-
             if ("S".equals(loginMode)) {
                 // Redis 기반 중복로그인 관리 적용
                 handleSessionLogin(mber, req, res);
@@ -79,29 +76,19 @@
                 // 기존 Redis 기반 관리 유지
                 handleJwtLogin(mber, req, res);
             }
-
-            // 스토리지 저장 방식 확인
-            String storageMode = storageModeService.findByStorageMode();
-
-            res.setHeader("login-type", loginMode);
-            res.setHeader("storage-type", storageMode);
-            log.info("통합 로그인 성공 처리 완료: {}, 로그인 모드: {}, 스토리지 모드: {}", mber.getMbrId(), loginMode, storageMode);
         }
         catch (IOException ioe) {
-            log.error("로그인 응답 처리 중 IO 오류", ioe);
             throw new RuntimeException(ioe);
         }
         catch (Exception e) {
-            log.error("로그인 처리 중 오류 발생", e);
             throw e;
         }
     }
 
     /**
-     * 세션 모드 로그인 처리 - Redis 기반 중복로그인 관리
+     * 세션 모드 로그인 처리 - Redis 트랜잭션 개선
      */
     private void handleSessionLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
-        log.info("세션 모드 로그인 처리 (Redis 통합): {}", mber.getMbrId());
 
         // JWT 토큰은 생성하되 세션에만 저장
         String accessToken = jwtUtil.createJwt("Authorization",
@@ -118,52 +105,38 @@
         session.setAttribute("loginType", req.getAttribute("loginType") != null ?
                 req.getAttribute("loginType") : "S");
 
-        //중복 로그인 비허용을 Redis로 통합 관리
+        // 토큰만 저장
         if (!loginPolicyService.getPolicy()) {
-            handleDuplicateSessionLogin(mber.getMbrId(), session);
+            String tokenKey = "session_token:" + mber.getMbrId();
+            // 기존 토큰 삭제 후 새 토큰 저장
+            redisTemplate.delete(tokenKey);
+            redisTemplate.opsForValue().set(tokenKey, accessToken, Duration.ofSeconds(session.getMaxInactiveInterval()));
         }
 
-        // 응답 데이터 구성 (OAuth2는 JSON 응답 없이 리다이렉트만)
+        // OAuth2가 아닌 경우 JSON 응답 전송
         String loginType = (String) req.getAttribute("loginType");
         if (!"OAUTH2".equals(loginType)) {
-            Map<String, Object> result = new HashMap<>();
-            result.put("mbrId", mber.getMbrId());
-            result.put("mbrNm", mber.getMbrNm());
-            result.put("roles", mber.getAuthorList());
+            try {
+                Map<String, Object> 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.setStatus(HttpStatus.OK.value());
-            new ObjectMapper().writeValue(res.getOutputStream(), result);
-        }
-    }
+                res.setContentType("application/json;charset=UTF-8");
+                res.setCharacterEncoding("UTF-8");
+                res.setStatus(HttpStatus.OK.value());
 
-    /**
-     * Redis 기반 세션 중복로그인 관리
-     */
-    private void handleDuplicateSessionLogin(String mbrId, HttpSession newSession) {
-        try {
-            String sessionKey = "session:" + mbrId;
-
-            // 기존 세션 확인 및 무효화
-            String oldSessionId = redisTemplate.opsForValue().get(sessionKey);
-            if (oldSessionId != null && !oldSessionId.equals(newSession.getId())) {
-                // 기존 세션 무효화
-                sessionUtil.invalidateSessionById(oldSessionId);
+                String jsonResponse = new ObjectMapper().writeValueAsString(result);
+                res.getWriter().write(jsonResponse);
+                res.getWriter().flush();
+            } catch (Exception e) {
+                throw e;
             }
-
-            // 새 세션 정보를 Redis에 저장
-            redisTemplate.opsForValue().set(sessionKey, newSession.getId(),
-                    Duration.ofSeconds(newSession.getMaxInactiveInterval()));
-
-            // 기존 SessionUtil에도 등록 (호환성 유지)
-            sessionUtil.registerSession(mbrId, newSession);
-        } catch (Exception e) {
-            // 실패해도 로그인은 계속 진행
         }
     }
 
     /**
-     * JWT 모드 로그인 처리 - 기존 방식 유지
+     * JWT 모드 로그인 처리
      */
     private void handleJwtLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException {
         // JWT 토큰 생성
src/main/java/com/takensoft/common/util/SessionUtil.java
--- src/main/java/com/takensoft/common/util/SessionUtil.java
+++ src/main/java/com/takensoft/common/util/SessionUtil.java
@@ -1,6 +1,7 @@
 package com.takensoft.common.util;
 
 import jakarta.servlet.http.HttpSession;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
 
@@ -16,8 +17,9 @@
  *  2025.03.21  |  takensoft   | 최초 등록
  *  2025.05.29  |  takensoft   | Redis 통합 중복로그인 관리
  *
- * 세션 로그인 방식의 유틸리티 - Redis 통합
+ * 세션 로그인 방식의 유틸리티
  */
+@Slf4j
 @Component
 public class SessionUtil {
 
@@ -40,18 +42,15 @@
                 try {
                     oldSession.invalidate();
                 } catch (IllegalStateException e) {
+
                 }
             }
-
             // 2. 새 세션을 메모리에 등록
             sessionMap.put(mbrId, newSession);
 
-            // 3. Redis에 세션 정보 저장 (중복로그인 관리용)
-            String sessionKey = "session:" + mbrId;
-            redisTemplate.opsForValue().set(sessionKey, newSession.getId(),
-                    Duration.ofSeconds(newSession.getMaxInactiveInterval()));
+            // 3. Redis 동기화는 LoginUtil에서 처리됨
+
         } catch (Exception e) {
-            e.printStackTrace();
         }
     }
 
@@ -91,17 +90,19 @@
                 try {
                     session.invalidate();
                 } catch (IllegalStateException e) {
-                    e.printStackTrace();
+
                 }
             }
             sessionMap.remove(mbrId);
 
             // 2. Redis에서도 제거
             String sessionKey = "session:" + mbrId;
-            redisTemplate.delete(sessionKey);
+            String deletedSessionId = redisTemplate.opsForValue().get(sessionKey);
+            if (deletedSessionId != null) {
+                redisTemplate.delete(sessionKey);
+            }
 
         } catch (Exception e) {
-            e.printStackTrace();
         }
     }
 
@@ -134,46 +135,6 @@
             }
         } catch (Exception e) {
             e.printStackTrace();
-        }
-    }
-
-    /**
-     * 현재 활성 세션 수 조회
-     */
-    public int getActiveSessionCount() {
-        return sessionMap.size();
-    }
-
-    /**
-     * 특정 사용자의 세션 존재 여부 확인
-     */
-    public boolean hasActiveSession(String mbrId) {
-        HttpSession session = sessionMap.get(mbrId);
-        if (session == null) {
-            return false;
-        }
-
-        try {
-            // 세션이 유효한지 확인
-            session.getAttribute("mbrId");
-            return true;
-        } catch (IllegalStateException e) {
-            // 세션이 무효화됨
-            sessionMap.remove(mbrId);
-            return false;
-        }
-    }
-
-    /**
-     * Redis에서 세션 정보 확인
-     */
-    public boolean isValidSessionInRedis(String mbrId, String sessionId) {
-        try {
-            String sessionKey = "session:" + mbrId;
-            String storedSessionId = redisTemplate.opsForValue().get(sessionKey);
-            return storedSessionId != null && storedSessionId.equals(sessionId);
-        } catch (Exception e) {
-            return false;
         }
     }
 }
(파일 끝에 줄바꿈 문자 없음)
Add a comment
List