
Merge branch 'master' of http://210.180.118.83/jhpark/cms_backend
@fec68772e0755c84d9c20382117f28f70188f35a
--- src/main/java/com/takensoft/cms/mber/service/Impl/LgnHstryServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/LgnHstryServiceImpl.java
... | ... | @@ -1,37 +1,49 @@ |
1 | 1 |
package com.takensoft.cms.mber.service.Impl; |
2 | 2 |
|
3 |
- |
|
4 | 3 |
import com.takensoft.cms.codeManage.service.CodeManageService; |
5 | 4 |
import com.takensoft.cms.codeManage.vo.CodeManageVO; |
6 | 5 |
import com.takensoft.cms.mber.dao.LgnHstryDAO; |
7 | 6 |
import com.takensoft.cms.mber.service.LgnHstryService; |
8 | 7 |
import com.takensoft.cms.mber.vo.LgnHstryVO; |
8 |
+import com.takensoft.cms.mber.vo.MberVO; |
|
9 | 9 |
import com.takensoft.common.Pagination; |
10 | 10 |
import com.takensoft.common.exception.CustomInsertFailException; |
11 |
+import com.takensoft.common.util.HttpRequestUtil; |
|
11 | 12 |
import lombok.RequiredArgsConstructor; |
13 |
+import lombok.extern.slf4j.Slf4j; |
|
12 | 14 |
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; |
13 | 15 |
import org.springframework.dao.DataAccessException; |
14 | 16 |
import org.springframework.stereotype.Service; |
15 | 17 |
|
18 |
+import jakarta.servlet.http.HttpServletRequest; |
|
16 | 19 |
import java.util.HashMap; |
17 | 20 |
import java.util.List; |
21 |
+ |
|
18 | 22 |
/** |
19 | 23 |
* @author takensoft |
20 | 24 |
* @since 2024.04.09 |
21 | 25 |
* @modification |
22 | 26 |
* since | author | description |
23 | 27 |
* 2024.04.09 | takensoft | 최초 등록 |
28 |
+ * 2025.06.18 | takensoft | 로그인 이력 저장 로직 통합 및 중복 제거 |
|
24 | 29 |
* |
25 |
- * 로그인 이력 정보 관련 구현체 |
|
30 |
+ * 로그인 이력 정보 관련 구현체 - 로그인 이력 저장 로직 통합 |
|
26 | 31 |
* EgovAbstractServiceImpl : 전자정부 상속 |
27 | 32 |
* LgnHstryService : 로그인 이력 정보 인터페이스 상속 |
28 |
- * |
|
29 | 33 |
*/ |
30 | 34 |
@Service("lgnHstryService") |
31 | 35 |
@RequiredArgsConstructor |
36 |
+@Slf4j |
|
32 | 37 |
public class LgnHstryServiceImpl extends EgovAbstractServiceImpl implements LgnHstryService { |
38 |
+ |
|
33 | 39 |
private final LgnHstryDAO lgnHstryDAO; |
34 | 40 |
private final CodeManageService codeManageService; |
41 |
+ private final HttpRequestUtil httpRequestUtil; |
|
42 |
+ |
|
43 |
+ // 로그인 타입 상수 정의 |
|
44 |
+ private static final String LOGIN_TYPE_ADMIN = "0"; |
|
45 |
+ private static final String LOGIN_TYPE_USER = "1"; |
|
46 |
+ private static final String ROLE_ADMIN = "ROLE_ADMIN"; |
|
35 | 47 |
|
36 | 48 |
/** |
37 | 49 |
* @param lgnHstryVO - 로그인 이력 정보 |
... | ... | @@ -40,10 +52,10 @@ |
40 | 52 |
* @throws DataAccessException - db 관련 예외 발생 시 |
41 | 53 |
* @throws Exception - 그 외 예외 발생 시 |
42 | 54 |
* |
43 |
- * 로그인 이력 등록 |
|
55 |
+ * 로그인 이력 등록 (기존 메서드) |
|
44 | 56 |
*/ |
45 | 57 |
@Override |
46 |
- public int LgnHstrySave(LgnHstryVO lgnHstryVO){ |
|
58 |
+ public int LgnHstrySave(LgnHstryVO lgnHstryVO) { |
|
47 | 59 |
try { |
48 | 60 |
int result = lgnHstryDAO.save(lgnHstryVO); |
49 | 61 |
if (result == 0) { |
... | ... | @@ -59,6 +71,130 @@ |
59 | 71 |
} |
60 | 72 |
|
61 | 73 |
/** |
74 |
+ * @param mber - 회원 정보 |
|
75 |
+ * @param request - HTTP 요청 객체 |
|
76 |
+ * @param loginType - 로그인 타입 ("SYSTEM", "OAUTH2", etc.) |
|
77 |
+ * @return int - 로그인 이력 저장 결과 |
|
78 |
+ * @throws CustomInsertFailException - 로그인 이력 등록 실패 예외 발생 시 |
|
79 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
80 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
81 |
+ * |
|
82 |
+ * 통합 로그인 이력 저장 메서드 - 모든 로그인 방식에서 사용 |
|
83 |
+ */ |
|
84 |
+ @Override |
|
85 |
+ public int saveLoginHistory(MberVO mber, HttpServletRequest request, String loginType) { |
|
86 |
+ try { |
|
87 |
+ if (mber == null) { |
|
88 |
+ throw new IllegalArgumentException("회원 정보가 null입니다."); |
|
89 |
+ } |
|
90 |
+ |
|
91 |
+ if (request == null) { |
|
92 |
+ throw new IllegalArgumentException("HTTP 요청 정보가 null입니다."); |
|
93 |
+ } |
|
94 |
+ |
|
95 |
+ // 로그인 이력 VO 생성 |
|
96 |
+ LgnHstryVO loginHistory = createLoginHistoryVO(mber, request, loginType); |
|
97 |
+ |
|
98 |
+ // 로그인 이력 저장 |
|
99 |
+ int result = lgnHstryDAO.save(loginHistory); |
|
100 |
+ if (result == 0) { |
|
101 |
+ throw new CustomInsertFailException("로그인 이력 등록에 실패했습니다."); |
|
102 |
+ } |
|
103 |
+ |
|
104 |
+ log.info("로그인 이력 저장 완료 - 사용자: {}, 타입: {}, IP: {}", |
|
105 |
+ mber.getLgnId(), loginType, loginHistory.getCntnIp()); |
|
106 |
+ |
|
107 |
+ return result; |
|
108 |
+ |
|
109 |
+ } catch (DataAccessException dae) { |
|
110 |
+ log.error("로그인 이력 저장 실패 - DB 오류: {}", dae.getMessage(), dae); |
|
111 |
+ throw dae; |
|
112 |
+ } catch (Exception e) { |
|
113 |
+ log.error("로그인 이력 저장 실패 - 사용자: {}, 오류: {}", |
|
114 |
+ mber != null ? mber.getLgnId() : "unknown", e.getMessage(), e); |
|
115 |
+ throw e; |
|
116 |
+ } |
|
117 |
+ } |
|
118 |
+ |
|
119 |
+ /** |
|
120 |
+ * @param mber - 회원 정보 |
|
121 |
+ * @param request - HTTP 요청 객체 |
|
122 |
+ * @param loginType - 로그인 타입 |
|
123 |
+ * @return LgnHstryVO - 생성된 로그인 이력 VO |
|
124 |
+ * |
|
125 |
+ * 로그인 이력 VO 생성 |
|
126 |
+ */ |
|
127 |
+ private LgnHstryVO createLoginHistoryVO(MberVO mber, HttpServletRequest request, String loginType) { |
|
128 |
+ // User-Agent 정보 추출 |
|
129 |
+ String userAgent = httpRequestUtil.getUserAgent(request); |
|
130 |
+ |
|
131 |
+ // 관리자/사용자 구분 |
|
132 |
+ String lgnType = determineLoginType(mber); |
|
133 |
+ |
|
134 |
+ // 로그인 이력 VO 생성 |
|
135 |
+ LgnHstryVO loginHistory = new LgnHstryVO(); |
|
136 |
+ loginHistory.setLgnId(mber.getLgnId()); |
|
137 |
+ loginHistory.setLgnType(lgnType); |
|
138 |
+ loginHistory.setCntnIp(httpRequestUtil.getIp(request)); |
|
139 |
+ loginHistory.setCntnOperSysm(httpRequestUtil.getOS(userAgent)); |
|
140 |
+ loginHistory.setDvcNm(httpRequestUtil.getDevice(userAgent)); |
|
141 |
+ loginHistory.setBrwsrNm(httpRequestUtil.getBrowser(userAgent)); |
|
142 |
+ |
|
143 |
+ log.debug("로그인 이력 VO 생성 완료 - 사용자: {}, 타입: {}, OS: {}, 디바이스: {}, 브라우저: {}", |
|
144 |
+ mber.getLgnId(), lgnType, loginHistory.getCntnOperSysm(), |
|
145 |
+ loginHistory.getDvcNm(), loginHistory.getBrwsrNm()); |
|
146 |
+ |
|
147 |
+ return loginHistory; |
|
148 |
+ } |
|
149 |
+ |
|
150 |
+ /** |
|
151 |
+ * @param mber - 회원 정보 |
|
152 |
+ * @return String - 로그인 타입 ("0": 관리자, "1": 사용자) |
|
153 |
+ * |
|
154 |
+ * 사용자 권한에 따른 로그인 타입 결정 |
|
155 |
+ */ |
|
156 |
+ private String determineLoginType(MberVO mber) { |
|
157 |
+ if (mber.getAuthorities() != null) { |
|
158 |
+ boolean isAdmin = mber.getAuthorities().stream() |
|
159 |
+ .anyMatch(authority -> ROLE_ADMIN.equals(authority.getAuthority())); |
|
160 |
+ return isAdmin ? LOGIN_TYPE_ADMIN : LOGIN_TYPE_USER; |
|
161 |
+ } |
|
162 |
+ |
|
163 |
+ // 권한 정보가 없으면 기본적으로 사용자로 분류 |
|
164 |
+ log.warn("사용자 권한 정보가 없습니다. 기본값(사용자)으로 설정 - 사용자: {}", mber.getLgnId()); |
|
165 |
+ return LOGIN_TYPE_USER; |
|
166 |
+ } |
|
167 |
+ |
|
168 |
+ /** |
|
169 |
+ * @param mber - 회원 정보 |
|
170 |
+ * @param request - HTTP 요청 객체 |
|
171 |
+ * @return int - 로그인 이력 저장 결과 |
|
172 |
+ * |
|
173 |
+ * 시스템 로그인 이력 저장 (편의 메서드) |
|
174 |
+ */ |
|
175 |
+ @Override |
|
176 |
+ public int saveSystemLoginHistory(MberVO mber, HttpServletRequest request) { |
|
177 |
+ return saveLoginHistory(mber, request, "SYSTEM"); |
|
178 |
+ } |
|
179 |
+ |
|
180 |
+ /** |
|
181 |
+ * @param mber - 회원 정보 |
|
182 |
+ * @param request - HTTP 요청 객체 |
|
183 |
+ * @param provider - OAuth2 제공자 (kakao, naver, google 등) |
|
184 |
+ * @return int - 로그인 이력 저장 결과 |
|
185 |
+ * |
|
186 |
+ * OAuth2 로그인 이력 저장 (편의 메서드) |
|
187 |
+ */ |
|
188 |
+ @Override |
|
189 |
+ public int saveOAuth2LoginHistory(MberVO mber, HttpServletRequest request, String provider) { |
|
190 |
+ String loginType = "OAUTH2"; |
|
191 |
+ if (provider != null && !provider.trim().isEmpty()) { |
|
192 |
+ loginType = "OAUTH2_" + provider.toUpperCase(); |
|
193 |
+ } |
|
194 |
+ return saveLoginHistory(mber, request, loginType); |
|
195 |
+ } |
|
196 |
+ |
|
197 |
+ /** |
|
62 | 198 |
* @param params -회원정보 |
63 | 199 |
* @return HashMap<String, Object> |
64 | 200 |
* - list : 로그인 이력 목록 |
... | ... | @@ -67,10 +203,10 @@ |
67 | 203 |
* @throws DataAccessException - db 관련 예외 발생 시 |
68 | 204 |
* @throws Exception - 그 외 예외 발생 시 |
69 | 205 |
* |
70 |
- * 로그인 이력 목록 조회 |
|
206 |
+ * 로그인 이력 목록 조회 (기존 메서드) |
|
71 | 207 |
*/ |
72 | 208 |
@Override |
73 |
- public HashMap<String, Object> lgnHstryList(HashMap<String, String> params){ |
|
209 |
+ public HashMap<String, Object> lgnHstryList(HashMap<String, String> params) { |
|
74 | 210 |
try { |
75 | 211 |
Pagination search = new Pagination(0, params); |
76 | 212 |
int cnt = lgnHstryDAO.selectLgnHstryListCnt(search); |
--- src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
... | ... | @@ -160,7 +160,7 @@ |
160 | 160 |
} |
161 | 161 |
|
162 | 162 |
/** |
163 |
- * 표준 회원가입 처리 (기존 로직) |
|
163 |
+ * 표준 회원가입 처리 |
|
164 | 164 |
*/ |
165 | 165 |
private HashMap<String, Object> performStandardJoin(HttpServletRequest req, JoinDTO joinDTO) { |
166 | 166 |
// 회원 아이디 생성 (이미 설정된 경우 건너뛰기) |
--- src/main/java/com/takensoft/cms/mber/service/Impl/UnifiedLoginServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/UnifiedLoginServiceImpl.java
... | ... | @@ -12,7 +12,11 @@ |
12 | 12 |
import lombok.extern.slf4j.Slf4j; |
13 | 13 |
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; |
14 | 14 |
import org.springframework.dao.DataAccessException; |
15 |
+import org.springframework.security.core.Authentication; |
|
15 | 16 |
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
17 |
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; |
|
18 |
+import org.springframework.security.oauth2.core.oidc.user.OidcUser; |
|
19 |
+import org.springframework.security.oauth2.core.user.OAuth2User; |
|
16 | 20 |
import org.springframework.stereotype.Service; |
17 | 21 |
import org.springframework.transaction.annotation.Transactional; |
18 | 22 |
|
... | ... | @@ -20,6 +24,7 @@ |
20 | 24 |
import java.util.ArrayList; |
21 | 25 |
import java.util.HashMap; |
22 | 26 |
import java.util.List; |
27 |
+import java.util.Map; |
|
23 | 28 |
|
24 | 29 |
/** |
25 | 30 |
* @author takensoft |
... | ... | @@ -27,8 +32,9 @@ |
27 | 32 |
* @modification |
28 | 33 |
* since | author | description |
29 | 34 |
* 2025.05.29 | takensoft | 최초 등록 |
35 |
+ * 2025.06.18 | takensoft | OAuth 사용자 정보 추출 로직 통합 |
|
30 | 36 |
* |
31 |
- * 통합 로그인 서비스 구현체 - 순환 의존성 해결 |
|
37 |
+ * 통합 로그인 서비스 구현체 |
|
32 | 38 |
* EgovAbstractServiceImpl : 전자정부 상속 |
33 | 39 |
* UnifiedLoginService : 통합 로그인 인터페이스 상속 |
34 | 40 |
*/ |
... | ... | @@ -45,6 +51,35 @@ |
45 | 51 |
private BCryptPasswordEncoder passwordEncoder; |
46 | 52 |
|
47 | 53 |
/** |
54 |
+ * OAuth2 사용자 정보를 담는 내부 클래스 |
|
55 |
+ */ |
|
56 |
+ public static class OAuth2UserInfo { |
|
57 |
+ private final String provider; |
|
58 |
+ private final String id; |
|
59 |
+ private final String name; |
|
60 |
+ private final String email; |
|
61 |
+ |
|
62 |
+ public OAuth2UserInfo(String provider, String id, String name, String email) { |
|
63 |
+ this.provider = provider; |
|
64 |
+ this.id = id; |
|
65 |
+ this.name = name; |
|
66 |
+ this.email = email; |
|
67 |
+ } |
|
68 |
+ |
|
69 |
+ // Getters |
|
70 |
+ public String getProvider() { return provider; } |
|
71 |
+ public String getId() { return id; } |
|
72 |
+ public String getName() { return name; } |
|
73 |
+ public String getEmail() { return email; } |
|
74 |
+ |
|
75 |
+ @Override |
|
76 |
+ public String toString() { |
|
77 |
+ return String.format("OAuth2UserInfo{provider='%s', id='%s', name='%s', email='%s'}", |
|
78 |
+ provider, id, name, email); |
|
79 |
+ } |
|
80 |
+ } |
|
81 |
+ |
|
82 |
+ /** |
|
48 | 83 |
* BCryptPasswordEncoder 지연 초기화 (순환 의존성 해결) |
49 | 84 |
*/ |
50 | 85 |
private BCryptPasswordEncoder getPasswordEncoder() { |
... | ... | @@ -53,6 +88,249 @@ |
53 | 88 |
} |
54 | 89 |
return passwordEncoder; |
55 | 90 |
} |
91 |
+ |
|
92 |
+ /** |
|
93 |
+ * @param authentication - Spring Security Authentication 객체 |
|
94 |
+ * @return OAuth2UserInfo - 추출된 사용자 정보 |
|
95 |
+ * |
|
96 |
+ * Authentication 객체에서 OAuth2 사용자 정보를 추출하는 통합 메서드 |
|
97 |
+ */ |
|
98 |
+ public OAuth2UserInfo extractUserInfoFromAuthentication(Authentication authentication) { |
|
99 |
+ if (authentication == null) { |
|
100 |
+ throw new IllegalArgumentException("Authentication 객체가 null입니다."); |
|
101 |
+ } |
|
102 |
+ |
|
103 |
+ try { |
|
104 |
+ // 제공자 결정 |
|
105 |
+ String provider = determineProvider(authentication); |
|
106 |
+ |
|
107 |
+ // Principal 타입에 따른 정보 추출 |
|
108 |
+ Object principal = authentication.getPrincipal(); |
|
109 |
+ |
|
110 |
+ if (principal instanceof OidcUser) { |
|
111 |
+ return extractOidcUserInfo((OidcUser) principal, provider); |
|
112 |
+ } else if (principal instanceof OAuth2User) { |
|
113 |
+ return extractOAuth2UserInfo((OAuth2User) principal, provider); |
|
114 |
+ } else { |
|
115 |
+ throw new IllegalArgumentException("지원하지 않는 Principal 타입: " + principal.getClass().getName()); |
|
116 |
+ } |
|
117 |
+ |
|
118 |
+ } catch (Exception e) { |
|
119 |
+ log.error("OAuth2 사용자 정보 추출 실패: {}", e.getMessage(), e); |
|
120 |
+ throw new CustomNotFoundException("OAuth2 사용자 정보 추출에 실패했습니다: " + e.getMessage()); |
|
121 |
+ } |
|
122 |
+ } |
|
123 |
+ |
|
124 |
+ /** |
|
125 |
+ * @param attributes - OAuth2 제공자에서 받은 사용자 정보 |
|
126 |
+ * @param provider - OAuth2 제공자 (kakao, naver, google 등) |
|
127 |
+ * @return OAuth2UserInfo - 추출된 사용자 정보 |
|
128 |
+ * |
|
129 |
+ * Map 형태의 attributes에서 OAuth2 사용자 정보를 추출하는 메서드 |
|
130 |
+ */ |
|
131 |
+ public OAuth2UserInfo extractUserInfoFromAttributes(Map<String, Object> attributes, String provider) { |
|
132 |
+ if (attributes == null || attributes.isEmpty()) { |
|
133 |
+ throw new IllegalArgumentException("OAuth2 사용자 정보가 비어있습니다."); |
|
134 |
+ } |
|
135 |
+ |
|
136 |
+ if (provider == null || provider.trim().isEmpty()) { |
|
137 |
+ throw new IllegalArgumentException("OAuth2 제공자 정보가 없습니다."); |
|
138 |
+ } |
|
139 |
+ |
|
140 |
+ try { |
|
141 |
+ String normalizedProvider = provider.toLowerCase(); |
|
142 |
+ |
|
143 |
+ switch (normalizedProvider) { |
|
144 |
+ case "kakao": |
|
145 |
+ return extractKakaoUserInfo(attributes); |
|
146 |
+ case "naver": |
|
147 |
+ return extractNaverUserInfo(attributes); |
|
148 |
+ case "google": |
|
149 |
+ return extractGoogleUserInfo(attributes); |
|
150 |
+ default: |
|
151 |
+ throw new IllegalArgumentException("지원하지 않는 OAuth2 제공자: " + provider); |
|
152 |
+ } |
|
153 |
+ |
|
154 |
+ } catch (Exception e) { |
|
155 |
+ log.error("OAuth2 사용자 정보 추출 실패 - Provider: {}, Error: {}", provider, e.getMessage(), e); |
|
156 |
+ throw new CustomNotFoundException("OAuth2 사용자 정보 추출에 실패했습니다: " + e.getMessage()); |
|
157 |
+ } |
|
158 |
+ } |
|
159 |
+ |
|
160 |
+ /** |
|
161 |
+ * OIDC 사용자 정보 추출 (주로 구글) |
|
162 |
+ */ |
|
163 |
+ private OAuth2UserInfo extractOidcUserInfo(OidcUser oidcUser, String provider) { |
|
164 |
+ try { |
|
165 |
+ String id = oidcUser.getSubject(); // OIDC의 subject가 사용자 ID |
|
166 |
+ String name = getValidString(oidcUser.getFullName()); |
|
167 |
+ |
|
168 |
+ // 이름이 없으면 given name 시도 |
|
169 |
+ if (name == null) { |
|
170 |
+ name = getValidString(oidcUser.getGivenName()); |
|
171 |
+ } |
|
172 |
+ |
|
173 |
+ // 여전히 없으면 이메일 사용 |
|
174 |
+ if (name == null) { |
|
175 |
+ name = getValidString(oidcUser.getEmail()); |
|
176 |
+ } |
|
177 |
+ |
|
178 |
+ String email = getValidString(oidcUser.getEmail()); |
|
179 |
+ |
|
180 |
+ validateRequiredFields(id, email, provider); |
|
181 |
+ |
|
182 |
+ return new OAuth2UserInfo(provider, id, name, email); |
|
183 |
+ |
|
184 |
+ } catch (Exception e) { |
|
185 |
+ log.error("OIDC 사용자 정보 추출 실패: {}", e.getMessage(), e); |
|
186 |
+ throw new CustomNotFoundException("OIDC 사용자 정보 추출에 실패했습니다."); |
|
187 |
+ } |
|
188 |
+ } |
|
189 |
+ |
|
190 |
+ /** |
|
191 |
+ * 일반 OAuth2 사용자 정보 추출 |
|
192 |
+ */ |
|
193 |
+ private OAuth2UserInfo extractOAuth2UserInfo(OAuth2User oauth2User, String provider) { |
|
194 |
+ Map<String, Object> attributes = oauth2User.getAttributes(); |
|
195 |
+ return extractUserInfoFromAttributes(attributes, provider); |
|
196 |
+ } |
|
197 |
+ |
|
198 |
+ /** |
|
199 |
+ * 카카오 사용자 정보 추출 |
|
200 |
+ */ |
|
201 |
+ private OAuth2UserInfo extractKakaoUserInfo(Map<String, Object> attributes) { |
|
202 |
+ try { |
|
203 |
+ String id = String.valueOf(attributes.get("id")); |
|
204 |
+ String name = null; |
|
205 |
+ String email = null; |
|
206 |
+ |
|
207 |
+ // kakao_account에서 정보 추출 |
|
208 |
+ Map<String, Object> kakaoAccount = safeCastToMap(attributes.get("kakao_account")); |
|
209 |
+ if (kakaoAccount != null) { |
|
210 |
+ email = getValidString((String) kakaoAccount.get("email")); |
|
211 |
+ |
|
212 |
+ // profile에서 닉네임 추출 |
|
213 |
+ Map<String, Object> profile = safeCastToMap(kakaoAccount.get("profile")); |
|
214 |
+ if (profile != null) { |
|
215 |
+ name = getValidString((String) profile.get("nickname")); |
|
216 |
+ } |
|
217 |
+ } |
|
218 |
+ |
|
219 |
+ validateRequiredFields(id, email, "kakao"); |
|
220 |
+ |
|
221 |
+ return new OAuth2UserInfo("kakao", id, name, email); |
|
222 |
+ |
|
223 |
+ } catch (Exception e) { |
|
224 |
+ log.error("카카오 사용자 정보 추출 실패: {}", e.getMessage(), e); |
|
225 |
+ throw new CustomNotFoundException("카카오 사용자 정보 추출에 실패했습니다."); |
|
226 |
+ } |
|
227 |
+ } |
|
228 |
+ |
|
229 |
+ /** |
|
230 |
+ * 네이버 사용자 정보 추출 |
|
231 |
+ */ |
|
232 |
+ private OAuth2UserInfo extractNaverUserInfo(Map<String, Object> attributes) { |
|
233 |
+ try { |
|
234 |
+ // 네이버는 response 객체 안에 사용자 정보가 있음 |
|
235 |
+ Map<String, Object> naverResponse = safeCastToMap(attributes.get("response")); |
|
236 |
+ if (naverResponse == null) { |
|
237 |
+ throw new IllegalArgumentException("네이버 응답에서 response 객체를 찾을 수 없습니다."); |
|
238 |
+ } |
|
239 |
+ |
|
240 |
+ String id = getValidString((String) naverResponse.get("id")); |
|
241 |
+ String name = getValidString((String) naverResponse.get("name")); |
|
242 |
+ String email = getValidString((String) naverResponse.get("email")); |
|
243 |
+ |
|
244 |
+ validateRequiredFields(id, email, "naver"); |
|
245 |
+ |
|
246 |
+ return new OAuth2UserInfo("naver", id, name, email); |
|
247 |
+ |
|
248 |
+ } catch (Exception e) { |
|
249 |
+ log.error("네이버 사용자 정보 추출 실패: {}", e.getMessage(), e); |
|
250 |
+ throw new CustomNotFoundException("네이버 사용자 정보 추출에 실패했습니다."); |
|
251 |
+ } |
|
252 |
+ } |
|
253 |
+ |
|
254 |
+ /** |
|
255 |
+ * 구글 사용자 정보 추출 |
|
256 |
+ */ |
|
257 |
+ private OAuth2UserInfo extractGoogleUserInfo(Map<String, Object> attributes) { |
|
258 |
+ try { |
|
259 |
+ String id = getValidString((String) attributes.get("sub")); |
|
260 |
+ if (id == null) { |
|
261 |
+ id = getValidString((String) attributes.get("id")); |
|
262 |
+ } |
|
263 |
+ |
|
264 |
+ String name = getValidString((String) attributes.get("name")); |
|
265 |
+ String email = getValidString((String) attributes.get("email")); |
|
266 |
+ |
|
267 |
+ validateRequiredFields(id, email, "google"); |
|
268 |
+ |
|
269 |
+ return new OAuth2UserInfo("google", id, name, email); |
|
270 |
+ |
|
271 |
+ } catch (Exception e) { |
|
272 |
+ log.error("구글 사용자 정보 추출 실패: {}", e.getMessage(), e); |
|
273 |
+ throw new CustomNotFoundException("구글 사용자 정보 추출에 실패했습니다."); |
|
274 |
+ } |
|
275 |
+ } |
|
276 |
+ |
|
277 |
+ /** |
|
278 |
+ * 제공자 결정 로직 |
|
279 |
+ */ |
|
280 |
+ private String determineProvider(Authentication authentication) { |
|
281 |
+ if (authentication instanceof OAuth2AuthenticationToken) { |
|
282 |
+ OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) authentication; |
|
283 |
+ return oauth2Token.getAuthorizedClientRegistrationId().toLowerCase(); |
|
284 |
+ } |
|
285 |
+ |
|
286 |
+ // Principal 이름에서 추론 시도 |
|
287 |
+ String name = authentication.getName(); |
|
288 |
+ if (name != null) { |
|
289 |
+ String lowerName = name.toLowerCase(); |
|
290 |
+ if (lowerName.contains("google")) return "google"; |
|
291 |
+ if (lowerName.contains("kakao")) return "kakao"; |
|
292 |
+ if (lowerName.contains("naver")) return "naver"; |
|
293 |
+ } |
|
294 |
+ |
|
295 |
+ // 기본값 |
|
296 |
+ log.warn("제공자를 결정할 수 없어 기본값(google)을 사용합니다. Authentication: {}", authentication.getClass().getName()); |
|
297 |
+ return "google"; |
|
298 |
+ } |
|
299 |
+ |
|
300 |
+ /** |
|
301 |
+ * 안전한 Map 캐스팅 |
|
302 |
+ */ |
|
303 |
+ @SuppressWarnings("unchecked") |
|
304 |
+ private Map<String, Object> safeCastToMap(Object obj) { |
|
305 |
+ if (obj instanceof Map) { |
|
306 |
+ return (Map<String, Object>) obj; |
|
307 |
+ } |
|
308 |
+ return null; |
|
309 |
+ } |
|
310 |
+ |
|
311 |
+ /** |
|
312 |
+ * 유효한 문자열 반환 (null, 빈 문자열, "null" 문자열 처리) |
|
313 |
+ */ |
|
314 |
+ private String getValidString(String value) { |
|
315 |
+ if (value == null || value.trim().isEmpty() || "null".equalsIgnoreCase(value.trim())) { |
|
316 |
+ return null; |
|
317 |
+ } |
|
318 |
+ return value.trim(); |
|
319 |
+ } |
|
320 |
+ |
|
321 |
+ /** |
|
322 |
+ * 필수 필드 검증 |
|
323 |
+ */ |
|
324 |
+ private void validateRequiredFields(String id, String email, String provider) { |
|
325 |
+ if (id == null || id.trim().isEmpty()) { |
|
326 |
+ throw new IllegalArgumentException(provider + " 사용자 ID가 없습니다."); |
|
327 |
+ } |
|
328 |
+ if (email == null || email.trim().isEmpty()) { |
|
329 |
+ throw new IllegalArgumentException(provider + " 이메일 정보가 없습니다."); |
|
330 |
+ } |
|
331 |
+ } |
|
332 |
+ |
|
333 |
+ // ===== 기존 UnifiedLoginService 메서드들 ===== |
|
56 | 334 |
|
57 | 335 |
/** |
58 | 336 |
* 통합 로그인 인증 |
... | ... | @@ -85,7 +363,7 @@ |
85 | 363 |
} |
86 | 364 |
|
87 | 365 |
/** |
88 |
- * OAuth2 사용자 처리 (가입 또는 연동) |
|
366 |
+ * OAuth2 사용자 처리 (가입 또는 연동) - 통합된 정보 추출 로직 사용 |
|
89 | 367 |
*/ |
90 | 368 |
@Override |
91 | 369 |
@Transactional(rollbackFor = Exception.class) |
... | ... | @@ -346,7 +624,7 @@ |
346 | 624 |
* 소셜 계정 정보 업데이트 |
347 | 625 |
*/ |
348 | 626 |
private void updateSocialAccountInfo(MberSocialAccountVO socialAccount) { |
349 |
- mberDAO.linkSocialAccount(socialAccount); |
|
627 |
+ mberDAO.linkSocialAccount(socialAccount); |
|
350 | 628 |
} |
351 | 629 |
|
352 | 630 |
/** |
--- src/main/java/com/takensoft/cms/mber/service/LgnHstryService.java
+++ src/main/java/com/takensoft/cms/mber/service/LgnHstryService.java
... | ... | @@ -1,17 +1,20 @@ |
1 | 1 |
package com.takensoft.cms.mber.service; |
2 | 2 |
|
3 | 3 |
import com.takensoft.cms.mber.vo.LgnHstryVO; |
4 |
-import org.springframework.dao.DataAccessException; |
|
4 |
+import com.takensoft.cms.mber.vo.MberVO; |
|
5 | 5 |
|
6 |
+import jakarta.servlet.http.HttpServletRequest; |
|
6 | 7 |
import java.util.HashMap; |
8 |
+ |
|
7 | 9 |
/** |
8 |
- * @author 박정하 |
|
10 |
+ * @author takensoft |
|
9 | 11 |
* @since 2024.04.09 |
10 | 12 |
* @modification |
11 | 13 |
* since | author | description |
12 |
- * 2024.04.09 | 박정하 | 최초 등록 |
|
14 |
+ * 2024.04.09 | takensoft | 최초 등록 |
|
15 |
+ * 2025.06.18 | takensoft | 로그인 이력 저장 메서드 추가 및 통합 |
|
13 | 16 |
* |
14 |
- * 로그인 이력 관련 인터페이스 |
|
17 |
+ * 로그인 이력 정보 관련 인터페이스 - 통합 로그인 이력 저장 기능 추가 |
|
15 | 18 |
*/ |
16 | 19 |
public interface LgnHstryService { |
17 | 20 |
|
... | ... | @@ -19,9 +22,38 @@ |
19 | 22 |
* @param lgnHstryVO - 로그인 이력 정보 |
20 | 23 |
* @return int - 로그인 이력 결과 |
21 | 24 |
* |
22 |
- * 로그인 이력 등록 |
|
25 |
+ * 로그인 이력 등록 (기존 메서드) |
|
23 | 26 |
*/ |
24 |
- public int LgnHstrySave(LgnHstryVO lgnHstryVO); |
|
27 |
+ int LgnHstrySave(LgnHstryVO lgnHstryVO); |
|
28 |
+ |
|
29 |
+ /** |
|
30 |
+ * @param mber - 회원 정보 |
|
31 |
+ * @param request - HTTP 요청 객체 |
|
32 |
+ * @param loginType - 로그인 타입 ("SYSTEM", "OAUTH2", etc.) |
|
33 |
+ * @return int - 로그인 이력 저장 결과 |
|
34 |
+ * |
|
35 |
+ * 통합 로그인 이력 저장 메서드 - 모든 로그인 방식에서 사용 |
|
36 |
+ */ |
|
37 |
+ int saveLoginHistory(MberVO mber, HttpServletRequest request, String loginType); |
|
38 |
+ |
|
39 |
+ /** |
|
40 |
+ * @param mber - 회원 정보 |
|
41 |
+ * @param request - HTTP 요청 객체 |
|
42 |
+ * @return int - 로그인 이력 저장 결과 |
|
43 |
+ * |
|
44 |
+ * 시스템 로그인 이력 저장 |
|
45 |
+ */ |
|
46 |
+ int saveSystemLoginHistory(MberVO mber, HttpServletRequest request); |
|
47 |
+ |
|
48 |
+ /** |
|
49 |
+ * @param mber - 회원 정보 |
|
50 |
+ * @param request - HTTP 요청 객체 |
|
51 |
+ * @param provider - OAuth2 제공자 (kakao, naver, google 등) |
|
52 |
+ * @return int - 로그인 이력 저장 결과 |
|
53 |
+ * |
|
54 |
+ * OAuth2 로그인 이력 저장 |
|
55 |
+ */ |
|
56 |
+ int saveOAuth2LoginHistory(MberVO mber, HttpServletRequest request, String provider); |
|
25 | 57 |
|
26 | 58 |
/** |
27 | 59 |
* @param params -회원정보 |
... | ... | @@ -32,5 +64,5 @@ |
32 | 64 |
* |
33 | 65 |
* 로그인 이력 목록 조회 |
34 | 66 |
*/ |
35 |
- public HashMap<String, Object> lgnHstryList(HashMap<String, String> params); |
|
67 |
+ HashMap<String, Object> lgnHstryList(HashMap<String, String> params); |
|
36 | 68 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/filter/JWTFilter.java
+++ src/main/java/com/takensoft/common/filter/JWTFilter.java
... | ... | @@ -99,50 +99,60 @@ |
99 | 99 |
throws ServletException, IOException { |
100 | 100 |
HttpSession session = request.getSession(false); |
101 | 101 |
|
102 |
- if (session == null) { |
|
103 |
- filterChain.doFilter(request, response); |
|
102 |
+ // 세션이 없거나 사용자 정보가 없으면 처리 |
|
103 |
+ if (session == null || session.getAttribute("mbrId") == null) { |
|
104 |
+ handleSessionInvalid(request, response, filterChain); |
|
104 | 105 |
return; |
105 | 106 |
} |
106 | 107 |
|
107 | 108 |
String mbrId = (String) session.getAttribute("mbrId"); |
108 |
- if (mbrId == null) { |
|
109 |
- filterChain.doFilter(request, response); |
|
110 |
- return; |
|
111 |
- } |
|
112 | 109 |
|
113 | 110 |
// 세션에서 JWT 토큰 꺼내기 |
114 | 111 |
String sessionToken = (String) session.getAttribute(SESSION_JWT_KEY); |
115 |
- if (sessionToken == null) { |
|
116 |
- // 토큰이 없어도 세션 자체는 유효할 수 있으므로 계속 진행 |
|
117 |
- filterChain.doFilter(request, response); |
|
118 |
- return; |
|
112 |
+ if (sessionToken != null) { |
|
113 |
+ try { |
|
114 |
+ // 토큰 만료 체크 |
|
115 |
+ Boolean isExpired = (Boolean) jwtUtil.getClaim("Bearer " + sessionToken, "isExpired"); |
|
116 |
+ if (isExpired != null && isExpired) { |
|
117 |
+ session.invalidate(); |
|
118 |
+ handleSessionInvalid(request, response, filterChain); |
|
119 |
+ return; |
|
120 |
+ } |
|
121 |
+ } catch (Exception e) { |
|
122 |
+ session.invalidate(); |
|
123 |
+ handleSessionInvalid(request, response, filterChain); |
|
124 |
+ return; |
|
125 |
+ } |
|
119 | 126 |
} |
120 | 127 |
|
121 | 128 |
// 중복 로그인 검증 (비허용 모드일 때만) |
122 |
- if (!loginPolicyService.getPolicy()) { |
|
129 |
+ if (!loginPolicyService.getPolicy() && sessionToken != null) { |
|
123 | 130 |
if (!validateSessionToken(mbrId, sessionToken, session)) { |
124 |
- sendTokenExpiredResponse(response, request); |
|
131 |
+ handleSessionInvalid(request, response, filterChain); |
|
125 | 132 |
return; |
126 | 133 |
} |
127 | 134 |
} |
128 | 135 |
|
129 | 136 |
// 세션에서 꺼낸 JWT 토큰을 헤더에 설정하고 JWT 검증 로직 재사용 |
130 |
- try { |
|
131 |
- HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(request) { |
|
132 |
- @Override |
|
133 |
- public String getHeader(String name) { |
|
134 |
- if (AUTHORIZATION_HEADER.equals(name)) { |
|
135 |
- return "Bearer " + sessionToken; |
|
137 |
+ if (sessionToken != null) { |
|
138 |
+ try { |
|
139 |
+ HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(request) { |
|
140 |
+ @Override |
|
141 |
+ public String getHeader(String name) { |
|
142 |
+ if (AUTHORIZATION_HEADER.equals(name)) { |
|
143 |
+ return "Bearer " + sessionToken; |
|
144 |
+ } |
|
145 |
+ return super.getHeader(name); |
|
136 | 146 |
} |
137 |
- return super.getHeader(name); |
|
138 |
- } |
|
139 |
- }; |
|
147 |
+ }; |
|
140 | 148 |
|
141 |
- // 기존 JWT 검증 로직 재사용 (단, 재귀 호출 방지) |
|
142 |
- processJwtToken(wrappedRequest, response, filterChain, sessionToken); |
|
143 |
- |
|
144 |
- } catch (Exception e) { |
|
145 |
- // JWT 토큰에 문제가 있어도 세션은 유효할 수 있으므로 계속 진행 |
|
149 |
+ // 기존 JWT 검증 로직 재사용 (단, 재귀 호출 방지) |
|
150 |
+ processJwtToken(wrappedRequest, response, filterChain, sessionToken); |
|
151 |
+ } catch (Exception e) { |
|
152 |
+ session.invalidate(); |
|
153 |
+ handleSessionInvalid(request, response, filterChain); |
|
154 |
+ } |
|
155 |
+ } else { |
|
146 | 156 |
filterChain.doFilter(request, response); |
147 | 157 |
} |
148 | 158 |
} |
... | ... | @@ -217,7 +227,7 @@ |
217 | 227 |
} |
218 | 228 |
|
219 | 229 |
/** |
220 |
- * JWT 토큰 유효성 검증 (개선된 로직) |
|
230 |
+ * JWT 토큰 유효성 검증 |
|
221 | 231 |
*/ |
222 | 232 |
private boolean validateJwtToken(String mbrId, String accessToken) { |
223 | 233 |
try { |
... | ... | @@ -303,4 +313,21 @@ |
303 | 313 |
response.setStatus(HttpStatus.UNAUTHORIZED.value()); |
304 | 314 |
response.getOutputStream().write(appConfig.getObjectMapper().writeValueAsBytes(errorResponse)); |
305 | 315 |
} |
316 |
+ |
|
317 |
+ private void handleSessionInvalid(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
|
318 |
+ throws IOException, ServletException { |
|
319 |
+ if (isApiRequest(request)) { |
|
320 |
+ sendTokenExpiredResponse(response, request); |
|
321 |
+ } else { |
|
322 |
+ response.sendRedirect("/login.page"); |
|
323 |
+ } |
|
324 |
+ } |
|
325 |
+ |
|
326 |
+ private boolean isApiRequest(HttpServletRequest request) { |
|
327 |
+ String requestURI = request.getRequestURI(); |
|
328 |
+ String acceptHeader = request.getHeader("Accept"); |
|
329 |
+ |
|
330 |
+ return requestURI.endsWith(".json") || |
|
331 |
+ (acceptHeader != null && acceptHeader.contains("application/json")); |
|
332 |
+ } |
|
306 | 333 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
... | ... | @@ -4,10 +4,8 @@ |
4 | 4 |
import com.takensoft.cms.loginPolicy.service.LoginPolicyService; |
5 | 5 |
import com.takensoft.cms.mber.service.LgnHstryService; |
6 | 6 |
import com.takensoft.cms.mber.service.UnifiedLoginService; |
7 |
-import com.takensoft.cms.mber.vo.LgnHstryVO; |
|
7 |
+import com.takensoft.cms.mber.service.Impl.UnifiedLoginServiceImpl; |
|
8 | 8 |
import com.takensoft.cms.mber.vo.MberVO; |
9 |
-import com.takensoft.common.oauth.vo.CustomOAuth2UserVO; |
|
10 |
-import com.takensoft.common.util.HttpRequestUtil; |
|
11 | 9 |
import com.takensoft.common.util.LoginUtil; |
12 | 10 |
import jakarta.servlet.ServletException; |
13 | 11 |
import jakarta.servlet.http.HttpServletRequest; |
... | ... | @@ -16,15 +14,11 @@ |
16 | 14 |
import lombok.extern.slf4j.Slf4j; |
17 | 15 |
import org.springframework.beans.factory.annotation.Value; |
18 | 16 |
import org.springframework.security.core.Authentication; |
19 |
-import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; |
|
20 |
-import org.springframework.security.oauth2.core.oidc.user.OidcUser; |
|
21 |
-import org.springframework.security.oauth2.core.user.OAuth2User; |
|
22 | 17 |
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; |
23 | 18 |
import org.springframework.stereotype.Component; |
24 | 19 |
|
25 | 20 |
import java.io.IOException; |
26 | 21 |
import java.net.URLEncoder; |
27 |
-import java.util.Map; |
|
28 | 22 |
|
29 | 23 |
/** |
30 | 24 |
* @author takensoft |
... | ... | @@ -36,8 +30,9 @@ |
36 | 30 |
* 2025.05.29 | takensoft | OAuth2 통합 문제 해결 |
37 | 31 |
* 2025.06.02 | takensoft | 세션 모드 중복로그인 처리 개선 |
38 | 32 |
* 2025.06.09 | takensoft | OIDC 타입 캐스팅 문제 해결 |
33 |
+ * 2025.06.18 | takensoft | OAuth 사용자 정보 추출 및 로그인 이력 저장 로직 통합 |
|
39 | 34 |
* |
40 |
- * OAuth2 로그인 성공 핸들러 |
|
35 |
+ * OAuth2 로그인 성공 핸들러 - 최종 중복 제거 완료 |
|
41 | 36 |
*/ |
42 | 37 |
@Slf4j |
43 | 38 |
@Component |
... | ... | @@ -46,7 +41,6 @@ |
46 | 41 |
|
47 | 42 |
private final UnifiedLoginService unifiedLoginService; |
48 | 43 |
private final LgnHstryService lgnHstryService; |
49 |
- private final HttpRequestUtil httpRequestUtil; |
|
50 | 44 |
private final LoginUtil loginUtil; |
51 | 45 |
private final LoginModeService loginModeService; |
52 | 46 |
private final LoginPolicyService loginPolicyService; |
... | ... | @@ -57,15 +51,13 @@ |
57 | 51 |
@Override |
58 | 52 |
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { |
59 | 53 |
try { |
60 |
- |
|
61 |
- // OAuth2User 타입 확인 및 정보 추출 |
|
62 |
- OAuth2UserInfo userInfo = extractUserInfo(authentication); |
|
54 |
+ // 통합된 사용자 정보 추출 로직 사용 |
|
55 |
+ UnifiedLoginServiceImpl.OAuth2UserInfo userInfo = extractUserInfo(authentication); |
|
63 | 56 |
|
64 | 57 |
if (userInfo == null) { |
65 | 58 |
handleOAuth2Error(response, new Exception("사용자 정보 추출 실패")); |
66 | 59 |
return; |
67 | 60 |
} |
68 |
- |
|
69 | 61 |
|
70 | 62 |
// 현재 설정된 로그인 모드 확인 |
71 | 63 |
String currentLoginMode = loginModeService.getLoginMode(); |
... | ... | @@ -73,15 +65,15 @@ |
73 | 65 |
|
74 | 66 |
// 통합 로그인 서비스를 통한 OAuth2 사용자 처리 |
75 | 67 |
MberVO mber = unifiedLoginService.processOAuth2User( |
76 |
- userInfo.email, |
|
77 |
- unifiedLoginService.convertProviderToMbrType(userInfo.provider), |
|
78 |
- userInfo.id, |
|
79 |
- userInfo.name, |
|
68 |
+ userInfo.getEmail(), |
|
69 |
+ unifiedLoginService.convertProviderToMbrType(userInfo.getProvider()), |
|
70 |
+ userInfo.getId(), |
|
71 |
+ userInfo.getName(), |
|
80 | 72 |
request |
81 | 73 |
); |
82 | 74 |
|
83 |
- // OAuth2 로그인 이력 저장 |
|
84 |
- saveLoginHistory(request, mber, userInfo.provider); |
|
75 |
+ // 통합된 OAuth2 로그인 이력 저장 로직 사용 |
|
76 |
+ lgnHstryService.saveOAuth2LoginHistory(mber, request, userInfo.getProvider()); |
|
85 | 77 |
|
86 | 78 |
request.setAttribute("loginType", "OAUTH2"); |
87 | 79 |
|
... | ... | @@ -101,32 +93,14 @@ |
101 | 93 |
} |
102 | 94 |
|
103 | 95 |
/** |
104 |
- * OAuth2User에서 사용자 정보 추출 (개선된 버전) |
|
96 |
+ * OAuth2 사용자 정보 추출 - UnifiedLoginService의 통합 로직 사용 |
|
105 | 97 |
*/ |
106 |
- private OAuth2UserInfo extractUserInfo(Authentication authentication) { |
|
107 |
- Object principal = authentication.getPrincipal(); |
|
108 |
- String provider = determineProvider(authentication); |
|
109 |
- |
|
98 |
+ private UnifiedLoginServiceImpl.OAuth2UserInfo extractUserInfo(Authentication authentication) { |
|
110 | 99 |
try { |
111 |
- if (principal instanceof CustomOAuth2UserVO) { |
|
112 |
- // 커스텀 OAuth2 사용자 |
|
113 |
- CustomOAuth2UserVO customUser = (CustomOAuth2UserVO) principal; |
|
114 |
- return new OAuth2UserInfo( |
|
115 |
- customUser.getProvider(), |
|
116 |
- customUser.getId(), |
|
117 |
- customUser.getName(), |
|
118 |
- customUser.getEmail() |
|
119 |
- ); |
|
120 |
- |
|
121 |
- } else if (principal instanceof OidcUser) { |
|
122 |
- // OIDC 사용자 (구글) |
|
123 |
- OidcUser oidcUser = (OidcUser) principal; |
|
124 |
- return extractOidcUserInfo(oidcUser, provider); |
|
125 |
- |
|
126 |
- } else if (principal instanceof OAuth2User) { |
|
127 |
- // 일반 OAuth2 사용자 |
|
128 |
- OAuth2User oauth2User = (OAuth2User) principal; |
|
129 |
- return extractOAuth2UserInfo(oauth2User, provider); |
|
100 |
+ // UnifiedLoginService의 통합된 사용자 정보 추출 메서드 사용 |
|
101 |
+ if (unifiedLoginService instanceof UnifiedLoginServiceImpl) { |
|
102 |
+ UnifiedLoginServiceImpl unifiedLoginServiceImpl = (UnifiedLoginServiceImpl) unifiedLoginService; |
|
103 |
+ return unifiedLoginServiceImpl.extractUserInfoFromAuthentication(authentication); |
|
130 | 104 |
} else { |
131 | 105 |
return null; |
132 | 106 |
} |
... | ... | @@ -136,141 +110,12 @@ |
136 | 110 |
} |
137 | 111 |
|
138 | 112 |
/** |
139 |
- * OIDC 사용자 정보 추출 (구글) |
|
140 |
- */ |
|
141 |
- private OAuth2UserInfo extractOidcUserInfo(OidcUser oidcUser, String provider) { |
|
142 |
- try { |
|
143 |
- |
|
144 |
- String id = oidcUser.getSubject(); // OIDC의 subject가 사용자 ID |
|
145 |
- String name = oidcUser.getFullName(); |
|
146 |
- if (name == null || name.trim().isEmpty()) { |
|
147 |
- name = oidcUser.getGivenName(); |
|
148 |
- } |
|
149 |
- if (name == null || name.trim().isEmpty()) { |
|
150 |
- name = oidcUser.getEmail(); |
|
151 |
- } |
|
152 |
- String email = oidcUser.getEmail(); |
|
153 |
- return new OAuth2UserInfo(provider, id, name, email); |
|
154 |
- } catch (Exception e) { |
|
155 |
- return null; |
|
156 |
- } |
|
157 |
- } |
|
158 |
- |
|
159 |
- /** |
|
160 |
- * 일반 OAuth2 사용자 정보 추출 |
|
161 |
- */ |
|
162 |
- private OAuth2UserInfo extractOAuth2UserInfo(OAuth2User oauth2User, String provider) { |
|
163 |
- try { |
|
164 |
- Map<String, Object> attributes = oauth2User.getAttributes(); |
|
165 |
- |
|
166 |
- String id = null; |
|
167 |
- String name = null; |
|
168 |
- String email = null; |
|
169 |
- |
|
170 |
- // 제공자별 정보 추출 |
|
171 |
- switch (provider.toLowerCase()) { |
|
172 |
- case "kakao": |
|
173 |
- id = String.valueOf(attributes.get("id")); |
|
174 |
- Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account"); |
|
175 |
- if (kakaoAccount != null) { |
|
176 |
- email = (String) kakaoAccount.get("email"); |
|
177 |
- Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile"); |
|
178 |
- if (profile != null) { |
|
179 |
- name = (String) profile.get("nickname"); |
|
180 |
- } |
|
181 |
- } |
|
182 |
- break; |
|
183 |
- |
|
184 |
- case "naver": |
|
185 |
- Map<String, Object> naverResponse = (Map<String, Object>) attributes.get("response"); |
|
186 |
- if (naverResponse != null) { |
|
187 |
- id = (String) naverResponse.get("id"); |
|
188 |
- name = (String) naverResponse.get("name"); |
|
189 |
- email = (String) naverResponse.get("email"); |
|
190 |
- } |
|
191 |
- break; |
|
192 |
- |
|
193 |
- case "google": |
|
194 |
- id = (String) attributes.get("sub"); |
|
195 |
- if (id == null) id = (String) attributes.get("id"); |
|
196 |
- name = (String) attributes.get("name"); |
|
197 |
- email = (String) attributes.get("email"); |
|
198 |
- break; |
|
199 |
- } |
|
200 |
- |
|
201 |
- return new OAuth2UserInfo(provider, id, name, email); |
|
202 |
- } catch (Exception e) { |
|
203 |
- return null; |
|
204 |
- } |
|
205 |
- } |
|
206 |
- |
|
207 |
- /** |
|
208 |
- * 제공자 결정 |
|
209 |
- */ |
|
210 |
- private String determineProvider(Authentication authentication) { |
|
211 |
- if (authentication instanceof OAuth2AuthenticationToken) { |
|
212 |
- OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) authentication; |
|
213 |
- String registrationId = oauth2Token.getAuthorizedClientRegistrationId(); |
|
214 |
- return registrationId.toLowerCase(); // 소문자로 통일 |
|
215 |
- } |
|
216 |
- |
|
217 |
- String name = authentication.getName(); |
|
218 |
- |
|
219 |
- if (name != null) { |
|
220 |
- String lowerName = name.toLowerCase(); |
|
221 |
- if (lowerName.contains("google")) return "google"; |
|
222 |
- if (lowerName.contains("kakao")) return "kakao"; |
|
223 |
- if (lowerName.contains("naver")) return "naver"; |
|
224 |
- } |
|
225 |
- |
|
226 |
- // 기본값 |
|
227 |
- return "google"; |
|
228 |
- } |
|
229 |
- |
|
230 |
- /** |
|
231 |
- * 로그인 이력 저장 - OAuth2 전용 |
|
232 |
- */ |
|
233 |
- private void saveLoginHistory(HttpServletRequest request, MberVO mber, String provider) { |
|
234 |
- try { |
|
235 |
- String userAgent = httpRequestUtil.getUserAgent(request); |
|
236 |
- |
|
237 |
- LgnHstryVO loginHistory = new LgnHstryVO(); |
|
238 |
- loginHistory.setLgnId(mber.getLgnId()); |
|
239 |
- loginHistory.setLgnType(mber.getAuthorities().stream().anyMatch(r -> r.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1"); |
|
240 |
- loginHistory.setCntnIp(httpRequestUtil.getIp(request)); |
|
241 |
- loginHistory.setCntnOperSysm(httpRequestUtil.getOS(userAgent)); |
|
242 |
- loginHistory.setDvcNm(httpRequestUtil.getDevice(userAgent)); |
|
243 |
- loginHistory.setBrwsrNm(httpRequestUtil.getBrowser(userAgent)); |
|
244 |
- |
|
245 |
- lgnHstryService.LgnHstrySave(loginHistory); |
|
246 |
- } catch (Exception e) { |
|
247 |
- log.error("로그인 이력 저장 실패", e); |
|
248 |
- } |
|
249 |
- } |
|
250 |
- |
|
251 |
- /** |
|
252 | 113 |
* OAuth2 오류 처리 |
253 | 114 |
*/ |
254 | 115 |
private void handleOAuth2Error(HttpServletResponse response, Exception e) throws IOException { |
116 |
+ |
|
255 | 117 |
String message = URLEncoder.encode("OAuth 로그인에 실패했습니다.", "UTF-8"); |
256 | 118 |
String errorUrl = String.format("%s/?error=oauth2_failed&message=%s", frontUrl, message); |
257 | 119 |
getRedirectStrategy().sendRedirect(null, response, errorUrl); |
258 |
- } |
|
259 |
- |
|
260 |
- /** |
|
261 |
- * OAuth2 사용자 정보 내부 클래스 |
|
262 |
- */ |
|
263 |
- private static class OAuth2UserInfo { |
|
264 |
- final String provider; |
|
265 |
- final String id; |
|
266 |
- final String name; |
|
267 |
- final String email; |
|
268 |
- |
|
269 |
- OAuth2UserInfo(String provider, String id, String name, String email) { |
|
270 |
- this.provider = provider; |
|
271 |
- this.id = id; |
|
272 |
- this.name = name; |
|
273 |
- this.email = email; |
|
274 |
- } |
|
275 | 120 |
} |
276 | 121 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
+++ src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
... | ... | @@ -8,7 +8,6 @@ |
8 | 8 |
import com.takensoft.common.util.HttpRequestUtil; |
9 | 9 |
import com.takensoft.common.util.JWTUtil; |
10 | 10 |
import com.takensoft.common.util.ResponseUtil; |
11 |
-import jakarta.servlet.http.Cookie; |
|
12 | 11 |
import jakarta.servlet.http.HttpServletRequest; |
13 | 12 |
import jakarta.servlet.http.HttpServletResponse; |
14 | 13 |
import jakarta.servlet.http.HttpSession; |
... | ... | @@ -22,6 +21,7 @@ |
22 | 21 |
import java.util.Arrays; |
23 | 22 |
import java.util.HashMap; |
24 | 23 |
import java.util.List; |
24 |
+import java.util.Map; |
|
25 | 25 |
|
26 | 26 |
/** |
27 | 27 |
* @author takensoft |
... | ... | @@ -31,8 +31,10 @@ |
31 | 31 |
* 2025.05.22 | takensoft | 최초 등록 |
32 | 32 |
* 2025.05.26 | takensoft | OAuth 리다이렉트 기능 추가 |
33 | 33 |
* 2025.05.28 | takensoft | 쿠키에서 OAuth 토큰 읽기 추가 |
34 |
+ * 2025.06.18 | takensoft | 토큰 추출 로직 통합 및 중복 제거 |
|
35 |
+ * 2025.06.18 | takensoft | 사용자 정보 응답 생성 로직 통합 완료 |
|
34 | 36 |
* |
35 |
- * OAuth2 관련 통합 컨트롤러 |
|
37 |
+ * OAuth2 관련 통합 컨트롤러 - 사용자 정보 응답 생성 로직 최종 통합 |
|
36 | 38 |
*/ |
37 | 39 |
@RestController |
38 | 40 |
@RequiredArgsConstructor |
... | ... | @@ -52,6 +54,14 @@ |
52 | 54 |
|
53 | 55 |
// 지원하는 OAuth 제공자 목록 |
54 | 56 |
private static final List<String> SUPPORTED_PROVIDERS = Arrays.asList("kakao", "naver", "google"); |
57 |
+ |
|
58 |
+ // 응답 키 상수 정의 |
|
59 |
+ private static final String RESPONSE_KEY_MBR_ID = "mbrId"; |
|
60 |
+ private static final String RESPONSE_KEY_MBR_NM = "mbrNm"; |
|
61 |
+ private static final String RESPONSE_KEY_ROLES = "roles"; |
|
62 |
+ private static final String RESPONSE_KEY_LOGIN_MODE = "loginMode"; |
|
63 |
+ private static final String RESPONSE_KEY_TOKEN = "token"; |
|
64 |
+ private static final String RESPONSE_KEY_EMAIL = "email"; |
|
55 | 65 |
|
56 | 66 |
/** |
57 | 67 |
* OAuth 로그인 리다이렉트 처리 |
... | ... | @@ -106,6 +116,7 @@ |
106 | 116 |
} |
107 | 117 |
|
108 | 118 |
} catch (Exception e) { |
119 |
+ log.error("사용자 정보 조회 중 오류 발생", e); |
|
109 | 120 |
return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); |
110 | 121 |
} |
111 | 122 |
} |
... | ... | @@ -125,25 +136,18 @@ |
125 | 136 |
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
126 | 137 |
} |
127 | 138 |
|
128 |
- // 세션에서 정보 조회 및 최신 권한 정보 가져오기 |
|
129 |
- HashMap<String, Object> params = new HashMap<>(); |
|
130 |
- params.put("mbrId", currentUserId); |
|
131 |
- MberVO mberInfo = mberService.findByMbr(params); |
|
132 |
- |
|
139 |
+ // 사용자 정보 조회 |
|
140 |
+ MberVO mberInfo = getUserById(currentUserId); |
|
133 | 141 |
if (mberInfo == null) { |
134 | 142 |
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
135 | 143 |
} |
136 | 144 |
|
137 |
- // 응답 데이터 구성 |
|
138 |
- HashMap<String, Object> result = new HashMap<>(); |
|
139 |
- result.put("mbrId", mberInfo.getMbrId()); |
|
140 |
- result.put("mbrNm", mberInfo.getMbrNm()); |
|
141 |
- result.put("roles", mberInfo.getAuthorList()); |
|
142 |
- result.put("loginMode", "S"); |
|
143 |
- |
|
145 |
+ // 통합된 응답 생성 |
|
146 |
+ Map<String, Object> result = createUserInfoResponse(mberInfo, "S", null, null); |
|
144 | 147 |
return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); |
145 | 148 |
|
146 | 149 |
} catch (Exception e) { |
150 |
+ log.error("세션 모드 사용자 정보 처리 중 오류", e); |
|
147 | 151 |
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
148 | 152 |
} |
149 | 153 |
} |
... | ... | @@ -153,97 +157,138 @@ |
153 | 157 |
*/ |
154 | 158 |
private ResponseEntity<?> handleJWTModeUserInfo(HttpServletRequest request) { |
155 | 159 |
try { |
156 |
- String token = extractToken(request); |
|
160 |
+ // 통합된 토큰 추출 로직 사용 |
|
161 |
+ String token = jwtUtil.extractTokenFromRequest(request); |
|
157 | 162 |
if (token == null) { |
158 | 163 |
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
159 | 164 |
} |
160 | 165 |
|
161 | 166 |
// 토큰에서 사용자 ID 추출 |
162 |
- String currentUserId; |
|
163 |
- try { |
|
164 |
- currentUserId = (String) jwtUtil.getClaim(token, "mbrId"); |
|
165 |
- if (currentUserId == null || currentUserId.isEmpty()) { |
|
166 |
- return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
167 |
- } |
|
168 |
- } catch (Exception e) { |
|
167 |
+ String currentUserId = extractUserIdFromToken(token); |
|
168 |
+ if (currentUserId == null) { |
|
169 | 169 |
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
170 | 170 |
} |
171 | 171 |
|
172 |
- // DB에서 최신 사용자 정보 조회 |
|
173 |
- HashMap<String, Object> params = new HashMap<>(); |
|
174 |
- params.put("mbrId", currentUserId); |
|
175 |
- MberVO mberInfo = mberService.findByMbr(params); |
|
176 |
- |
|
172 |
+ // 사용자 정보 조회 |
|
173 |
+ MberVO mberInfo = getUserById(currentUserId); |
|
177 | 174 |
if (mberInfo == null) { |
178 | 175 |
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
179 | 176 |
} |
180 | 177 |
|
181 |
- // 응답 데이터 구성 |
|
182 |
- HashMap<String, Object> result = new HashMap<>(); |
|
183 |
- result.put("mbrId", mberInfo.getMbrId()); |
|
184 |
- result.put("mbrNm", mberInfo.getMbrNm()); |
|
185 |
- result.put("roles", mberInfo.getAuthorList()); |
|
186 |
- result.put("loginMode", "J"); |
|
187 |
- |
|
188 |
- // JWT 토큰도 함께 전달 |
|
189 |
- String authHeader = request.getHeader("Authorization"); |
|
190 |
- if (authHeader != null && !authHeader.isEmpty()) { |
|
191 |
- result.put("token", authHeader); |
|
192 |
- } else { |
|
193 |
- // 쿠키에서 가져온 토큰이면 Bearer 형태로 반환 |
|
194 |
- result.put("token", "Bearer " + token); |
|
195 |
- } |
|
196 |
- |
|
178 |
+ // 통합된 응답 생성 (토큰 포함) |
|
179 |
+ Map<String, Object> result = createUserInfoResponse(mberInfo, "J", "Bearer " + token, null); |
|
197 | 180 |
return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); |
198 | 181 |
|
199 | 182 |
} catch (Exception e) { |
183 |
+ log.error("JWT 모드 사용자 정보 처리 중 오류", e); |
|
200 | 184 |
return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
201 | 185 |
} |
202 | 186 |
} |
203 | 187 |
|
204 |
- |
|
205 | 188 |
/** |
206 |
- * 토큰 추출 로직 통합 및 개선 |
|
189 |
+ * @param mberInfo - 사용자 정보 |
|
190 |
+ * @param loginMode - 로그인 모드 ("J", "S") |
|
191 |
+ * @param token - JWT 토큰 (JWT 모드일 때만) |
|
192 |
+ * @param additionalData - 추가 데이터 (필요시) |
|
193 |
+ * @return Map<String, Object> - 표준화된 사용자 정보 응답 |
|
194 |
+ * |
|
195 |
+ * 통합된 사용자 정보 응답 생성 메서드 - 모든 곳에서 사용 가능 |
|
207 | 196 |
*/ |
208 |
- private String extractToken(HttpServletRequest request) { |
|
209 |
- // Authorization 헤더에서 토큰 추출 시도 |
|
210 |
- String authHeader = request.getHeader("Authorization"); |
|
211 |
- if (authHeader != null && !authHeader.isEmpty()) { |
|
212 |
- return jwtUtil.extractToken(authHeader); |
|
197 |
+ public Map<String, Object> createUserInfoResponse(MberVO mberInfo, String loginMode, String token, Map<String, Object> additionalData) { |
|
198 |
+ if (mberInfo == null) { |
|
199 |
+ throw new IllegalArgumentException("사용자 정보가 null입니다."); |
|
213 | 200 |
} |
214 | 201 |
|
215 |
- // OAuth 전용 쿠키에서 토큰 추출 시도 |
|
216 |
- String oauthToken = getTokenFromOAuthCookie(request); |
|
217 |
- if (oauthToken != null && !oauthToken.isEmpty()) { |
|
218 |
- return oauthToken; |
|
202 |
+ Map<String, Object> result = new HashMap<>(); |
|
203 |
+ |
|
204 |
+ // 기본 사용자 정보 설정 |
|
205 |
+ result.put(RESPONSE_KEY_MBR_ID, mberInfo.getMbrId()); |
|
206 |
+ result.put(RESPONSE_KEY_MBR_NM, mberInfo.getMbrNm()); |
|
207 |
+ result.put(RESPONSE_KEY_ROLES, mberInfo.getAuthorList()); |
|
208 |
+ result.put(RESPONSE_KEY_LOGIN_MODE, loginMode); |
|
209 |
+ |
|
210 |
+ // 이메일 정보 (있는 경우) |
|
211 |
+ if (mberInfo.getEml() != null && !mberInfo.getEml().trim().isEmpty()) { |
|
212 |
+ result.put(RESPONSE_KEY_EMAIL, mberInfo.getEml()); |
|
219 | 213 |
} |
220 | 214 |
|
221 |
- // 일반 Authorization 쿠키에서 토큰 추출 시도 |
|
222 |
- if (request.getCookies() != null) { |
|
223 |
- for (Cookie cookie : request.getCookies()) { |
|
224 |
- if ("Authorization".equals(cookie.getName())) { |
|
225 |
- return jwtUtil.extractToken(cookie.getValue()); |
|
226 |
- } |
|
227 |
- } |
|
215 |
+ // JWT 모드인 경우 토큰 추가 |
|
216 |
+ if ("J".equals(loginMode) && token != null && !token.trim().isEmpty()) { |
|
217 |
+ result.put(RESPONSE_KEY_TOKEN, token); |
|
228 | 218 |
} |
229 | 219 |
|
230 |
- return null; |
|
220 |
+ // 추가 데이터가 있으면 병합 |
|
221 |
+ if (additionalData != null && !additionalData.isEmpty()) { |
|
222 |
+ result.putAll(additionalData); |
|
223 |
+ } |
|
224 |
+ |
|
225 |
+ log.debug("사용자 정보 응답 생성 완료 - 사용자: {}, 모드: {}, 토큰포함: {}", |
|
226 |
+ mberInfo.getMbrId(), loginMode, token != null); |
|
227 |
+ |
|
228 |
+ return result; |
|
231 | 229 |
} |
232 | 230 |
|
233 | 231 |
/** |
234 |
- * OAuth 전용 쿠키에서 access token 추출 |
|
232 |
+ * @param mberInfo - 사용자 정보 |
|
233 |
+ * @param loginMode - 로그인 모드 |
|
234 |
+ * @return Map<String, Object> - 기본 사용자 정보 응답 (토큰 없음) |
|
235 |
+ * |
|
236 |
+ * 기본 사용자 정보 응답 생성 |
|
235 | 237 |
*/ |
236 |
- private String getTokenFromOAuthCookie(HttpServletRequest request) { |
|
237 |
- if (request.getCookies() != null) { |
|
238 |
- for (Cookie cookie : request.getCookies()) { |
|
239 |
- if ("Authorization".equals(cookie.getName()) || |
|
240 |
- "refresh".equals(cookie.getName())) { |
|
241 |
- String token = cookie.getValue(); |
|
242 |
- return token.startsWith("Bearer ") ? token.substring(7) : token; |
|
243 |
- } |
|
244 |
- } |
|
238 |
+ public Map<String, Object> createBasicUserInfoResponse(MberVO mberInfo, String loginMode) { |
|
239 |
+ return createUserInfoResponse(mberInfo, loginMode, null, null); |
|
240 |
+ } |
|
241 |
+ |
|
242 |
+ /** |
|
243 |
+ * @param mberInfo - 사용자 정보 |
|
244 |
+ * @param token - JWT 토큰 |
|
245 |
+ * @return Map<String, Object> - JWT 사용자 정보 응답 |
|
246 |
+ * |
|
247 |
+ * JWT 사용자 정보 응답 생성 |
|
248 |
+ */ |
|
249 |
+ public Map<String, Object> createJWTUserInfoResponse(MberVO mberInfo, String token) { |
|
250 |
+ return createUserInfoResponse(mberInfo, "J", token, null); |
|
251 |
+ } |
|
252 |
+ |
|
253 |
+ /** |
|
254 |
+ * @param mberInfo - 사용자 정보 |
|
255 |
+ * @return Map<String, Object> - 세션 사용자 정보 응답 |
|
256 |
+ * |
|
257 |
+ * 세션 사용자 정보 응답 생성 |
|
258 |
+ */ |
|
259 |
+ public Map<String, Object> createSessionUserInfoResponse(MberVO mberInfo) { |
|
260 |
+ return createUserInfoResponse(mberInfo, "S", null, null); |
|
261 |
+ } |
|
262 |
+ |
|
263 |
+ /** |
|
264 |
+ * 사용자 ID로 사용자 정보 조회 |
|
265 |
+ */ |
|
266 |
+ private MberVO getUserById(String userId) { |
|
267 |
+ try { |
|
268 |
+ HashMap<String, Object> params = new HashMap<>(); |
|
269 |
+ params.put("mbrId", userId); |
|
270 |
+ return mberService.findByMbr(params); |
|
271 |
+ } catch (Exception e) { |
|
272 |
+ log.error("사용자 정보 조회 실패 - 사용자 ID: {}", userId, e); |
|
273 |
+ return null; |
|
245 | 274 |
} |
246 |
- return null; |
|
275 |
+ } |
|
276 |
+ |
|
277 |
+ /** |
|
278 |
+ * 토큰에서 사용자 ID 추출 |
|
279 |
+ */ |
|
280 |
+ private String extractUserIdFromToken(String token) { |
|
281 |
+ try { |
|
282 |
+ String currentUserId = (String) jwtUtil.getClaim("Bearer " + token, "mbrId"); |
|
283 |
+ if (currentUserId == null || currentUserId.isEmpty()) { |
|
284 |
+ log.warn("토큰에서 사용자 ID를 찾을 수 없습니다."); |
|
285 |
+ return null; |
|
286 |
+ } |
|
287 |
+ return currentUserId; |
|
288 |
+ } catch (Exception e) { |
|
289 |
+ log.error("토큰에서 사용자 ID 추출 실패", e); |
|
290 |
+ return null; |
|
291 |
+ } |
|
247 | 292 |
} |
248 | 293 |
|
249 | 294 |
/** |
--- src/main/java/com/takensoft/common/util/JWTUtil.java
+++ src/main/java/com/takensoft/common/util/JWTUtil.java
... | ... | @@ -15,21 +15,29 @@ |
15 | 15 |
import javax.crypto.SecretKey; |
16 | 16 |
import javax.crypto.spec.SecretKeySpec; |
17 | 17 |
import jakarta.servlet.http.Cookie; |
18 |
+import jakarta.servlet.http.HttpServletRequest; |
|
18 | 19 |
import java.nio.charset.StandardCharsets; |
19 | 20 |
import java.util.*; |
21 |
+ |
|
20 | 22 |
/** |
21 | 23 |
* @author : takensoft |
22 | 24 |
* @since : 2025.01.22 |
23 | 25 |
* @modification |
24 | 26 |
* since | author | description |
25 | 27 |
* 2025.01.22 | takensoft | 최초 등록 |
28 |
+ * 2025.06.18 | takensoft | 토큰 추출 로직 통합 및 중복 제거 |
|
26 | 29 |
* |
27 |
- * JWT 토큰 생성 및 검증, 쿠키 생성 등의 유틸리티 |
|
30 |
+ * JWT 토큰 생성 및 검증, 쿠키 생성 등의 유틸리티 - 토큰 추출 로직 통합 |
|
28 | 31 |
*/ |
29 | 32 |
@Component |
30 | 33 |
public class JWTUtil { |
31 | 34 |
|
32 | 35 |
private static SecretKey JWT_SECRET_KEY; |
36 |
+ |
|
37 |
+ // 토큰 추출 우선순위 정의 |
|
38 |
+ private static final String[] TOKEN_COOKIE_NAMES = {"Authorization", "refresh"}; |
|
39 |
+ private static final String BEARER_PREFIX = "Bearer "; |
|
40 |
+ private static final String AUTHORIZATION_HEADER = "Authorization"; |
|
33 | 41 |
|
34 | 42 |
/** |
35 | 43 |
* @param secret - JWT 서명을 위한 키 (application.yml에서 값을 읽어 옴) |
... | ... | @@ -38,6 +46,97 @@ |
38 | 46 |
*/ |
39 | 47 |
public JWTUtil(@Value("${jwt.secret}")String secret) { |
40 | 48 |
this.JWT_SECRET_KEY = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); |
49 |
+ } |
|
50 |
+ |
|
51 |
+ /** |
|
52 |
+ * @param request - HTTP 요청 객체 |
|
53 |
+ * @return 추출된 JWT 토큰 (Bearer 접두사 제거된 순수 토큰) 또는 null |
|
54 |
+ * |
|
55 |
+ * HTTP 요청에서 JWT 토큰을 추출하는 통합 메서드 |
|
56 |
+ * 우선순위: Authorization 헤더 → Authorization 쿠키 → refresh 쿠키 |
|
57 |
+ */ |
|
58 |
+ public String extractTokenFromRequest(HttpServletRequest request) { |
|
59 |
+ if (request == null) { |
|
60 |
+ return null; |
|
61 |
+ } |
|
62 |
+ |
|
63 |
+ // 1. Authorization 헤더에서 토큰 추출 시도 |
|
64 |
+ String authHeader = request.getHeader(AUTHORIZATION_HEADER); |
|
65 |
+ if (isValidTokenString(authHeader)) { |
|
66 |
+ return removeBearerPrefix(authHeader); |
|
67 |
+ } |
|
68 |
+ |
|
69 |
+ // 2. 쿠키에서 토큰 추출 시도 |
|
70 |
+ String tokenFromCookie = extractTokenFromCookie(request); |
|
71 |
+ if (isValidTokenString(tokenFromCookie)) { |
|
72 |
+ return removeBearerPrefix(tokenFromCookie); |
|
73 |
+ } |
|
74 |
+ |
|
75 |
+ return null; |
|
76 |
+ } |
|
77 |
+ |
|
78 |
+ /** |
|
79 |
+ * @param authHeader - Authorization 헤더 문자열 또는 토큰 문자열 |
|
80 |
+ * @return Bearer 접두사가 제거된 순수 토큰 문자열 |
|
81 |
+ * |
|
82 |
+ * Bearer 접두사를 제거하는 기존 메서드 (하위 호환성 유지) |
|
83 |
+ */ |
|
84 |
+ public String extractToken(String authHeader) { |
|
85 |
+ return removeBearerPrefix(authHeader); |
|
86 |
+ } |
|
87 |
+ |
|
88 |
+ /** |
|
89 |
+ * @param request - HTTP 요청 객체 |
|
90 |
+ * @return 쿠키에서 추출된 토큰 문자열 또는 null |
|
91 |
+ * |
|
92 |
+ * 쿠키에서 토큰을 추출하는 전용 메서드 |
|
93 |
+ * 우선순위: Authorization 쿠키 → refresh 쿠키 |
|
94 |
+ */ |
|
95 |
+ private String extractTokenFromCookie(HttpServletRequest request) { |
|
96 |
+ Cookie[] cookies = request.getCookies(); |
|
97 |
+ if (cookies == null) { |
|
98 |
+ return null; |
|
99 |
+ } |
|
100 |
+ |
|
101 |
+ // 쿠키 우선순위에 따라 토큰 추출 |
|
102 |
+ for (String cookieName : TOKEN_COOKIE_NAMES) { |
|
103 |
+ for (Cookie cookie : cookies) { |
|
104 |
+ if (cookieName.equals(cookie.getName())) { |
|
105 |
+ String cookieValue = cookie.getValue(); |
|
106 |
+ if (isValidTokenString(cookieValue)) { |
|
107 |
+ return cookieValue; |
|
108 |
+ } |
|
109 |
+ } |
|
110 |
+ } |
|
111 |
+ } |
|
112 |
+ |
|
113 |
+ return null; |
|
114 |
+ } |
|
115 |
+ |
|
116 |
+ /** |
|
117 |
+ * @param tokenString - 검증할 토큰 문자열 |
|
118 |
+ * @return 유효한 토큰 문자열인지 여부 |
|
119 |
+ * |
|
120 |
+ * 토큰 문자열 유효성 검증 |
|
121 |
+ */ |
|
122 |
+ private boolean isValidTokenString(String tokenString) { |
|
123 |
+ return tokenString != null && |
|
124 |
+ !tokenString.trim().isEmpty() && |
|
125 |
+ !tokenString.equalsIgnoreCase("null") && |
|
126 |
+ !tokenString.equalsIgnoreCase("undefined"); |
|
127 |
+ } |
|
128 |
+ |
|
129 |
+ /** |
|
130 |
+ * @param authHeader - Bearer 접두사가 포함될 수 있는 토큰 문자열 |
|
131 |
+ * @return Bearer 접두사가 제거된 순수 토큰 문자열 |
|
132 |
+ * |
|
133 |
+ * Bearer 접두사 제거 로직 |
|
134 |
+ */ |
|
135 |
+ private String removeBearerPrefix(String authHeader) { |
|
136 |
+ if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) { |
|
137 |
+ return authHeader.substring(BEARER_PREFIX.length()); |
|
138 |
+ } |
|
139 |
+ return authHeader; |
|
41 | 140 |
} |
42 | 141 |
|
43 | 142 |
/** |
... | ... | @@ -96,7 +195,7 @@ |
96 | 195 |
// 토큰 값 검증 |
97 | 196 |
if (tkn == null || tkn.trim().isEmpty()) { |
98 | 197 |
throw new IllegalArgumentException("Token is null or empty"); |
99 |
- }else{ |
|
198 |
+ } else { |
|
100 | 199 |
tkn = extractToken(tkn); |
101 | 200 |
} |
102 | 201 |
|
... | ... | @@ -140,18 +239,4 @@ |
140 | 239 |
throw new IllegalArgumentException("Invalid knd : " + knd); |
141 | 240 |
} |
142 | 241 |
} |
143 |
- |
|
144 |
- /** |
|
145 |
- * @param authHeader JWT 토큰 문자열 |
|
146 |
- * @return 실제 토큰 부분만 추출 |
|
147 |
- * |
|
148 |
- * Bearer 토큰에서 실제 토큰 부분만 추출하는 메서드 |
|
149 |
- */ |
|
150 |
- public String extractToken(String authHeader) { |
|
151 |
- if (authHeader != null && authHeader.startsWith("Bearer ")) { |
|
152 |
- return authHeader.substring(7); |
|
153 |
- } |
|
154 |
- return authHeader; // Bearer가 없으면 그대로 반환 |
|
155 |
- } |
|
156 |
- |
|
157 |
-} |
|
242 |
+}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/util/LoginUtil.java
+++ src/main/java/com/takensoft/common/util/LoginUtil.java
... | ... | @@ -4,7 +4,6 @@ |
4 | 4 |
import com.takensoft.cms.loginPolicy.service.LoginModeService; |
5 | 5 |
import com.takensoft.cms.loginPolicy.service.LoginPolicyService; |
6 | 6 |
import com.takensoft.cms.mber.service.LgnHstryService; |
7 |
-import com.takensoft.cms.mber.vo.LgnHstryVO; |
|
8 | 7 |
import com.takensoft.cms.mber.vo.MberVO; |
9 | 8 |
import com.takensoft.cms.token.service.RefreshTokenService; |
10 | 9 |
import com.takensoft.cms.token.vo.RefreshTknVO; |
... | ... | @@ -36,8 +35,9 @@ |
36 | 35 |
* 2025.05.29 | takensoft | Redis 통합 중복로그인 관리 |
37 | 36 |
* 2025.06.04 | takensoft | Redis 트랜잭션 및 타이밍 이슈 해결 |
38 | 37 |
* 2025.06.09 | takensoft | 중복로그인 처리 로직 개선 |
38 |
+ * 2025.06.18 | takensoft | 로그인 이력 저장 로직 통합 및 중복 제거 |
|
39 | 39 |
* |
40 |
- * 통합 로그인 유틸리티 - 중복로그인 처리 개선 |
|
40 |
+ * 통합 로그인 유틸리티 - 로그인 이력 저장 로직 중복 제거 |
|
41 | 41 |
*/ |
42 | 42 |
@Component |
43 | 43 |
@RequiredArgsConstructor |
... | ... | @@ -69,10 +69,11 @@ |
69 | 69 |
res.setHeader("loginMode", loginMode); // J, S |
70 | 70 |
res.setHeader("policyMode", allowMultipleLogin ? "Y" : "N"); // Y, N |
71 | 71 |
|
72 |
- // 로그인 이력 등록 |
|
72 |
+ // 통합된 로그인 이력 저장 로직 사용 |
|
73 | 73 |
String loginType = (String) req.getAttribute("loginType"); |
74 | 74 |
if (!"OAUTH2".equals(loginType)) { |
75 |
- saveLoginHistory(mber, req); |
|
75 |
+ // 시스템 로그인인 경우에만 이력 저장 (OAuth2는 핸들러에서 처리) |
|
76 |
+ lgnHstryService.saveSystemLoginHistory(mber, req); |
|
76 | 77 |
} |
77 | 78 |
|
78 | 79 |
if ("S".equals(loginMode)) { |
... | ... | @@ -119,7 +120,6 @@ |
119 | 120 |
if (!"OAUTH2".equals(loginType)) { |
120 | 121 |
sendSessionLoginResponse(res, mber); |
121 | 122 |
} |
122 |
- |
|
123 | 123 |
} |
124 | 124 |
|
125 | 125 |
/** |
... | ... | @@ -166,7 +166,6 @@ |
166 | 166 |
if (!"OAUTH2".equals(loginType)) { |
167 | 167 |
res.setStatus(HttpStatus.OK.value()); |
168 | 168 |
} |
169 |
- |
|
170 | 169 |
} |
171 | 170 |
|
172 | 171 |
/** |
... | ... | @@ -241,23 +240,5 @@ |
241 | 240 |
String jsonResponse = new ObjectMapper().writeValueAsString(result); |
242 | 241 |
res.getWriter().write(jsonResponse); |
243 | 242 |
res.getWriter().flush(); |
244 |
- } |
|
245 |
- |
|
246 |
- /** |
|
247 |
- * 로그인 이력 저장 |
|
248 |
- */ |
|
249 |
- private void saveLoginHistory(MberVO mber, HttpServletRequest req) { |
|
250 |
- String userAgent = httpRequestUtil.getUserAgent(req); |
|
251 |
- |
|
252 |
- LgnHstryVO lgnHstryVO = new LgnHstryVO(); |
|
253 |
- lgnHstryVO.setLgnId(mber.getLgnId()); |
|
254 |
- lgnHstryVO.setLgnType(mber.getAuthorities().stream() |
|
255 |
- .anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1"); |
|
256 |
- lgnHstryVO.setCntnIp(httpRequestUtil.getIp(req)); |
|
257 |
- lgnHstryVO.setCntnOperSysm(httpRequestUtil.getOS(userAgent)); |
|
258 |
- lgnHstryVO.setDvcNm(httpRequestUtil.getDevice(userAgent)); |
|
259 |
- lgnHstryVO.setBrwsrNm(httpRequestUtil.getBrowser(userAgent)); |
|
260 |
- |
|
261 |
- lgnHstryService.LgnHstrySave(lgnHstryVO); |
|
262 | 243 |
} |
263 | 244 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/resources/mybatis/mapper/mber/mber-SQL.xml
+++ src/main/resources/mybatis/mapper/mber/mber-SQL.xml
... | ... | @@ -489,15 +489,6 @@ |
489 | 489 |
|
490 | 490 |
<!-- 메인 프로필 설정 --> |
491 | 491 |
<update id="setPrimaryProfile" parameterType="map"> |
492 |
- <!-- 기존 메인 프로필 해제 --> |
|
493 |
- UPDATE mbr_sns_acnt_info |
|
494 |
- SET main_prfl_yn = false, |
|
495 |
- mdfr = #{mdfr}, |
|
496 |
- mdfcn_dt = NOW() |
|
497 |
- WHERE mbr_id = #{mbrId} |
|
498 |
- AND main_prfl_yn = true; |
|
499 |
- |
|
500 |
- <!-- 새로운 메인 프로필 설정 --> |
|
501 | 492 |
UPDATE mbr_sns_acnt_info |
502 | 493 |
SET main_prfl_yn = true, |
503 | 494 |
mdfr = #{mdfr}, |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?