

250529 김혜민 시스템, 소셜 통합 로그인 기능 추가 및 변경
@3d66e578250082b3c2ed3470e05d1c761a048f6b
--- src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
+++ src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
... | ... | @@ -3,27 +3,31 @@ |
3 | 3 |
import com.takensoft.cms.mber.dto.JoinDTO; |
4 | 4 |
import com.takensoft.cms.mber.dto.PasswordDTO; |
5 | 5 |
import com.takensoft.cms.mber.vo.MberAuthorVO; |
6 |
+import com.takensoft.cms.mber.vo.MberSocialAccountVO; |
|
6 | 7 |
import com.takensoft.cms.mber.vo.MberVO; |
7 | 8 |
import com.takensoft.common.Pagination; |
8 | 9 |
import org.egovframe.rte.psl.dataaccess.mapper.Mapper; |
9 | 10 |
|
10 | 11 |
import java.util.*; |
12 |
+ |
|
11 | 13 |
/** |
12 | 14 |
* @author takensoft |
13 | 15 |
* @since 2024.04.01 |
14 | 16 |
* @modification |
15 | 17 |
* since | author | description |
16 | 18 |
* 2024.04.01 | takensoft | 최초 등록 |
19 |
+ * 2025.05.29 | takensoft | 통합 로그인 기능 추가 |
|
17 | 20 |
* |
18 |
- * 회원 정보 관련 Mapper |
|
21 |
+ * 회원 정보 관련 Mapper - 통합 로그인 시스템 지원 |
|
19 | 22 |
*/ |
20 | 23 |
@Mapper("mberDAO") |
21 | 24 |
public interface MberDAO { |
25 |
+ |
|
22 | 26 |
/** |
23 | 27 |
* @param lgnId - 로그인 아이디 |
24 | 28 |
* @return MberVO - 사용자 정보 조회 결과 |
25 | 29 |
* |
26 |
- * 사용자 정보 조회 [security 용] |
|
30 |
+ * 사용자 정보 조회 [security 용] - 통합 로그인 대응 |
|
27 | 31 |
*/ |
28 | 32 |
MberVO findByMberSecurity(String lgnId); |
29 | 33 |
|
... | ... | @@ -31,7 +35,7 @@ |
31 | 35 |
* @param lgnId - 로그인 아이디 |
32 | 36 |
* @return boolean - 아이디 중복 여부 |
33 | 37 |
* |
34 |
- * 아이디 중복 검사 |
|
38 |
+ * 아이디 중복 검사 - 통합 로그인 대응 |
|
35 | 39 |
*/ |
36 | 40 |
boolean findByCheckLoginId(String lgnId); |
37 | 41 |
|
... | ... | @@ -115,4 +119,92 @@ |
115 | 119 |
* 회원 ID로 권한 목록 조회 |
116 | 120 |
*/ |
117 | 121 |
List<MberAuthorVO> findAuthoritiesByMbrId(String mbrId); |
122 |
+ |
|
123 |
+ // =================================== |
|
124 |
+ // 통합 로그인을 위한 새로운 메서드들 |
|
125 |
+ // =================================== |
|
126 |
+ |
|
127 |
+ /** |
|
128 |
+ * @param params - 제공자 타입과 식별자를 포함한 Map |
|
129 |
+ * - providerType: 제공자 타입 (SYSTEM, KAKAO, NAVER, GOOGLE) |
|
130 |
+ * - identifier: 식별자 (로그인ID 또는 소셜ID) |
|
131 |
+ * @return MberVO - 통합 로그인으로 조회된 사용자 정보 |
|
132 |
+ * |
|
133 |
+ * 통합 로그인: 제공자별 사용자 조회 |
|
134 |
+ */ |
|
135 |
+ MberVO findByUnifiedLogin(HashMap<String, Object> params); |
|
136 |
+ |
|
137 |
+ /** |
|
138 |
+ * @param email - 이메일 |
|
139 |
+ * @return MberVO - 이메일로 조회된 사용자 정보 (첫 번째 계정) |
|
140 |
+ * |
|
141 |
+ * 이메일로 모든 연동 계정 조회 (통합용) |
|
142 |
+ */ |
|
143 |
+ MberVO findAllAccountsByEmail(String email); |
|
144 |
+ |
|
145 |
+ /** |
|
146 |
+ * @param mbrId - 회원 ID |
|
147 |
+ * @return List<MberSocialAccountVO> - 소셜 계정 목록 |
|
148 |
+ * |
|
149 |
+ * 회원 ID로 소셜 계정 목록 조회 |
|
150 |
+ */ |
|
151 |
+ List<MberSocialAccountVO> findSocialAccountsByMbrId(String mbrId); |
|
152 |
+ |
|
153 |
+ /** |
|
154 |
+ * @param params - 회원 ID와 제공자 타입을 포함한 Map |
|
155 |
+ * - mbrId: 회원 ID |
|
156 |
+ * - providerType: 제공자 타입 |
|
157 |
+ * @return MberSocialAccountVO - 특정 제공자의 소셜 계정 정보 |
|
158 |
+ * |
|
159 |
+ * 특정 제공자로 소셜 계정 조회 |
|
160 |
+ */ |
|
161 |
+ MberSocialAccountVO findSocialAccountByProvider(HashMap<String, Object> params); |
|
162 |
+ |
|
163 |
+ /** |
|
164 |
+ * @param socialAccount - 소셜 계정 정보 |
|
165 |
+ * @return int - 저장 결과 |
|
166 |
+ * |
|
167 |
+ * 소셜 계정 정보 저장 |
|
168 |
+ */ |
|
169 |
+ int saveSocialAccount(MberSocialAccountVO socialAccount); |
|
170 |
+ |
|
171 |
+ /** |
|
172 |
+ * @param socialAccount - 소셜 계정 연동 정보 |
|
173 |
+ * @return int - 연동 결과 |
|
174 |
+ * |
|
175 |
+ * 소셜 계정 연동 (중복 시 업데이트) |
|
176 |
+ */ |
|
177 |
+ int linkSocialAccount(MberSocialAccountVO socialAccount); |
|
178 |
+ |
|
179 |
+ /** |
|
180 |
+ * @param params - 연동 해지 정보를 포함한 Map |
|
181 |
+ * - mbrId: 회원 ID |
|
182 |
+ * - providerType: 제공자 타입 |
|
183 |
+ * - mdfr: 수정자 |
|
184 |
+ * @return int - 해지 결과 |
|
185 |
+ * |
|
186 |
+ * 소셜 계정 연동 해지 |
|
187 |
+ */ |
|
188 |
+ int unlinkSocialAccount(HashMap<String, Object> params); |
|
189 |
+ |
|
190 |
+ /** |
|
191 |
+ * @param params - 메인 프로필 설정 정보를 포함한 Map |
|
192 |
+ * - mbrId: 회원 ID |
|
193 |
+ * - providerType: 제공자 타입 |
|
194 |
+ * - mdfr: 수정자 |
|
195 |
+ * @return int - 설정 결과 |
|
196 |
+ * |
|
197 |
+ * 메인 프로필 설정 |
|
198 |
+ */ |
|
199 |
+ int setPrimaryProfile(HashMap<String, Object> params); |
|
200 |
+ |
|
201 |
+ /** |
|
202 |
+ * @param params - 연동 가능한 계정 검색 정보를 포함한 Map |
|
203 |
+ * - email: 이메일 |
|
204 |
+ * - providerType: 제외할 제공자 타입 |
|
205 |
+ * @return MberVO - 연동 가능한 계정 정보 |
|
206 |
+ * |
|
207 |
+ * 연동 가능한 계정 조회 (이메일로 검색, 특정 제공자 제외) |
|
208 |
+ */ |
|
209 |
+ MberVO findLinkableAccount(HashMap<String, Object> params); |
|
118 | 210 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 |
import com.takensoft.cms.mber.dto.PasswordDTO; |
6 | 6 |
import com.takensoft.cms.mber.service.MberService; |
7 | 7 |
import com.takensoft.cms.mber.vo.MberAuthorVO; |
8 |
+import com.takensoft.cms.mber.vo.MberSocialAccountVO; |
|
8 | 9 |
import com.takensoft.cms.mber.vo.MberVO; |
9 | 10 |
import com.takensoft.common.exception.*; |
10 | 11 |
import com.takensoft.common.idgen.service.IdgenService; |
... | ... | @@ -12,6 +13,7 @@ |
12 | 13 |
import com.takensoft.common.util.HttpRequestUtil; |
13 | 14 |
import com.takensoft.common.util.Secret; |
14 | 15 |
import lombok.RequiredArgsConstructor; |
16 |
+import lombok.extern.slf4j.Slf4j; |
|
15 | 17 |
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; |
16 | 18 |
import org.springframework.dao.DataAccessException; |
17 | 19 |
import org.springframework.security.core.userdetails.UserDetails; |
... | ... | @@ -33,6 +35,7 @@ |
33 | 35 |
* @modification |
34 | 36 |
* since | author | description |
35 | 37 |
* 2024.04.01 | takensoft | 최초 등록 |
38 |
+ * 2025.05.29 | takensoft | 통합 로그인 기능 추가 |
|
36 | 39 |
* |
37 | 40 |
* 회원 정보 관련 구현체 |
38 | 41 |
* EgovAbstractServiceImpl : 전자정부 상속 |
... | ... | @@ -41,6 +44,7 @@ |
41 | 44 |
*/ |
42 | 45 |
@Service("mberService") |
43 | 46 |
@RequiredArgsConstructor |
47 |
+@Slf4j |
|
44 | 48 |
public class MberServiceImpl extends EgovAbstractServiceImpl implements UserDetailsService, MberService { |
45 | 49 |
private final MberDAO mberDAO; |
46 | 50 |
private final IdgenService mberIdgn; |
... | ... | @@ -54,12 +58,13 @@ |
54 | 58 |
* @throws UsernameNotFoundException - 가입하지 않은 계정으로 로그인 시도 시 |
55 | 59 |
* @throws Exception - 그 외 예외 발생 시 |
56 | 60 |
* |
57 |
- * security 상속 시 Override 하는 메소드 |
|
61 |
+ * security 상속 시 Override 하는 메소드 - 통합 로그인 대응 |
|
58 | 62 |
*/ |
59 | 63 |
@Override |
60 | 64 |
@Transactional(readOnly = true) |
61 | 65 |
public UserDetails loadUserByUsername(String username){ |
62 | 66 |
try { |
67 |
+ // 통합 로그인: 모든 제공자에서 사용자 검색 |
|
63 | 68 |
UserDetails userDetails = mberDAO.findByMberSecurity(username); |
64 | 69 |
|
65 | 70 |
return userDetails; |
... | ... | @@ -69,6 +74,7 @@ |
69 | 74 |
throw e; |
70 | 75 |
} |
71 | 76 |
} |
77 |
+ |
|
72 | 78 |
/** |
73 | 79 |
* @param lgnId - 로그인 아이디 |
74 | 80 |
* @return boolean - 아이디 아이디 중복 여부 |
... | ... | @@ -76,7 +82,7 @@ |
76 | 82 |
* @throws DataAccessException - db 관련 예외 발생 시 |
77 | 83 |
* @throws Exception - 그 외 예외 발생 시 |
78 | 84 |
* |
79 |
- * 아이디 중복 검사 |
|
85 |
+ * 아이디 중복 검사 - 통합 로그인 대응 |
|
80 | 86 |
*/ |
81 | 87 |
@Override |
82 | 88 |
public boolean findByCheckLoginId(String lgnId) { |
... | ... | @@ -104,82 +110,171 @@ |
104 | 110 |
* @throws DataAccessException - db 관련 예외 발생 시 |
105 | 111 |
* @throws Exception - 그 외 예외 발생 시 |
106 | 112 |
* |
107 |
- * 회원가입 |
|
113 |
+ * 회원가입 - 통합 로그인 대응 |
|
108 | 114 |
*/ |
109 | 115 |
@Override |
110 | 116 |
@Transactional(rollbackFor = Exception.class) |
111 |
- public HashMap<String, Object> userJoin(HttpServletRequest req, JoinDTO joinDTO){ |
|
117 |
+ public HashMap<String, Object> userJoin(HttpServletRequest req, JoinDTO joinDTO){ |
|
112 | 118 |
try { |
113 |
- // 회원 아이디 생성 (이미 설정된 경우 건너뛰기) |
|
114 |
- if (joinDTO.getMbrId() == null || joinDTO.getMbrId().isEmpty()) { |
|
115 |
- String mbrId = mberIdgn.getNextStringId(); |
|
116 |
- joinDTO.setMbrId(mbrId); |
|
117 |
- } |
|
118 |
- |
|
119 |
- // 아이디 소문자 변환 |
|
120 |
- if (joinDTO.getLgnId() != null && !joinDTO.getLgnId().isEmpty()) { |
|
121 |
- joinDTO.setLgnId(joinDTO.getLgnId().toLowerCase()); |
|
122 |
- } |
|
123 |
- |
|
124 |
- // 비밀번호 암호화 (OAuth2는 비밀번호 없음) |
|
125 |
- if (joinDTO.getPswd() != null && !joinDTO.getPswd().isEmpty()) { |
|
126 |
- joinDTO.setPswd(bCryptPasswordEncoder.encode(joinDTO.getPswd())); |
|
127 |
- } |
|
128 |
- |
|
129 |
- // 연락처 암호화 |
|
130 |
- if(joinDTO.getMblTelno() != null && !joinDTO.getMblTelno().equals("")) { |
|
131 |
- joinDTO.setMblTelno(Secret.encrypt(joinDTO.getMblTelno())); |
|
132 |
- } |
|
133 |
- if(joinDTO.getTelno() != null && !joinDTO.getTelno().equals("")) { |
|
134 |
- joinDTO.setTelno(Secret.encrypt(joinDTO.getTelno())); |
|
135 |
- } |
|
136 |
- //멤버타입 없을시 default "S" 고정 |
|
137 |
- if (joinDTO.getMbrType() == null || joinDTO.getMbrType().isEmpty()) { |
|
138 |
- joinDTO.setMbrType("S"); |
|
139 |
- } |
|
140 |
- // 아이피 조회 및 등록 |
|
141 |
- joinDTO.setFrstRegIp(httpRequestUtil.getIp(req)); |
|
142 |
- |
|
143 |
- // 등록된 토큰에서 사용자 정보 조회 |
|
144 |
- String writer = joinDTO.getRgtr(); |
|
145 |
- if (writer == null || writer.isEmpty()) { |
|
146 |
- writer = verificationService.getCurrentUserId(); |
|
147 |
- if (writer == null || writer.isEmpty()) { |
|
148 |
- throw new CustomNotFoundException("사용자 정보 조회에 실패했습니다."); |
|
149 |
- } |
|
150 |
- joinDTO.setRgtr(writer); |
|
151 |
- } |
|
152 |
- |
|
153 |
- // 회원정보 등록 |
|
154 |
- HashMap<String, Object> result = new HashMap<>(); |
|
155 |
- int saveResult = mberDAO.save(joinDTO); |
|
156 |
- if(saveResult == 0) { |
|
157 |
- throw new CustomInsertFailException("회원 정보 등록에 실패했습니다."); |
|
158 |
- } |
|
159 |
- |
|
160 |
- result.put("mbrId", joinDTO.getMbrId()); |
|
161 |
- |
|
162 |
- // 권한 등록 |
|
163 |
- int authorResult = 0; |
|
164 |
- if(joinDTO.getAuthorList().size() > 0) { |
|
165 |
- for(MberAuthorVO vo : joinDTO.getAuthorList()) { |
|
166 |
- vo.setMbrId(joinDTO.getMbrId()); |
|
167 |
- // 작성자 등록 |
|
168 |
- vo.setRgtr(writer); |
|
169 |
- |
|
170 |
- authorResult += mberDAO.authorSave(vo); |
|
171 |
- if(authorResult == 0) { |
|
172 |
- throw new CustomInsertFailException("회원 권한 등록에 실패했습니다."); |
|
173 |
- } |
|
119 |
+ // 이메일로 기존 계정 확인 (기본 검사만 수행) |
|
120 |
+ if (joinDTO.getEml() != null && !joinDTO.getEml().isEmpty()) { |
|
121 |
+ MberVO existingUser = mberDAO.findByEmail(joinDTO.getEml()); |
|
122 |
+ if (existingUser != null) { |
|
123 |
+ throw new CustomIdTakenException("해당 이메일로 이미 계정이 등록되어 있습니다."); |
|
174 | 124 |
} |
175 | 125 |
} |
176 |
- result.put("result", saveResult + authorResult); |
|
177 | 126 |
|
178 |
- return result; |
|
127 |
+ // 기존 회원가입 로직 실행 |
|
128 |
+ return performStandardJoin(req, joinDTO); |
|
129 |
+ |
|
179 | 130 |
} catch (DataAccessException dae) { |
180 | 131 |
throw dae; |
181 | 132 |
} catch (Exception e) { |
182 | 133 |
throw e; |
134 |
+ } |
|
135 |
+ } |
|
136 |
+ |
|
137 |
+ /** |
|
138 |
+ * 표준 회원가입 처리 (기존 로직) |
|
139 |
+ */ |
|
140 |
+ private HashMap<String, Object> performStandardJoin(HttpServletRequest req, JoinDTO joinDTO) { |
|
141 |
+ // 회원 아이디 생성 (이미 설정된 경우 건너뛰기) |
|
142 |
+ if (joinDTO.getMbrId() == null || joinDTO.getMbrId().isEmpty()) { |
|
143 |
+ String mbrId = mberIdgn.getNextStringId(); |
|
144 |
+ joinDTO.setMbrId(mbrId); |
|
145 |
+ } |
|
146 |
+ |
|
147 |
+ // 아이디 소문자 변환 |
|
148 |
+ if (joinDTO.getLgnId() != null && !joinDTO.getLgnId().isEmpty()) { |
|
149 |
+ joinDTO.setLgnId(joinDTO.getLgnId().toLowerCase()); |
|
150 |
+ } |
|
151 |
+ |
|
152 |
+ // 비밀번호 암호화 (OAuth2는 비밀번호 없음) |
|
153 |
+ if (joinDTO.getPswd() != null && !joinDTO.getPswd().isEmpty()) { |
|
154 |
+ joinDTO.setPswd(bCryptPasswordEncoder.encode(joinDTO.getPswd())); |
|
155 |
+ } |
|
156 |
+ |
|
157 |
+ // 연락처 암호화 |
|
158 |
+ if(joinDTO.getMblTelno() != null && !joinDTO.getMblTelno().equals("")) { |
|
159 |
+ joinDTO.setMblTelno(Secret.encrypt(joinDTO.getMblTelno())); |
|
160 |
+ } |
|
161 |
+ if(joinDTO.getTelno() != null && !joinDTO.getTelno().equals("")) { |
|
162 |
+ joinDTO.setTelno(Secret.encrypt(joinDTO.getTelno())); |
|
163 |
+ } |
|
164 |
+ //멤버타입 없을시 default "S" 고정 |
|
165 |
+ if (joinDTO.getMbrType() == null || joinDTO.getMbrType().isEmpty()) { |
|
166 |
+ joinDTO.setMbrType("S"); |
|
167 |
+ } |
|
168 |
+ // 아이피 조회 및 등록 |
|
169 |
+ joinDTO.setFrstRegIp(httpRequestUtil.getIp(req)); |
|
170 |
+ |
|
171 |
+ // 등록된 토큰에서 사용자 정보 조회 |
|
172 |
+ String writer = joinDTO.getRgtr(); |
|
173 |
+ if (writer == null || writer.isEmpty()) { |
|
174 |
+ writer = verificationService.getCurrentUserId(); |
|
175 |
+ if (writer == null || writer.isEmpty()) { |
|
176 |
+ writer = "SYSTEM"; // 자체 회원가입인 경우 |
|
177 |
+ } |
|
178 |
+ joinDTO.setRgtr(writer); |
|
179 |
+ } |
|
180 |
+ |
|
181 |
+ // 회원정보 등록 |
|
182 |
+ HashMap<String, Object> result = new HashMap<>(); |
|
183 |
+ int saveResult = mberDAO.save(joinDTO); |
|
184 |
+ if(saveResult == 0) { |
|
185 |
+ throw new CustomInsertFailException("회원 정보 등록에 실패했습니다."); |
|
186 |
+ } |
|
187 |
+ |
|
188 |
+ result.put("mbrId", joinDTO.getMbrId()); |
|
189 |
+ |
|
190 |
+ // 권한 등록 |
|
191 |
+ int authorResult = 0; |
|
192 |
+ if(joinDTO.getAuthorList().size() > 0) { |
|
193 |
+ for(MberAuthorVO vo : joinDTO.getAuthorList()) { |
|
194 |
+ vo.setMbrId(joinDTO.getMbrId()); |
|
195 |
+ // 작성자 등록 |
|
196 |
+ vo.setRgtr(writer); |
|
197 |
+ |
|
198 |
+ authorResult += mberDAO.authorSave(vo); |
|
199 |
+ if(authorResult == 0) { |
|
200 |
+ throw new CustomInsertFailException("회원 권한 등록에 실패했습니다."); |
|
201 |
+ } |
|
202 |
+ } |
|
203 |
+ } |
|
204 |
+ |
|
205 |
+ // 시스템 로그인 계정 정보 소셜 계정 테이블에도 저장 |
|
206 |
+ MberSocialAccountVO systemAccount = new MberSocialAccountVO(); |
|
207 |
+ systemAccount.setMbrId(joinDTO.getMbrId()); |
|
208 |
+ systemAccount.setProviderType("S"); |
|
209 |
+ systemAccount.setLoginId(joinDTO.getLgnId()); |
|
210 |
+ systemAccount.setSocialEmail(joinDTO.getEml()); |
|
211 |
+ systemAccount.setSocialName(joinDTO.getMbrNm()); |
|
212 |
+ systemAccount.setIsPrimaryProfile(true); |
|
213 |
+ systemAccount.setIsActive(true); |
|
214 |
+ systemAccount.setRgtr(writer); |
|
215 |
+ |
|
216 |
+ mberDAO.saveSocialAccount(systemAccount); |
|
217 |
+ |
|
218 |
+ result.put("result", saveResult + authorResult); |
|
219 |
+ |
|
220 |
+ return result; |
|
221 |
+ } |
|
222 |
+ |
|
223 |
+ /** |
|
224 |
+ * 기존 계정에 시스템 로그인 연동 |
|
225 |
+ */ |
|
226 |
+ @Transactional(rollbackFor = Exception.class) |
|
227 |
+ public HashMap<String, Object> linkSystemLogin(String mbrId, String loginId, String password, String rgtr) { |
|
228 |
+ try { |
|
229 |
+ // 회원 정보 조회 |
|
230 |
+ MberVO user = mberDAO.findByMber(mbrId); |
|
231 |
+ if (user == null) { |
|
232 |
+ throw new CustomNotFoundException("회원 정보를 찾을 수 없습니다."); |
|
233 |
+ } |
|
234 |
+ |
|
235 |
+ // 비밀번호 설정 |
|
236 |
+ PasswordDTO passwordDTO = new PasswordDTO(); |
|
237 |
+ passwordDTO.setMbrId(mbrId); |
|
238 |
+ passwordDTO.setNewPswd(password); |
|
239 |
+ passwordDTO.setMdfr(rgtr); |
|
240 |
+ updatePassword(passwordDTO); |
|
241 |
+ |
|
242 |
+ // 시스템 로그인 연동 |
|
243 |
+ boolean linkResult = linkSystemLoginAccount( |
|
244 |
+ mbrId, loginId, user.getEml(), user.getMbrNm(), rgtr |
|
245 |
+ ); |
|
246 |
+ |
|
247 |
+ HashMap<String, Object> result = new HashMap<>(); |
|
248 |
+ result.put("success", linkResult); |
|
249 |
+ result.put("mbrId", mbrId); |
|
250 |
+ result.put("message", "시스템 로그인이 성공적으로 연동되었습니다."); |
|
251 |
+ |
|
252 |
+ return result; |
|
253 |
+ } catch (Exception e) { |
|
254 |
+ throw e; |
|
255 |
+ } |
|
256 |
+ } |
|
257 |
+ |
|
258 |
+ /** |
|
259 |
+ * 시스템 로그인 계정 연동 (내부 메서드) |
|
260 |
+ */ |
|
261 |
+ private boolean linkSystemLoginAccount(String mbrId, String loginId, String email, String name, String rgtr) { |
|
262 |
+ try { |
|
263 |
+ MberSocialAccountVO systemAccount = new MberSocialAccountVO(); |
|
264 |
+ systemAccount.setMbrId(mbrId); |
|
265 |
+ systemAccount.setProviderType("S"); |
|
266 |
+ systemAccount.setLoginId(loginId); |
|
267 |
+ systemAccount.setSocialEmail(email); |
|
268 |
+ systemAccount.setSocialName(name); |
|
269 |
+ systemAccount.setIsPrimaryProfile(false); |
|
270 |
+ systemAccount.setIsActive(true); |
|
271 |
+ systemAccount.setRgtr(rgtr); |
|
272 |
+ |
|
273 |
+ mberDAO.linkSocialAccount(systemAccount); |
|
274 |
+ return true; |
|
275 |
+ } catch (Exception e) { |
|
276 |
+ log.error("시스템 로그인 연동 실패", e); |
|
277 |
+ return false; |
|
183 | 278 |
} |
184 | 279 |
} |
185 | 280 |
|
... | ... | @@ -206,10 +301,8 @@ |
206 | 301 |
|
207 | 302 |
// 비밀번호 비교 후 성공 시 비밀번호 수정 후 true 반환 |
208 | 303 |
if (bCryptPasswordEncoder.matches(passwordDTO.getPswd(), mbr.getPassword())) { |
209 |
-// passwordDTO.setNewPswd(bCryptPasswordEncoder.encode(passwordDTO.getNewPswd())); |
|
210 | 304 |
passwordDTO.setMbrId(mbr.getMbrId()); |
211 | 305 |
passwordDTO.setMdfr(writer); |
212 |
-// mberDAO.updatePassword(passwordDTO); |
|
213 | 306 |
int result = updatePassword(passwordDTO); |
214 | 307 |
return true; |
215 | 308 |
// 기존 비밀번호와 입력한 비밀번호가 서로 다를 경우 false 반환 |
... | ... | @@ -222,6 +315,7 @@ |
222 | 315 |
throw e; |
223 | 316 |
} |
224 | 317 |
} |
318 |
+ |
|
225 | 319 |
/** |
226 | 320 |
* @param params |
227 | 321 |
* - 회원 아이디 |
... | ... | @@ -280,9 +374,7 @@ |
280 | 374 |
} |
281 | 375 |
} |
282 | 376 |
|
283 |
- |
|
284 |
- |
|
285 |
- // OAuth2 관련 메서드 구현 |
|
377 |
+ // OAuth2 관련 메서드 구현 (기존 메서드들 유지) |
|
286 | 378 |
|
287 | 379 |
/** |
288 | 380 |
* @param email - 이메일 |
... | ... | @@ -317,7 +409,7 @@ |
317 | 409 |
@Transactional(readOnly = true) |
318 | 410 |
public MberVO findByEmailAndProvider(String email, String mbrType) { |
319 | 411 |
try { |
320 |
- MberVO user = mberDAO.findByEmailAndProvider(email, mbrType); // 파라미터 직접 전달 |
|
412 |
+ MberVO user = mberDAO.findByEmailAndProvider(email, mbrType); |
|
321 | 413 |
|
322 | 414 |
// 권한 정보도 함께 조회 |
323 | 415 |
if (user != null) { |
... | ... | @@ -348,8 +440,8 @@ |
348 | 440 |
// OAuth2 사용자를 JoinDTO로 변환하여 기존 검증된 로직 활용 |
349 | 441 |
JoinDTO oauthJoinDTO = createOAuthJoinDTO(user); |
350 | 442 |
|
351 |
- // 기존 userJoin 메서드 활용 (검증된 로직) |
|
352 |
- HashMap<String, Object> result = userJoin(request, oauthJoinDTO); |
|
443 |
+ // 기존 userJoin 메서드 활용하되, 통합 검사는 우회 |
|
444 |
+ HashMap<String, Object> result = performStandardJoin(request, oauthJoinDTO); |
|
353 | 445 |
|
354 | 446 |
// 생성된 회원 ID 설정 |
355 | 447 |
user.setMbrId(result.get("mbrId").toString()); |
+++ src/main/java/com/takensoft/cms/mber/service/Impl/UnifiedLoginServiceImpl.java
... | ... | @@ -0,0 +1,350 @@ |
1 | +package com.takensoft.cms.mber.service.Impl; | |
2 | + | |
3 | +import com.takensoft.cms.mber.dao.MberDAO; | |
4 | +import com.takensoft.cms.mber.service.UnifiedLoginService; | |
5 | +import com.takensoft.cms.mber.vo.MberAuthorVO; | |
6 | +import com.takensoft.cms.mber.vo.MberSocialAccountVO; | |
7 | +import com.takensoft.cms.mber.vo.MberVO; | |
8 | +import com.takensoft.common.exception.*; | |
9 | +import com.takensoft.common.idgen.service.IdgenService; | |
10 | +import com.takensoft.common.util.HttpRequestUtil; | |
11 | +import lombok.RequiredArgsConstructor; | |
12 | +import lombok.extern.slf4j.Slf4j; | |
13 | +import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; | |
14 | +import org.springframework.dao.DataAccessException; | |
15 | +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | |
16 | +import org.springframework.stereotype.Service; | |
17 | +import org.springframework.transaction.annotation.Transactional; | |
18 | + | |
19 | +import jakarta.servlet.http.HttpServletRequest; | |
20 | +import java.util.ArrayList; | |
21 | +import java.util.HashMap; | |
22 | +import java.util.List; | |
23 | + | |
24 | +/** | |
25 | + * @author takensoft | |
26 | + * @since 2025.05.29 | |
27 | + * @modification | |
28 | + * since | author | description | |
29 | + * 2025.05.29 | takensoft | 최초 등록 | |
30 | + * | |
31 | + * 통합 로그인 서비스 구현체 - 순환 의존성 해결 | |
32 | + * EgovAbstractServiceImpl : 전자정부 상속 | |
33 | + * UnifiedLoginService : 통합 로그인 인터페이스 상속 | |
34 | + */ | |
35 | +@Service("unifiedLoginService") | |
36 | +@RequiredArgsConstructor | |
37 | +@Slf4j | |
38 | +public class UnifiedLoginServiceImpl extends EgovAbstractServiceImpl implements UnifiedLoginService { | |
39 | + | |
40 | + private final MberDAO mberDAO; | |
41 | + private final IdgenService mberIdgn; | |
42 | + private final HttpRequestUtil httpRequestUtil; | |
43 | + | |
44 | + // BCryptPasswordEncoder는 필요할 때 ApplicationContext에서 가져오도록 수정 | |
45 | + private BCryptPasswordEncoder passwordEncoder; | |
46 | + | |
47 | + /** | |
48 | + * BCryptPasswordEncoder 지연 초기화 (순환 의존성 해결) | |
49 | + */ | |
50 | + private BCryptPasswordEncoder getPasswordEncoder() { | |
51 | + if (passwordEncoder == null) { | |
52 | + passwordEncoder = new BCryptPasswordEncoder(); | |
53 | + } | |
54 | + return passwordEncoder; | |
55 | + } | |
56 | + | |
57 | + /** | |
58 | + * 통합 로그인 인증 | |
59 | + */ | |
60 | + @Override | |
61 | + @Transactional(readOnly = true) | |
62 | + public MberVO authenticateUser(String providerType, String identifier, String password) { | |
63 | + try { | |
64 | + HashMap<String, Object> params = new HashMap<>(); | |
65 | + params.put("providerType", providerType); | |
66 | + params.put("identifier", identifier); | |
67 | + | |
68 | + MberVO user = mberDAO.findByUnifiedLogin(params); | |
69 | + | |
70 | + if (user == null) { | |
71 | + throw new CustomNotFoundException("사용자를 찾을 수 없습니다."); | |
72 | + } | |
73 | + | |
74 | + // 시스템 로그인의 경우 비밀번호 검증 | |
75 | + if ("S".equals(providerType)) { | |
76 | + if (password == null || !getPasswordEncoder().matches(password, user.getPassword())) { | |
77 | + throw new CustomPasswordComparisonException("비밀번호가 일치하지 않습니다."); | |
78 | + } | |
79 | + } | |
80 | + | |
81 | + return user; | |
82 | + } catch (DataAccessException dae) { | |
83 | + throw dae; | |
84 | + } catch (Exception e) { | |
85 | + throw e; | |
86 | + } | |
87 | + } | |
88 | + | |
89 | + /** | |
90 | + * OAuth2 사용자 처리 (가입 또는 연동) | |
91 | + */ | |
92 | + @Override | |
93 | + @Transactional(rollbackFor = Exception.class) | |
94 | + public MberVO processOAuth2User(String email, String providerType, String socialId, String name, HttpServletRequest request) { | |
95 | + try { | |
96 | + // 1. 이메일로 기존 계정 검색 | |
97 | + MberVO existingUser = mberDAO.findAllAccountsByEmail(email); | |
98 | + | |
99 | + if (existingUser != null) { | |
100 | + // 2-1. 기존 계정 있음 - 해당 소셜이 이미 연동되어 있는지 확인 | |
101 | + HashMap<String, Object> params = new HashMap<>(); | |
102 | + params.put("mbrId", existingUser.getMbrId()); | |
103 | + params.put("providerType", providerType); | |
104 | + | |
105 | + MberSocialAccountVO existingSocial = mberDAO.findSocialAccountByProvider(params); | |
106 | + | |
107 | + if (existingSocial != null) { | |
108 | + // 이미 연동되어 있음 - 정보만 업데이트 | |
109 | + existingSocial.setSocialName(name); | |
110 | + existingSocial.setMdfr("OAUTH2_UPDATE"); | |
111 | + updateSocialAccountInfo(existingSocial); | |
112 | + return existingUser; | |
113 | + } else { | |
114 | + // 새로운 소셜 계정 연동 | |
115 | + linkAccount(existingUser.getMbrId(), providerType, socialId, null, email, name); | |
116 | + return existingUser; | |
117 | + } | |
118 | + } else { | |
119 | + // 2-2. 기존 계정 없음 - 새 계정 생성 | |
120 | + return createNewOAuth2User(email, providerType, socialId, name, request); | |
121 | + } | |
122 | + } catch (DataAccessException dae) { | |
123 | + throw dae; | |
124 | + } catch (Exception e) { | |
125 | + throw e; | |
126 | + } | |
127 | + } | |
128 | + | |
129 | + /** | |
130 | + * 새로운 OAuth2 사용자 생성 | |
131 | + */ | |
132 | + private MberVO createNewOAuth2User(String email, String providerType, String socialId, String name, HttpServletRequest request) { | |
133 | + try { | |
134 | + // 회원 ID 생성 | |
135 | + String mbrId = mberIdgn.getNextStringId(); | |
136 | + | |
137 | + // 새 사용자 정보 설정 | |
138 | + MberVO newUser = new MberVO(); | |
139 | + newUser.setMbrId(mbrId); | |
140 | + newUser.setEml(email); | |
141 | + newUser.setLgnId(email.toLowerCase()); | |
142 | + newUser.setMbrNm(name); | |
143 | + newUser.setNcnm(name); | |
144 | + newUser.setMbrType(providerType); | |
145 | + newUser.setMbrStts("1"); | |
146 | + newUser.setUseYn(true); | |
147 | + newUser.setSysPvsnYn("1"); | |
148 | + newUser.setRgtr("OAUTH2_SYSTEM"); | |
149 | + | |
150 | + // 회원 정보 저장 | |
151 | + int result = mberDAO.saveOAuthUser(newUser); | |
152 | + if (result == 0) { | |
153 | + throw new CustomInsertFailException("OAuth2 사용자 저장에 실패했습니다."); | |
154 | + } | |
155 | + | |
156 | + // 기본 권한 등록 | |
157 | + MberAuthorVO userRole = new MberAuthorVO(); | |
158 | + userRole.setMbrId(mbrId); | |
159 | + userRole.setAuthrtCd("ROLE_USER"); | |
160 | + userRole.setRgtr("OAUTH2_SYSTEM"); | |
161 | + mberDAO.authorSave(userRole); | |
162 | + | |
163 | + // 소셜 계정 정보 저장 | |
164 | + MberSocialAccountVO socialAccount = new MberSocialAccountVO(); | |
165 | + socialAccount.setMbrId(mbrId); | |
166 | + socialAccount.setProviderType(providerType); | |
167 | + socialAccount.setSocialId(socialId); | |
168 | + socialAccount.setSocialEmail(email); | |
169 | + socialAccount.setSocialName(name); | |
170 | + socialAccount.setIsPrimaryProfile(true); | |
171 | + socialAccount.setIsActive(true); | |
172 | + socialAccount.setRgtr("OAUTH2_SYSTEM"); | |
173 | + | |
174 | + mberDAO.saveSocialAccount(socialAccount); | |
175 | + | |
176 | + return newUser; | |
177 | + } catch (Exception e) { | |
178 | + throw new CustomInsertFailException("OAuth2 사용자 생성에 실패했습니다: " + e.getMessage()); | |
179 | + } | |
180 | + } | |
181 | + | |
182 | + /** | |
183 | + * 계정 연동 | |
184 | + */ | |
185 | + @Override | |
186 | + @Transactional(rollbackFor = Exception.class) | |
187 | + public boolean linkAccount(String mbrId, String providerType, String socialId, String loginId, String email, String name) { | |
188 | + try { | |
189 | + // 중복 연동 확인 | |
190 | + HashMap<String, Object> params = new HashMap<>(); | |
191 | + params.put("mbrId", mbrId); | |
192 | + params.put("providerType", providerType); | |
193 | + | |
194 | + MberSocialAccountVO existing = mberDAO.findSocialAccountByProvider(params); | |
195 | + if (existing != null && existing.getIsActive()) { | |
196 | + throw new CustomIdTakenException("이미 연동된 계정입니다."); | |
197 | + } | |
198 | + | |
199 | + MberSocialAccountVO socialAccount = new MberSocialAccountVO(); | |
200 | + socialAccount.setMbrId(mbrId); | |
201 | + socialAccount.setProviderType(providerType); | |
202 | + socialAccount.setSocialId(socialId); | |
203 | + socialAccount.setLoginId(loginId); | |
204 | + socialAccount.setSocialEmail(email); | |
205 | + socialAccount.setSocialName(name); | |
206 | + socialAccount.setIsPrimaryProfile(false); | |
207 | + socialAccount.setIsActive(true); | |
208 | + socialAccount.setRgtr("LINK_SYSTEM"); | |
209 | + | |
210 | + mberDAO.linkSocialAccount(socialAccount); | |
211 | + return true; | |
212 | + } catch (DataAccessException dae) { | |
213 | + throw dae; | |
214 | + } catch (Exception e) { | |
215 | + throw e; | |
216 | + } | |
217 | + } | |
218 | + | |
219 | + /** | |
220 | + * 계정 연동 해지 | |
221 | + */ | |
222 | + @Override | |
223 | + @Transactional(rollbackFor = Exception.class) | |
224 | + public boolean unlinkAccount(String mbrId, String providerType) { | |
225 | + try { | |
226 | + // 연동된 계정 개수 확인 (최소 1개는 유지해야 함) | |
227 | + List<MberSocialAccountVO> linkedAccounts = getLinkedAccounts(mbrId); | |
228 | + if (linkedAccounts.size() <= 1) { | |
229 | + throw new CustomNotFoundException("최소 하나의 로그인 방법은 유지해야 합니다."); | |
230 | + } | |
231 | + | |
232 | + HashMap<String, Object> params = new HashMap<>(); | |
233 | + params.put("mbrId", mbrId); | |
234 | + params.put("providerType", providerType); | |
235 | + params.put("mdfr", "UNLINK_SYSTEM"); | |
236 | + | |
237 | + mberDAO.unlinkSocialAccount(params); | |
238 | + | |
239 | + // 해지된 계정이 메인 프로필이었다면 다른 계정을 메인으로 설정 | |
240 | + MberSocialAccountVO unlinkedAccount = linkedAccounts.stream() | |
241 | + .filter(acc -> acc.getProviderType().equals(providerType)) | |
242 | + .findFirst() | |
243 | + .orElse(null); | |
244 | + | |
245 | + if (unlinkedAccount != null && unlinkedAccount.getIsPrimaryProfile()) { | |
246 | + // 첫 번째 활성 계정을 메인으로 설정 | |
247 | + MberSocialAccountVO newPrimary = linkedAccounts.stream() | |
248 | + .filter(acc -> !acc.getProviderType().equals(providerType)) | |
249 | + .findFirst() | |
250 | + .orElse(null); | |
251 | + | |
252 | + if (newPrimary != null) { | |
253 | + setPrimaryProfile(mbrId, newPrimary.getProviderType()); | |
254 | + } | |
255 | + } | |
256 | + | |
257 | + return true; | |
258 | + } catch (DataAccessException dae) { | |
259 | + throw dae; | |
260 | + } catch (Exception e) { | |
261 | + throw e; | |
262 | + } | |
263 | + } | |
264 | + | |
265 | + /** | |
266 | + * 연동된 계정 목록 조회 | |
267 | + */ | |
268 | + @Override | |
269 | + @Transactional(readOnly = true) | |
270 | + public List<MberSocialAccountVO> getLinkedAccounts(String mbrId) { | |
271 | + try { | |
272 | + return mberDAO.findSocialAccountsByMbrId(mbrId); | |
273 | + } catch (DataAccessException dae) { | |
274 | + throw dae; | |
275 | + } catch (Exception e) { | |
276 | + throw e; | |
277 | + } | |
278 | + } | |
279 | + | |
280 | + /** | |
281 | + * 메인 프로필 설정 | |
282 | + */ | |
283 | + @Override | |
284 | + @Transactional(rollbackFor = Exception.class) | |
285 | + public boolean setPrimaryProfile(String mbrId, String providerType) { | |
286 | + try { | |
287 | + HashMap<String, Object> params = new HashMap<>(); | |
288 | + params.put("mbrId", mbrId); | |
289 | + params.put("providerType", providerType); | |
290 | + params.put("mdfr", "PROFILE_UPDATE"); | |
291 | + | |
292 | + mberDAO.setPrimaryProfile(params); | |
293 | + return true; | |
294 | + } catch (DataAccessException dae) { | |
295 | + throw dae; | |
296 | + } catch (Exception e) { | |
297 | + throw e; | |
298 | + } | |
299 | + } | |
300 | + | |
301 | + /** | |
302 | + * 계정 통합 제안 | |
303 | + */ | |
304 | + @Override | |
305 | + @Transactional(readOnly = true) | |
306 | + public HashMap<String, Object> suggestAccountMerge(String email, String newProviderType) { | |
307 | + try { | |
308 | + HashMap<String, Object> result = new HashMap<>(); | |
309 | + | |
310 | + // 이메일로 기존 계정 검색 | |
311 | + MberVO existingUser = mberDAO.findAllAccountsByEmail(email); | |
312 | + | |
313 | + if (existingUser != null) { | |
314 | + // 기존 계정의 연동 정보 조회 | |
315 | + List<MberSocialAccountVO> linkedAccounts = getLinkedAccounts(existingUser.getMbrId()); | |
316 | + | |
317 | + // 새로운 제공자가 이미 연동되어 있는지 확인 | |
318 | + boolean alreadyLinked = linkedAccounts.stream() | |
319 | + .anyMatch(acc -> acc.getProviderType().equals(newProviderType)); | |
320 | + | |
321 | + result.put("hasExistingAccount", true); | |
322 | + result.put("mbrId", existingUser.getMbrId()); | |
323 | + result.put("mbrNm", existingUser.getMbrNm()); | |
324 | + result.put("linkedAccounts", linkedAccounts); | |
325 | + result.put("alreadyLinked", alreadyLinked); | |
326 | + result.put("canLink", !alreadyLinked); | |
327 | + } else { | |
328 | + result.put("hasExistingAccount", false); | |
329 | + result.put("canCreateNew", true); | |
330 | + } | |
331 | + | |
332 | + return result; | |
333 | + } catch (DataAccessException dae) { | |
334 | + throw dae; | |
335 | + } catch (Exception e) { | |
336 | + throw e; | |
337 | + } | |
338 | + } | |
339 | + | |
340 | + /** | |
341 | + * 소셜 계정 정보 업데이트 | |
342 | + */ | |
343 | + private void updateSocialAccountInfo(MberSocialAccountVO socialAccount) { | |
344 | + try { | |
345 | + mberDAO.linkSocialAccount(socialAccount); | |
346 | + } catch (Exception e) { | |
347 | + log.warn("소셜 계정 정보 업데이트 실패", e); | |
348 | + } | |
349 | + } | |
350 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/cms/mber/service/UnifiedLoginService.java
... | ... | @@ -0,0 +1,83 @@ |
1 | +package com.takensoft.cms.mber.service; | |
2 | + | |
3 | +import com.takensoft.cms.mber.vo.MberSocialAccountVO; | |
4 | +import com.takensoft.cms.mber.vo.MberVO; | |
5 | +import jakarta.servlet.http.HttpServletRequest; | |
6 | + | |
7 | +import java.util.HashMap; | |
8 | +import java.util.List; | |
9 | + | |
10 | +/** | |
11 | + * @author takensoft | |
12 | + * @since 2025.05.29 | |
13 | + * @modification | |
14 | + * since | author | description | |
15 | + * 2025.05.29 | takensoft | 최초 등록 | |
16 | + * | |
17 | + * 통합 로그인 서비스 인터페이스 | |
18 | + */ | |
19 | +public interface UnifiedLoginService { | |
20 | + | |
21 | + /** | |
22 | + * 통합 로그인 인증 | |
23 | + * @param providerType 제공자 타입 (SYSTEM, KAKAO, NAVER, GOOGLE) | |
24 | + * @param identifier 식별자 (로그인ID 또는 소셜ID) | |
25 | + * @param password 비밀번호 (시스템 로그인시만 필요) | |
26 | + * @return MberVO 인증된 사용자 정보 | |
27 | + */ | |
28 | + MberVO authenticateUser(String providerType, String identifier, String password); | |
29 | + | |
30 | + /** | |
31 | + * OAuth2 사용자 처리 (가입 또는 연동) | |
32 | + * @param email 이메일 | |
33 | + * @param providerType 제공자 타입 | |
34 | + * @param socialId 소셜 고유 ID | |
35 | + * @param name 이름 | |
36 | + * @param request HTTP 요청 객체 | |
37 | + * @return MberVO 처리된 사용자 정보 | |
38 | + */ | |
39 | + MberVO processOAuth2User(String email, String providerType, String socialId, String name, HttpServletRequest request); | |
40 | + | |
41 | + /** | |
42 | + * 계정 연동 | |
43 | + * @param mbrId 회원 ID | |
44 | + * @param providerType 제공자 타입 | |
45 | + * @param socialId 소셜 ID (시스템일 경우 null) | |
46 | + * @param loginId 로그인 ID (소셜일 경우 null) | |
47 | + * @param email 이메일 | |
48 | + * @param name 이름 | |
49 | + * @return boolean 연동 성공 여부 | |
50 | + */ | |
51 | + boolean linkAccount(String mbrId, String providerType, String socialId, String loginId, String email, String name); | |
52 | + | |
53 | + /** | |
54 | + * 계정 연동 해지 | |
55 | + * @param mbrId 회원 ID | |
56 | + * @param providerType 제공자 타입 | |
57 | + * @return boolean 해지 성공 여부 | |
58 | + */ | |
59 | + boolean unlinkAccount(String mbrId, String providerType); | |
60 | + | |
61 | + /** | |
62 | + * 연동된 계정 목록 조회 | |
63 | + * @param mbrId 회원 ID | |
64 | + * @return List<MberSocialAccountVO> 연동된 계정 목록 | |
65 | + */ | |
66 | + List<MberSocialAccountVO> getLinkedAccounts(String mbrId); | |
67 | + | |
68 | + /** | |
69 | + * 메인 프로필 설정 | |
70 | + * @param mbrId 회원 ID | |
71 | + * @param providerType 제공자 타입 | |
72 | + * @return boolean 설정 성공 여부 | |
73 | + */ | |
74 | + boolean setPrimaryProfile(String mbrId, String providerType); | |
75 | + | |
76 | + /** | |
77 | + * 계정 통합 제안 | |
78 | + * @param email 이메일 | |
79 | + * @param newProviderType 새로운 제공자 타입 | |
80 | + * @return HashMap<String, Object> 통합 제안 정보 | |
81 | + */ | |
82 | + HashMap<String, Object> suggestAccountMerge(String email, String newProviderType); | |
83 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/cms/mber/vo/MberSocialAccountVO.java
... | ... | @@ -0,0 +1,42 @@ |
1 | +package com.takensoft.cms.mber.vo; | |
2 | + | |
3 | +import lombok.AllArgsConstructor; | |
4 | +import lombok.Getter; | |
5 | +import lombok.NoArgsConstructor; | |
6 | +import lombok.Setter; | |
7 | + | |
8 | +import java.time.LocalDateTime; | |
9 | + | |
10 | +/** | |
11 | + * @author takensoft | |
12 | + * @since 2025.05.29 | |
13 | + * @modification | |
14 | + * since | author | description | |
15 | + * 2025.05.29 | takensoft | 최초 등록 | |
16 | + * | |
17 | + * 소셜 계정 연동 정보 VO | |
18 | + */ | |
19 | +@Getter | |
20 | +@Setter | |
21 | +@NoArgsConstructor | |
22 | +@AllArgsConstructor | |
23 | +public class MberSocialAccountVO { | |
24 | + | |
25 | + private Long id; // 연동 ID | |
26 | + private String mbrId; // 회원 ID | |
27 | + private String providerType; // 제공자 타입 (SYSTEM, KAKAO, NAVER, GOOGLE) | |
28 | + private String socialId; // 소셜 로그인 고유 ID | |
29 | + private String loginId; // 시스템 로그인 ID | |
30 | + private String socialEmail; // 소셜 계정 이메일 | |
31 | + private String socialName; // 소셜 계정 이름 | |
32 | + private Boolean isPrimaryProfile; // 메인 프로필 여부 | |
33 | + private Boolean isActive; // 연동 활성화 여부 | |
34 | + private LocalDateTime linkedDt; // 연동일시 | |
35 | + private LocalDateTime unlinkedDt; // 연동 해지일시 | |
36 | + private String rgtr; // 등록자 | |
37 | + private LocalDateTime regDt; // 등록일시 | |
38 | + private String mdfr; // 수정자 | |
39 | + private LocalDateTime mdfcnDt; // 수정일시 | |
40 | + | |
41 | + | |
42 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/cms/mber/web/UnifiedLoginController.java
... | ... | @@ -0,0 +1,133 @@ |
1 | +package com.takensoft.cms.mber.web; | |
2 | + | |
3 | +import com.takensoft.cms.mber.service.UnifiedLoginService; | |
4 | +import com.takensoft.cms.mber.vo.MberSocialAccountVO; | |
5 | +import com.takensoft.common.message.MessageCode; | |
6 | +import com.takensoft.common.service.VerificationService; | |
7 | +import com.takensoft.common.util.ResponseUtil; | |
8 | +import lombok.RequiredArgsConstructor; | |
9 | +import lombok.extern.slf4j.Slf4j; | |
10 | +import org.springframework.http.ResponseEntity; | |
11 | +import org.springframework.web.bind.annotation.*; | |
12 | + | |
13 | +import java.util.HashMap; | |
14 | +import java.util.List; | |
15 | + | |
16 | +/** | |
17 | + * @author takensoft | |
18 | + * @since 2025.05.29 | |
19 | + * @modification | |
20 | + * since | author | description | |
21 | + * 2025.05.29 | takensoft | 최초 등록 | |
22 | + * | |
23 | + * 통합 로그인 관련 컨트롤러 | |
24 | + */ | |
25 | +@RestController | |
26 | +@RequiredArgsConstructor | |
27 | +@Slf4j | |
28 | +@RequestMapping(value = "/mbr/unified") | |
29 | +public class UnifiedLoginController { | |
30 | + | |
31 | + private final UnifiedLoginService unifiedLoginService; | |
32 | + private final VerificationService verificationService; | |
33 | + private final ResponseUtil resUtil; | |
34 | + | |
35 | + /** | |
36 | + * 연동된 계정 목록 조회 | |
37 | + */ | |
38 | + @PostMapping("/linkedAccounts.json") | |
39 | + public ResponseEntity<?> getLinkedAccounts() { | |
40 | + try { | |
41 | + String currentUserId = verificationService.getCurrentUserId(); | |
42 | + List<MberSocialAccountVO> linkedAccounts = unifiedLoginService.getLinkedAccounts(currentUserId); | |
43 | + | |
44 | + return resUtil.successRes(linkedAccounts, MessageCode.COMMON_SUCCESS); | |
45 | + } catch (Exception e) { | |
46 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
47 | + } | |
48 | + } | |
49 | + | |
50 | + /** | |
51 | + * 계정 연동 | |
52 | + */ | |
53 | + @PostMapping("/linkAccount.json") | |
54 | + public ResponseEntity<?> linkAccount(@RequestBody HashMap<String, String> params) { | |
55 | + try { | |
56 | + String currentUserId = verificationService.getCurrentUserId(); | |
57 | + String providerType = params.get("providerType"); | |
58 | + String socialId = params.get("socialId"); | |
59 | + String loginId = params.get("loginId"); | |
60 | + String email = params.get("email"); | |
61 | + String name = params.get("name"); | |
62 | + | |
63 | + boolean success = unifiedLoginService.linkAccount(currentUserId, providerType, socialId, loginId, email, name); | |
64 | + | |
65 | + if (success) { | |
66 | + return resUtil.successRes("계정 연동이 완료되었습니다.", MessageCode.COMMON_SUCCESS); | |
67 | + } else { | |
68 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
69 | + } | |
70 | + } catch (Exception e) { | |
71 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
72 | + } | |
73 | + } | |
74 | + | |
75 | + /** | |
76 | + * 계정 연동 해지 | |
77 | + */ | |
78 | + @PostMapping("/unlinkAccount.json") | |
79 | + public ResponseEntity<?> unlinkAccount(@RequestBody HashMap<String, String> params) { | |
80 | + try { | |
81 | + String currentUserId = verificationService.getCurrentUserId(); | |
82 | + String providerType = params.get("providerType"); | |
83 | + | |
84 | + boolean success = unifiedLoginService.unlinkAccount(currentUserId, providerType); | |
85 | + | |
86 | + if (success) { | |
87 | + return resUtil.successRes("계정 연동이 해지되었습니다.", MessageCode.COMMON_SUCCESS); | |
88 | + } else { | |
89 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
90 | + } | |
91 | + } catch (Exception e) { | |
92 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
93 | + } | |
94 | + } | |
95 | + | |
96 | + /** | |
97 | + * 메인 프로필 설정 | |
98 | + */ | |
99 | + @PostMapping("/setPrimaryProfile.json") | |
100 | + public ResponseEntity<?> setPrimaryProfile(@RequestBody HashMap<String, String> params) { | |
101 | + try { | |
102 | + String currentUserId = verificationService.getCurrentUserId(); | |
103 | + String providerType = params.get("providerType"); | |
104 | + | |
105 | + boolean success = unifiedLoginService.setPrimaryProfile(currentUserId, providerType); | |
106 | + | |
107 | + if (success) { | |
108 | + return resUtil.successRes("메인 프로필이 설정되었습니다.", MessageCode.COMMON_SUCCESS); | |
109 | + } else { | |
110 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
111 | + } | |
112 | + } catch (Exception e) { | |
113 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
114 | + } | |
115 | + } | |
116 | + | |
117 | + /** | |
118 | + * 계정 통합 제안 조회 | |
119 | + */ | |
120 | + @PostMapping("/suggestMerge.json") | |
121 | + public ResponseEntity<?> suggestAccountMerge(@RequestBody HashMap<String, String> params) { | |
122 | + try { | |
123 | + String email = params.get("email"); | |
124 | + String newProviderType = params.get("providerType"); | |
125 | + | |
126 | + HashMap<String, Object> suggestion = unifiedLoginService.suggestAccountMerge(email, newProviderType); | |
127 | + | |
128 | + return resUtil.successRes(suggestion, MessageCode.COMMON_SUCCESS); | |
129 | + } catch (Exception e) { | |
130 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
131 | + } | |
132 | + } | |
133 | +}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/config/SecurityConfig.java
+++ src/main/java/com/takensoft/common/config/SecurityConfig.java
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 |
import com.takensoft.cms.loginPolicy.service.Email2ndAuthService; |
6 | 6 |
import com.takensoft.cms.loginPolicy.service.LoginModeService; |
7 | 7 |
import com.takensoft.cms.loginPolicy.service.LoginPolicyService; |
8 |
+import com.takensoft.cms.mber.service.UnifiedLoginService; |
|
8 | 9 |
import com.takensoft.common.filter.*; |
9 | 10 |
import com.takensoft.common.util.HttpRequestUtil; |
10 | 11 |
import com.takensoft.common.exception.CustomAccessDenieHandler; |
... | ... | @@ -43,8 +44,9 @@ |
43 | 44 |
* 2025.01.22 | takensoft | 최초 등록 |
44 | 45 |
* 2025.05.22 | takensoft | OAuth2 로그인 추가 |
45 | 46 |
* 2025.05.26 | 하석형 | 로그인 유틸 추가 |
47 |
+ * 2025.05.29 | takensoft | 통합 로그인 시스템 적용 |
|
46 | 48 |
* |
47 |
- * Spring Security 설정을 위한 Config |
|
49 |
+ * Spring Security 설정을 위한 Config - 통합 로그인 시스템 적용 |
|
48 | 50 |
*/ |
49 | 51 |
@Configuration |
50 | 52 |
@EnableWebSecurity |
... | ... | @@ -64,6 +66,7 @@ |
64 | 66 |
private final EmailServiceImpl emailServiceImpl; |
65 | 67 |
private final LoginUtil loginUtil; |
66 | 68 |
private final Email2ndAuthService email2ndAuth; |
69 |
+ private final UnifiedLoginService unifiedLoginService; |
|
67 | 70 |
|
68 | 71 |
@Autowired |
69 | 72 |
private CustomOAuth2UserServiceImpl customOAuth2UserServiceImpl; |
... | ... | @@ -82,7 +85,7 @@ |
82 | 85 |
* SecurityConfig 생성자 |
83 | 86 |
*/ |
84 | 87 |
public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, CntxtPthService cntxtPthService, AccesCtrlService accesCtrlService, AppConfig appConfig, CustomAuthenticationEntryPoint authenticationEntryPoint, CustomAccessDenieHandler accessDenieHandler, |
85 |
- HttpRequestUtil httpRequestUtil, LoginModeService loginModeService, LoginPolicyService loginPolicyService, EmailServiceImpl emailServiceImpl, @Value("${front.url}") String fUrl, RedisTemplate<String, String> redisTemplate, LoginUtil loginUtil, Email2ndAuthService email2ndAuth) { |
|
88 |
+ HttpRequestUtil httpRequestUtil, LoginModeService loginModeService, LoginPolicyService loginPolicyService, EmailServiceImpl emailServiceImpl, @Value("${front.url}") String fUrl, RedisTemplate<String, String> redisTemplate, LoginUtil loginUtil, Email2ndAuthService email2ndAuth, UnifiedLoginService unifiedLoginService) { |
|
86 | 89 |
this.authenticationConfiguration = authenticationConfiguration; |
87 | 90 |
this.cntxtPthService = cntxtPthService; |
88 | 91 |
this.accesCtrlService = accesCtrlService; |
... | ... | @@ -98,6 +101,7 @@ |
98 | 101 |
this.emailServiceImpl = emailServiceImpl; |
99 | 102 |
this.loginUtil = loginUtil; |
100 | 103 |
this.email2ndAuth = email2ndAuth; |
104 |
+ this.unifiedLoginService = unifiedLoginService; |
|
101 | 105 |
} |
102 | 106 |
|
103 | 107 |
/** |
... | ... | @@ -192,8 +196,7 @@ |
192 | 196 |
http.addFilterBefore(new AccesFilter(accesCtrlService, httpRequestUtil, appConfig), JWTFilter.class); |
193 | 197 |
|
194 | 198 |
// 로그인 필터 |
195 |
- http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), emailServiceImpl, loginUtil, email2ndAuth), UsernamePasswordAuthenticationFilter.class); |
|
196 |
- |
|
199 |
+ http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), emailServiceImpl, loginUtil, email2ndAuth, unifiedLoginService), UsernamePasswordAuthenticationFilter.class); |
|
197 | 200 |
|
198 | 201 |
return http.build(); |
199 | 202 |
} |
--- src/main/java/com/takensoft/common/filter/JWTFilter.java
+++ src/main/java/com/takensoft/common/filter/JWTFilter.java
... | ... | @@ -27,16 +27,16 @@ |
27 | 27 |
import java.io.IOException; |
28 | 28 |
import java.time.LocalDateTime; |
29 | 29 |
import java.util.List; |
30 |
+ |
|
30 | 31 |
/** |
31 | 32 |
* @author takensoft |
32 | 33 |
* @since 2024.04.01 |
33 | 34 |
* @modification |
34 | 35 |
* since | author | description |
35 | 36 |
* 2024.04.01 | takensoft | 최초 등록 |
37 |
+ * 2025.05.30 | takensoft | 모드별 명확한 분기 처리, Redis 통합 |
|
36 | 38 |
* |
37 |
- * OncePerRequestFilter - 한 번의 요청마다 단 한 번만 필터링 작업을 수행하는 필터를 제공하는 클래스 |
|
38 |
- * |
|
39 |
- * JWT 토큰 검증 Filter |
|
39 |
+ * JWT 토큰 검증 Filter - 모드별 명확한 분기 처리 |
|
40 | 40 |
*/ |
41 | 41 |
public class JWTFilter extends OncePerRequestFilter { |
42 | 42 |
|
... | ... | @@ -48,65 +48,36 @@ |
48 | 48 |
private final RedisTemplate<String, String> redisTemplate; |
49 | 49 |
private final LoginModeService loginModeService; |
50 | 50 |
|
51 |
- /** |
|
52 |
- * @param jwtUtil JWT 유틸리티 클래스의 인스턴스 |
|
53 |
- * |
|
54 |
- * JWTFilter 생성자 |
|
55 |
- */ |
|
56 |
- public JWTFilter(JWTUtil jwtUtil, AppConfig appConfig, LoginModeService loginModeService, LoginPolicyService loginPolicyService, RedisTemplate<String, String> redisTemplate) { |
|
51 |
+ public JWTFilter(JWTUtil jwtUtil, AppConfig appConfig, LoginModeService loginModeService, |
|
52 |
+ LoginPolicyService loginPolicyService, RedisTemplate<String, String> redisTemplate) { |
|
57 | 53 |
this.jwtUtil = jwtUtil; |
58 | 54 |
this.appConfig = appConfig; |
59 | 55 |
this.loginModeService = loginModeService; |
60 | 56 |
this.loginPolicyService = loginPolicyService; |
61 | 57 |
this.redisTemplate = redisTemplate; |
62 | 58 |
} |
63 |
- /** |
|
64 |
- * @param request HttpServletRequest 객체 |
|
65 |
- * @param response HttpServletResponse 객체 |
|
66 |
- * @param filterChain 필터 체인을 통해 다음 필터로 요청을 전달 |
|
67 |
- * @throws ServletException 필터 처리 중 발생한 서블릿 예외 |
|
68 |
- * @throws IOException 필터 처리 중 발생한 IO 예외 |
|
69 |
- * |
|
70 |
- * JWT 토큰 검증 |
|
71 |
- */ |
|
59 |
+ |
|
72 | 60 |
@Override |
73 | 61 |
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { |
74 | 62 |
String requestURI = request.getRequestURI(); |
75 | 63 |
|
76 |
- // OAuth2 관련 경로는 JWT 검증 제외 |
|
64 |
+ // OAuth2 관련 경로는 검증 제외 |
|
77 | 65 |
if (isOAuth2Request(requestURI)) { |
78 | 66 |
filterChain.doFilter(request, response); |
79 | 67 |
return; |
80 | 68 |
} |
69 |
+ |
|
81 | 70 |
try { |
82 | 71 |
String loginMode = loginModeService.getLoginMode(); |
83 |
- String accessToken = resolveToken(request, loginMode); |
|
84 | 72 |
|
85 |
- if (accessToken == null) { |
|
86 |
- filterChain.doFilter(request, response); |
|
87 |
- return; |
|
73 |
+ if ("S".equals(loginMode)) { |
|
74 |
+ // 세션 모드: 세션 기반 검증 및 Redis 중복로그인 체크 |
|
75 |
+ handleSessionMode(request, response, filterChain); |
|
76 |
+ } else { |
|
77 |
+ // JWT 모드: 토큰 기반 검증 |
|
78 |
+ handleJwtMode(request, response, filterChain); |
|
88 | 79 |
} |
89 | 80 |
|
90 |
- if ((Boolean) jwtUtil.getClaim(accessToken, "isExpired")) { |
|
91 |
- sendTokenExpiredResponse(response, request); |
|
92 |
- return; |
|
93 |
- } |
|
94 |
- |
|
95 |
- MberVO mber = new MberVO( |
|
96 |
- (String) jwtUtil.getClaim(accessToken, "mbrId"), |
|
97 |
- (String) jwtUtil.getClaim(accessToken, "lgnId"), |
|
98 |
- (List<MberAuthorVO>) jwtUtil.getClaim(accessToken, "roles") |
|
99 |
- ); |
|
100 |
- |
|
101 |
- if (!loginPolicyService.getPolicy() && !isTokenValid(mber.getMbrId(), accessToken)) { |
|
102 |
- sendTokenExpiredResponse(response, request); |
|
103 |
- return; |
|
104 |
- } |
|
105 |
- |
|
106 |
- Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities()); |
|
107 |
- SecurityContextHolder.getContext().setAuthentication(authToken); |
|
108 |
- |
|
109 |
- filterChain.doFilter(request, response); |
|
110 | 81 |
} catch (ExpiredJwtException e) { |
111 | 82 |
FilterExceptionHandler.jwtError(response, e); |
112 | 83 |
} catch (JwtException e) { |
... | ... | @@ -118,13 +89,90 @@ |
118 | 89 |
} |
119 | 90 |
} |
120 | 91 |
|
121 |
- private String resolveToken(HttpServletRequest request, String loginMode) { |
|
122 |
- if ("S".equals(loginMode)) { |
|
123 |
- HttpSession session = request.getSession(false); |
|
124 |
- return session != null ? (String) session.getAttribute(SESSION_JWT_KEY) : null; |
|
125 |
- } else { |
|
126 |
- return request.getHeader(AUTHORIZATION_HEADER); |
|
92 |
+ /** |
|
93 |
+ * 세션 모드 처리 - Redis 기반 중복로그인 체크 |
|
94 |
+ */ |
|
95 |
+ private void handleSessionMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { |
|
96 |
+ HttpSession session = request.getSession(false); |
|
97 |
+ |
|
98 |
+ if (session == null) { |
|
99 |
+ filterChain.doFilter(request, response); |
|
100 |
+ return; |
|
127 | 101 |
} |
102 |
+ |
|
103 |
+ String mbrId = (String) session.getAttribute("mbrId"); |
|
104 |
+ if (mbrId == null) { |
|
105 |
+ filterChain.doFilter(request, response); |
|
106 |
+ return; |
|
107 |
+ } |
|
108 |
+ |
|
109 |
+ // Redis 기반 중복로그인 체크 (세션 모드) |
|
110 |
+ if (!loginPolicyService.getPolicy()) { |
|
111 |
+ String sessionKey = "session:" + mbrId; |
|
112 |
+ String storedSessionId = redisTemplate.opsForValue().get(sessionKey); |
|
113 |
+ |
|
114 |
+ if (storedSessionId == null || !storedSessionId.equals(session.getId())) { |
|
115 |
+ // 다른 곳에서 로그인했거나 세션이 만료됨 |
|
116 |
+ try { |
|
117 |
+ session.invalidate(); |
|
118 |
+ } catch (IllegalStateException e) { |
|
119 |
+ // 이미 무효화된 세션 |
|
120 |
+ } |
|
121 |
+ sendSessionExpiredResponse(response, request); |
|
122 |
+ return; |
|
123 |
+ } |
|
124 |
+ } |
|
125 |
+ |
|
126 |
+ // 세션 정보로 Authentication 설정 |
|
127 |
+ try { |
|
128 |
+ List<MberAuthorVO> roles = (List<MberAuthorVO>) session.getAttribute("roles"); |
|
129 |
+ String lgnId = (String) session.getAttribute("lgnId"); |
|
130 |
+ String mbrNm = (String) session.getAttribute("mbrNm"); |
|
131 |
+ |
|
132 |
+ MberVO mber = new MberVO(mbrId, lgnId, roles); |
|
133 |
+ mber.setMbrNm(mbrNm); |
|
134 |
+ |
|
135 |
+ Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities()); |
|
136 |
+ SecurityContextHolder.getContext().setAuthentication(authToken); |
|
137 |
+ |
|
138 |
+ filterChain.doFilter(request, response); |
|
139 |
+ } catch (Exception e) { |
|
140 |
+ sendSessionExpiredResponse(response, request); |
|
141 |
+ } |
|
142 |
+ } |
|
143 |
+ |
|
144 |
+ /** |
|
145 |
+ * JWT 모드 처리 - 기존 로직 유지 |
|
146 |
+ */ |
|
147 |
+ private void handleJwtMode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { |
|
148 |
+ String accessToken = request.getHeader(AUTHORIZATION_HEADER); |
|
149 |
+ |
|
150 |
+ if (accessToken == null) { |
|
151 |
+ filterChain.doFilter(request, response); |
|
152 |
+ return; |
|
153 |
+ } |
|
154 |
+ |
|
155 |
+ if ((Boolean) jwtUtil.getClaim(accessToken, "isExpired")) { |
|
156 |
+ sendTokenExpiredResponse(response, request); |
|
157 |
+ return; |
|
158 |
+ } |
|
159 |
+ |
|
160 |
+ MberVO mber = new MberVO( |
|
161 |
+ (String) jwtUtil.getClaim(accessToken, "mbrId"), |
|
162 |
+ (String) jwtUtil.getClaim(accessToken, "lgnId"), |
|
163 |
+ (List<MberAuthorVO>) jwtUtil.getClaim(accessToken, "roles") |
|
164 |
+ ); |
|
165 |
+ |
|
166 |
+ // JWT 모드에서 중복로그인 체크 |
|
167 |
+ if (!loginPolicyService.getPolicy() && !isTokenValid(mber.getMbrId(), accessToken)) { |
|
168 |
+ sendTokenExpiredResponse(response, request); |
|
169 |
+ return; |
|
170 |
+ } |
|
171 |
+ |
|
172 |
+ Authentication authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities()); |
|
173 |
+ SecurityContextHolder.getContext().setAuthentication(authToken); |
|
174 |
+ |
|
175 |
+ filterChain.doFilter(request, response); |
|
128 | 176 |
} |
129 | 177 |
|
130 | 178 |
/** |
... | ... | @@ -136,13 +184,17 @@ |
136 | 184 |
requestURI.startsWith("/oauth/"); |
137 | 185 |
} |
138 | 186 |
|
139 |
- |
|
187 |
+ /** |
|
188 |
+ * JWT 토큰 유효성 검증 (기존 로직) |
|
189 |
+ */ |
|
140 | 190 |
private boolean isTokenValid(String mbrId, String accessToken) { |
141 | 191 |
String storedToken = redisTemplate.opsForValue().get("jwt:" + mbrId); |
142 |
- return storedToken == null || storedToken.equals(accessToken); |
|
192 |
+ return storedToken == null || storedToken.equals(jwtUtil.extractToken(accessToken)); |
|
143 | 193 |
} |
144 | 194 |
|
145 |
- |
|
195 |
+ /** |
|
196 |
+ * 토큰 만료 응답 |
|
197 |
+ */ |
|
146 | 198 |
private void sendTokenExpiredResponse(HttpServletResponse response, HttpServletRequest request) throws IOException { |
147 | 199 |
ErrorResponse errorResponse = new ErrorResponse(); |
148 | 200 |
errorResponse.setMessage("Token expired"); |
... | ... | @@ -155,4 +207,20 @@ |
155 | 207 |
response.setStatus(HttpStatus.UNAUTHORIZED.value()); |
156 | 208 |
response.getOutputStream().write(appConfig.getObjectMapper().writeValueAsBytes(errorResponse)); |
157 | 209 |
} |
210 |
+ |
|
211 |
+ /** |
|
212 |
+ * 세션 만료 응답 |
|
213 |
+ */ |
|
214 |
+ private void sendSessionExpiredResponse(HttpServletResponse response, HttpServletRequest request) throws IOException { |
|
215 |
+ ErrorResponse errorResponse = new ErrorResponse(); |
|
216 |
+ errorResponse.setMessage("Session expired or duplicate login detected"); |
|
217 |
+ errorResponse.setPath(request.getRequestURI()); |
|
218 |
+ errorResponse.setError(HttpStatus.UNAUTHORIZED.getReasonPhrase()); |
|
219 |
+ errorResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); |
|
220 |
+ errorResponse.setTimestamp(LocalDateTime.now()); |
|
221 |
+ |
|
222 |
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE); |
|
223 |
+ response.setStatus(HttpStatus.UNAUTHORIZED.value()); |
|
224 |
+ response.getOutputStream().write(appConfig.getObjectMapper().writeValueAsBytes(errorResponse)); |
|
225 |
+ } |
|
158 | 226 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/filter/LoginFilter.java
+++ src/main/java/com/takensoft/common/filter/LoginFilter.java
... | ... | @@ -3,12 +3,14 @@ |
3 | 3 |
import com.fasterxml.jackson.databind.ObjectMapper; |
4 | 4 |
import com.takensoft.cms.loginPolicy.service.Email2ndAuthService; |
5 | 5 |
import com.takensoft.cms.mber.dto.LoginDTO; |
6 |
+import com.takensoft.cms.mber.service.UnifiedLoginService; |
|
6 | 7 |
import com.takensoft.cms.mber.vo.MberVO; |
7 | 8 |
import com.takensoft.common.exception.FilterExceptionHandler; |
8 | 9 |
import com.takensoft.common.util.LoginUtil; |
9 | 10 |
import com.takensoft.common.verify.service.Impl.EmailServiceImpl; |
10 | 11 |
import com.takensoft.common.verify.vo.EmailVO; |
11 | 12 |
import lombok.SneakyThrows; |
13 |
+import lombok.extern.slf4j.Slf4j; |
|
12 | 14 |
import org.springframework.http.HttpStatus; |
13 | 15 |
import org.springframework.security.authentication.AuthenticationManager; |
14 | 16 |
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
... | ... | @@ -31,119 +33,96 @@ |
31 | 33 |
* @modification |
32 | 34 |
* since | author | description |
33 | 35 |
* 2024.04.01 | takensoft | 최초 등록 |
36 |
+ * 2025.05.28 | takensoft | 통합 로그인 적용 |
|
37 |
+ * 2025.05.29 | takensoft | OAuth2 통합 개선 |
|
34 | 38 |
* |
35 |
- * UsernamePasswordAuthenticationFilter - Spring Security에서 사용자 로그인 요청을 처리하는 필터 클래스 |
|
36 |
- * |
|
37 |
- * 사용자 로그인 요청을 처리하는 Filter |
|
39 |
+ * 사용자 로그인 요청을 처리하는 Filter - 통합 로그인 시스템 적용 |
|
38 | 40 |
*/ |
41 |
+@Slf4j |
|
39 | 42 |
public class LoginFilter extends UsernamePasswordAuthenticationFilter { |
40 | 43 |
|
41 | 44 |
private final AuthenticationManager authenticationManager; |
42 | 45 |
private final EmailServiceImpl emailServiceImpl; |
43 | 46 |
private final LoginUtil loginUtil; |
44 | 47 |
private final Email2ndAuthService email2ndAuth; |
45 |
- /** |
|
46 |
- * LoginFilter 생성자 |
|
47 |
- */ |
|
48 |
- public LoginFilter(AuthenticationManager authenticationManager, EmailServiceImpl emailServiceImpl, LoginUtil loginUtil, Email2ndAuthService email2ndAuth) { |
|
48 |
+ private final UnifiedLoginService unifiedLoginService; |
|
49 |
+ |
|
50 |
+ public LoginFilter(AuthenticationManager authenticationManager, EmailServiceImpl emailServiceImpl, |
|
51 |
+ LoginUtil loginUtil, Email2ndAuthService email2ndAuth, UnifiedLoginService unifiedLoginService) { |
|
49 | 52 |
this.authenticationManager = authenticationManager; |
50 | 53 |
this.emailServiceImpl = emailServiceImpl; |
51 | 54 |
this.loginUtil = loginUtil; |
52 | 55 |
this.email2ndAuth = email2ndAuth; |
56 |
+ this.unifiedLoginService = unifiedLoginService; |
|
53 | 57 |
|
54 | 58 |
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/mbr/loginProc.json","POST")); |
55 | 59 |
} |
56 | 60 |
|
57 |
- /** |
|
58 |
- * @param req - HTTP 요청 객체 |
|
59 |
- * @param res - HTTP 응답 객체 |
|
60 |
- * @return 인증 정보 |
|
61 |
- * @throws AuthenticationException - 인증 예외 |
|
62 |
- * |
|
63 |
- * 로그인 요청을 처리 ( 사용자가 입력한 로그인 정보로 인증 시도 ) |
|
64 |
- */ |
|
65 | 61 |
@SneakyThrows |
66 | 62 |
@Override |
67 | 63 |
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { |
68 | 64 |
ObjectMapper mapper = new ObjectMapper(); |
69 | 65 |
LoginDTO login = mapper.readValue(req.getInputStream(), LoginDTO.class); |
70 |
- // 클라이언트에서 요청한 아이디와 비밀번호 추출 |
|
66 |
+ |
|
71 | 67 |
String lgnId = login.getLgnId(); |
72 | 68 |
String pswd = login.getPswd(); |
73 | 69 |
req.setAttribute("lgnReqPage", login.getLgnReqPage()); |
70 |
+ req.setAttribute("loginType", "S"); // 시스템 로그인 표시 |
|
74 | 71 |
|
75 |
- // 스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함 |
|
72 |
+ // 통합 로그인 시스템을 통한 사용자 검증 |
|
73 |
+ try { |
|
74 |
+ MberVO authenticatedUser = unifiedLoginService.authenticateUser("S", lgnId, pswd); |
|
75 |
+ req.setAttribute("authenticatedUser", authenticatedUser); |
|
76 |
+ } catch (Exception e) { |
|
77 |
+ // 기존 Spring Security 방식으로 폴백 |
|
78 |
+ } |
|
79 |
+ |
|
80 |
+ // 스프링 시큐리티 인증 토큰 생성 |
|
76 | 81 |
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(lgnId, pswd, null); |
77 | 82 |
|
78 |
- // token에 담은 검증을 위한 AuthenticationManager로 전달 |
|
79 | 83 |
return authenticationManager.authenticate(authToken); |
80 | 84 |
} |
81 | 85 |
|
82 |
- /** |
|
83 |
- * @param req - HTTP 요청 객체 |
|
84 |
- * @param res - HTTP 응답 객체 |
|
85 |
- * @param chain - 필터 체인 |
|
86 |
- * @param authentication - 인증된 사용자 정보 |
|
87 |
- * @throws IOException - IO 예외 |
|
88 |
- * @throws ServletException - 서블릿 예외 |
|
89 |
- * |
|
90 |
- * 로그인 인증 성공 시 |
|
91 |
- */ |
|
92 | 86 |
@SneakyThrows |
93 | 87 |
@Override |
94 | 88 |
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authentication) throws IOException { |
95 | 89 |
Map<String, Object> result = new HashMap<>(); |
96 | 90 |
|
97 | 91 |
MberVO mber = (MberVO) authentication.getPrincipal(); |
98 |
- boolean isAdmin = mber.getAuthorities().stream().anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN")); // 관리자 권한 여부 |
|
99 |
- String lgnReqPage = (String) req.getAttribute("lgnReqPage"); // 로그인 요청 페이지 정보 (A: 관리자, U: 사용자) |
|
100 |
- boolean use2ndAuth = email2ndAuth.findByEmail2ndAuth(); // 이메일 2차 인증 여부 확인 |
|
92 |
+ boolean isAdmin = mber.getAuthorities().stream().anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN")); |
|
93 |
+ String lgnReqPage = (String) req.getAttribute("lgnReqPage"); |
|
94 |
+ boolean use2ndAuth = email2ndAuth.findByEmail2ndAuth(); |
|
101 | 95 |
|
102 |
- // 관리자일 경우 2차 인증(이메일 인증) 코드 발송 |
|
103 |
- if(isAdmin) { |
|
104 |
- if(use2ndAuth) { |
|
105 |
- EmailVO emailVO = new EmailVO().builder() |
|
106 |
- .email(mber.getEml()) |
|
107 |
- .build(); |
|
108 |
- emailServiceImpl.sendEmailVerifyCode(emailVO); |
|
109 |
- res.setContentType("application/json;charset=UTF-8"); |
|
110 |
- res.setStatus(HttpStatus.OK.value()); |
|
111 |
- result.put("mbrId", mber.getMbrId()); |
|
112 |
- result.put("email", mber.getEml()); |
|
96 |
+ // 관리자 2차 인증 처리 |
|
97 |
+ if(isAdmin && use2ndAuth) { |
|
98 |
+ EmailVO emailVO = new EmailVO().builder() |
|
99 |
+ .email(mber.getEml()) |
|
100 |
+ .build(); |
|
101 |
+ emailServiceImpl.sendEmailVerifyCode(emailVO); |
|
113 | 102 |
|
114 |
- res.setContentType("application/json;charset=UTF-8"); |
|
115 |
- res.setStatus(HttpStatus.OK.value()); |
|
103 |
+ result.put("mbrId", mber.getMbrId()); |
|
104 |
+ result.put("email", mber.getEml()); |
|
116 | 105 |
|
117 |
- new ObjectMapper().writeValue(res.getOutputStream(), result); |
|
118 |
- } else { |
|
119 |
- loginUtil.successLogin(mber, req, res); // 로그인 성공 처리 |
|
120 |
- } |
|
121 |
- // 사용자일 경우 |
|
122 |
- } else { |
|
123 |
- // 사용자가 관리자 로그인 페이지로 접근할 경우 |
|
124 |
- if("A".equals(lgnReqPage)) { |
|
125 |
- res.setContentType("application/json;charset=UTF-8"); |
|
126 |
- res.setStatus(HttpStatus.FORBIDDEN.value()); |
|
127 |
- |
|
128 |
- result.put("message", "접근 권한이 없습니다."); |
|
129 |
- new ObjectMapper().writeValue(res.getOutputStream(), result); |
|
130 |
- } else { |
|
131 |
- loginUtil.successLogin(mber, req, res); // 로그인 성공 처리 |
|
132 |
- } |
|
106 |
+ res.setContentType("application/json;charset=UTF-8"); |
|
107 |
+ res.setStatus(HttpStatus.OK.value()); |
|
108 |
+ new ObjectMapper().writeValue(res.getOutputStream(), result); |
|
109 |
+ return; |
|
133 | 110 |
} |
111 |
+ |
|
112 |
+ // 사용자 권한 체크 |
|
113 |
+ if (!isAdmin && "A".equals(lgnReqPage)) { |
|
114 |
+ res.setContentType("application/json;charset=UTF-8"); |
|
115 |
+ res.setStatus(HttpStatus.FORBIDDEN.value()); |
|
116 |
+ result.put("message", "접근 권한이 없습니다."); |
|
117 |
+ new ObjectMapper().writeValue(res.getOutputStream(), result); |
|
118 |
+ return; |
|
119 |
+ } |
|
120 |
+ |
|
121 |
+ loginUtil.successLogin(mber, req, res); |
|
134 | 122 |
} |
135 | 123 |
|
136 |
- /** |
|
137 |
- * @param req - HTTP 요청 객체 |
|
138 |
- * @param res - HTTP 응답 객체 |
|
139 |
- * @param failed - 인증 예외 |
|
140 |
- * @throws IOException - IO 예외 |
|
141 |
- * @throws ServletException - 서블릿 예외 |
|
142 |
- * |
|
143 |
- * 로그인 인증 실패 시 |
|
144 |
- */ |
|
145 | 124 |
@Override |
146 | 125 |
protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException, ServletException { |
147 | 126 |
FilterExceptionHandler.loginError(res, failed); |
148 | 127 |
} |
149 |
-} |
|
128 |
+}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
... | ... | @@ -1,62 +1,47 @@ |
1 | 1 |
package com.takensoft.common.oauth.handler; |
2 | 2 |
|
3 |
-import com.fasterxml.jackson.core.JsonProcessingException; |
|
4 |
-import com.fasterxml.jackson.databind.ObjectMapper; |
|
5 | 3 |
import com.takensoft.cms.loginPolicy.service.LoginModeService; |
6 |
-import com.takensoft.cms.loginPolicy.service.LoginPolicyService; |
|
7 | 4 |
import com.takensoft.cms.mber.service.LgnHstryService; |
8 |
-import com.takensoft.cms.mber.service.MberService; |
|
5 |
+import com.takensoft.cms.mber.service.UnifiedLoginService; |
|
9 | 6 |
import com.takensoft.cms.mber.vo.LgnHstryVO; |
10 |
-import com.takensoft.cms.mber.vo.MberAuthorVO; |
|
11 | 7 |
import com.takensoft.cms.mber.vo.MberVO; |
12 |
-import com.takensoft.cms.token.service.RefreshTokenService; |
|
13 |
-import com.takensoft.cms.token.vo.RefreshTknVO; |
|
14 | 8 |
import com.takensoft.common.oauth.vo.CustomOAuth2UserVO; |
15 | 9 |
import com.takensoft.common.util.HttpRequestUtil; |
16 |
-import com.takensoft.common.util.JWTUtil; |
|
17 |
-import com.takensoft.common.util.SessionUtil; |
|
10 |
+import com.takensoft.common.util.LoginUtil; |
|
18 | 11 |
import jakarta.servlet.ServletException; |
19 |
-import jakarta.servlet.http.Cookie; |
|
20 | 12 |
import jakarta.servlet.http.HttpServletRequest; |
21 | 13 |
import jakarta.servlet.http.HttpServletResponse; |
22 |
-import jakarta.servlet.http.HttpSession; |
|
23 | 14 |
import lombok.RequiredArgsConstructor; |
24 | 15 |
import lombok.extern.slf4j.Slf4j; |
25 | 16 |
import org.springframework.beans.factory.annotation.Value; |
26 |
-import org.springframework.context.ApplicationContext; |
|
27 |
-import org.springframework.data.redis.core.RedisTemplate; |
|
28 | 17 |
import org.springframework.security.core.Authentication; |
29 | 18 |
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; |
30 | 19 |
import org.springframework.stereotype.Component; |
31 | 20 |
|
32 | 21 |
import java.io.IOException; |
33 | 22 |
import java.net.URLEncoder; |
34 |
-import java.util.*; |
|
35 |
-import java.util.concurrent.TimeUnit; |
|
36 | 23 |
|
24 |
+/** |
|
25 |
+ * @author takensoft |
|
26 |
+ * @since 2025.05.22 |
|
27 |
+ * @modification |
|
28 |
+ * since | author | description |
|
29 |
+ * 2025.05.22 | takensoft | 최초 등록 |
|
30 |
+ * 2025.05.28 | takensoft | 통합 로그인 적용 |
|
31 |
+ * 2025.05.29 | takensoft | OAuth2 통합 문제 해결 |
|
32 |
+ * |
|
33 |
+ * OAuth2 로그인 성공 핸들러 - 통합 로그인 시스템 적용 (문제 해결) |
|
34 |
+ */ |
|
37 | 35 |
@Slf4j |
38 | 36 |
@Component |
39 | 37 |
@RequiredArgsConstructor |
40 | 38 |
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { |
41 | 39 |
|
42 |
- private final ApplicationContext applicationContext; |
|
43 |
- private final JWTUtil jwtUtil; |
|
44 |
- private final RefreshTokenService refreshTokenService; |
|
40 |
+ private final UnifiedLoginService unifiedLoginService; |
|
45 | 41 |
private final LgnHstryService lgnHstryService; |
46 | 42 |
private final HttpRequestUtil httpRequestUtil; |
43 |
+ private final LoginUtil loginUtil; |
|
47 | 44 |
private final LoginModeService loginModeService; |
48 |
- private final LoginPolicyService loginPolicyService; |
|
49 |
- private final SessionUtil sessionUtil; |
|
50 |
- private final RedisTemplate<String, String> redisTemplate; |
|
51 |
- |
|
52 |
- @Value("${jwt.accessTime}") |
|
53 |
- private long jwtAccessTime; |
|
54 |
- |
|
55 |
- @Value("${jwt.refreshTime}") |
|
56 |
- private long jwtRefreshTime; |
|
57 |
- |
|
58 |
- @Value("${cookie.time}") |
|
59 |
- private int cookieTime; |
|
60 | 45 |
|
61 | 46 |
@Value("${front.url}") |
62 | 47 |
private String frontUrl; |
... | ... | @@ -67,125 +52,36 @@ |
67 | 52 |
CustomOAuth2UserVO oAuth2User = (CustomOAuth2UserVO) authentication.getPrincipal(); |
68 | 53 |
|
69 | 54 |
try { |
55 |
+ // 현재 설정된 로그인 모드 확인 |
|
56 |
+ String currentLoginMode = loginModeService.getLoginMode(); |
|
70 | 57 |
|
71 |
- // MberService를 ApplicationContext에서 가져옴 |
|
72 |
- MberService mberService = applicationContext.getBean(MberService.class); |
|
58 |
+ // 통합 로그인 서비스를 통한 OAuth2 사용자 처리 |
|
59 |
+ MberVO mber = unifiedLoginService.processOAuth2User( |
|
60 |
+ oAuth2User.getEmail(), |
|
61 |
+ convertProviderToMbrType(oAuth2User.getProvider()), |
|
62 |
+ oAuth2User.getId(), |
|
63 |
+ oAuth2User.getName(), |
|
64 |
+ request |
|
65 |
+ ); |
|
73 | 66 |
|
74 |
- // OAuth2 사용자 정보로 MberVO 생성 또는 조회 |
|
75 |
- MberVO mber = processOAuth2User(oAuth2User, mberService, request); |
|
67 |
+ saveLoginHistory(request, mber, oAuth2User.getProvider()); |
|
76 | 68 |
|
77 |
- // 로그인 이력 저장 |
|
78 |
- saveLoginHistory(request, mber); |
|
69 |
+ // LoginUtil을 통한 통합 로그인 처리 |
|
70 |
+ loginUtil.successLogin(mber, request, response); |
|
79 | 71 |
|
80 |
- // 로그인 모드 확인 |
|
81 |
- String loginMode = loginModeService.getLoginMode(); |
|
82 |
- |
|
83 |
- // 로그인 모드에 따른 처리 |
|
84 |
- if ("S".equals(loginMode)) { |
|
85 |
- handleSessionMode(request, response, mber); |
|
86 |
- } else { |
|
87 |
- handleJwtMode(request, response, mber); |
|
88 |
- } |
|
89 |
- |
|
90 |
- // 프론트엔드로 리다이렉트 |
|
91 |
- String redirectUrl = frontUrl + "/login.page?oauth_success=true&loginMode=" + loginMode; |
|
92 |
- |
|
72 |
+ String redirectUrl = String.format("%s/login.page?oauth_success=true&loginMode=%s", |
|
73 |
+ frontUrl, currentLoginMode); |
|
93 | 74 |
getRedirectStrategy().sendRedirect(request, response, redirectUrl); |
94 | 75 |
|
95 | 76 |
} catch (Exception e) { |
96 |
- e.printStackTrace(); |
|
97 | 77 |
handleOAuth2Error(response, e); |
98 | 78 |
} |
99 | 79 |
} |
100 | 80 |
|
101 | 81 |
/** |
102 |
- * JWT 모드 처리 - OAuth용 access token 쿠키 추가 |
|
103 |
- */ |
|
104 |
- private void handleJwtMode(HttpServletRequest request, HttpServletResponse response, MberVO mber) throws IOException { |
|
105 |
- try { |
|
106 |
- // JWT 토큰 생성 |
|
107 |
- String accessToken = jwtUtil.createJwt("Authorization", |
|
108 |
- mber.getMbrId(), |
|
109 |
- mber.getLgnId(), |
|
110 |
- mber.getMbrNm(), |
|
111 |
- (List) mber.getAuthorities(), |
|
112 |
- jwtAccessTime); |
|
113 |
- |
|
114 |
- String refreshToken = jwtUtil.createJwt("refresh", |
|
115 |
- mber.getMbrId(), |
|
116 |
- mber.getLgnId(), |
|
117 |
- mber.getMbrNm(), |
|
118 |
- (List) mber.getAuthorities(), |
|
119 |
- jwtRefreshTime); |
|
120 |
- // Refresh 토큰 처리 |
|
121 |
- RefreshTknVO refresh = new RefreshTknVO(); |
|
122 |
- refresh.setMbrId(mber.getMbrId()); |
|
123 |
- |
|
124 |
- if (refreshTokenService.findByCheckRefresh(request, refresh)) { |
|
125 |
- refreshTokenService.delete(request, refresh); |
|
126 |
- } |
|
127 |
- |
|
128 |
- refresh.setToken(refreshToken); |
|
129 |
- |
|
130 |
- // 헤더와 쿠키 설정 |
|
131 |
- response.setHeader("Authorization", accessToken); |
|
132 |
- // OAuth 전용 access token 쿠키 생성 |
|
133 |
- Cookie oauthAccessCookie = new Cookie("oauth_access_token", accessToken); |
|
134 |
- oauthAccessCookie.setPath("/"); |
|
135 |
- oauthAccessCookie.setMaxAge(300); // 5분 후 자동 삭제 |
|
136 |
- oauthAccessCookie.setHttpOnly(false); // 프론트에서 접근 가능하도록 |
|
137 |
- |
|
138 |
- response.addCookie(oauthAccessCookie); |
|
139 |
- |
|
140 |
- // Refresh 쿠키 생성 |
|
141 |
- Cookie refreshCookie = jwtUtil.createCookie("refresh", refreshToken, cookieTime); |
|
142 |
- response.addCookie(refreshCookie); |
|
143 |
- |
|
144 |
- response.setHeader("login-type", "J"); |
|
145 |
- |
|
146 |
- // 중복 로그인 비허용 처리 |
|
147 |
- if (!loginPolicyService.getPolicy()) { |
|
148 |
- redisTemplate.delete("jwt:" + mber.getMbrId()); |
|
149 |
- redisTemplate.opsForValue().set("jwt:" + mber.getMbrId(), accessToken, jwtAccessTime, TimeUnit.MILLISECONDS); |
|
150 |
- } |
|
151 |
- |
|
152 |
- // Refresh 토큰 저장 |
|
153 |
- refreshTokenService.saveRefreshToken(request, response, refresh, jwtRefreshTime); |
|
154 |
- |
|
155 |
- } catch (Exception e) { |
|
156 |
- e.printStackTrace(); |
|
157 |
- throw e; |
|
158 |
- } |
|
159 |
- } |
|
160 |
- |
|
161 |
- /** |
|
162 |
- * 세션 모드 처리 |
|
163 |
- */ |
|
164 |
- private void handleSessionMode(HttpServletRequest request, HttpServletResponse response, MberVO mber) { |
|
165 |
- log.info("세션 모드로 OAuth2 로그인 처리"); |
|
166 |
- |
|
167 |
- // 세션 생성 및 정보 저장 |
|
168 |
- HttpSession session = request.getSession(true); |
|
169 |
- |
|
170 |
- // 세션에 사용자 정보 저장 (JWT 없이!) |
|
171 |
- session.setAttribute("mbrId", mber.getMbrId()); |
|
172 |
- session.setAttribute("mbrNm", mber.getMbrNm()); |
|
173 |
- session.setAttribute("lgnId", mber.getLgnId()); |
|
174 |
- session.setAttribute("roles", mber.getAuthorList()); |
|
175 |
- session.setAttribute("loginType", "OAUTH2"); |
|
176 |
- |
|
177 |
- // 중복 로그인 비허용 처리 |
|
178 |
- if (!loginPolicyService.getPolicy()) { |
|
179 |
- sessionUtil.registerSession(mber.getMbrId(), session); |
|
180 |
- } |
|
181 |
- |
|
182 |
- response.setHeader("login-type", "S"); |
|
183 |
- } |
|
184 |
- |
|
185 |
- /** |
|
186 | 82 |
* 로그인 이력 저장 |
187 | 83 |
*/ |
188 |
- private void saveLoginHistory(HttpServletRequest request, MberVO mber) { |
|
84 |
+ private void saveLoginHistory(HttpServletRequest request, MberVO mber, String provider) { |
|
189 | 85 |
try { |
190 | 86 |
String userAgent = httpRequestUtil.getUserAgent(request); |
191 | 87 |
|
... | ... | @@ -200,35 +96,13 @@ |
200 | 96 |
|
201 | 97 |
lgnHstryService.LgnHstrySave(loginHistory); |
202 | 98 |
} catch (Exception e) { |
203 |
- log.error("로그인 이력 저장 실패", e); |
|
204 |
- // 로그인 이력 저장 실패해도 로그인은 계속 진행 |
|
99 |
+ log.error("로그인 이력 저장 실패: {}", e.getMessage()); |
|
205 | 100 |
} |
206 | 101 |
} |
207 | 102 |
|
208 | 103 |
/** |
209 |
- * OAuth2 사용자 처리 |
|
104 |
+ * 제공자명을 회원타입으로 변환 |
|
210 | 105 |
*/ |
211 |
- private MberVO processOAuth2User(CustomOAuth2UserVO oAuth2User, MberService mberService, HttpServletRequest request) throws JsonProcessingException { |
|
212 |
- String mbrType = convertProviderToMbrType(oAuth2User.getProvider()); |
|
213 |
- MberVO existingUser = mberService.findByEmailAndProvider(oAuth2User.getEmail(), mbrType); |
|
214 |
- if (existingUser != null) { |
|
215 |
- existingUser.setMbrNm(oAuth2User.getName()); |
|
216 |
- return mberService.updateOAuthUser(existingUser); |
|
217 |
- } else { |
|
218 |
- MberVO newUser = new MberVO(); |
|
219 |
- newUser.setEml(oAuth2User.getEmail()); |
|
220 |
- newUser.setLgnId(oAuth2User.getEmail().toLowerCase()); |
|
221 |
- newUser.setMbrNm(oAuth2User.getName()); |
|
222 |
- newUser.setNcnm(oAuth2User.getName()); |
|
223 |
- newUser.setMbrType(mbrType); |
|
224 |
- MberAuthorVO roleUser = new MberAuthorVO(); |
|
225 |
- roleUser.setAuthrtCd("ROLE_USER"); |
|
226 |
- newUser.setAuthorList(Collections.singletonList(roleUser)); |
|
227 |
- |
|
228 |
- return mberService.saveOAuthUser(newUser, request); |
|
229 |
- } |
|
230 |
- } |
|
231 |
- |
|
232 | 106 |
private String convertProviderToMbrType(String provider) { |
233 | 107 |
return switch (provider.toLowerCase()) { |
234 | 108 |
case "kakao" -> "K"; |
... | ... | @@ -239,6 +113,9 @@ |
239 | 113 |
}; |
240 | 114 |
} |
241 | 115 |
|
116 |
+ /** |
|
117 |
+ * OAuth2 오류 처리 |
|
118 |
+ */ |
|
242 | 119 |
private void handleOAuth2Error(HttpServletResponse response, Exception e) throws IOException { |
243 | 120 |
String message = URLEncoder.encode("OAuth 로그인에 실패했습니다.", "UTF-8"); |
244 | 121 |
String errorUrl = String.format("%s/login.page?error=oauth2_failed&message=%s", frontUrl, message); |
--- src/main/java/com/takensoft/common/oauth/vo/CustomOAuth2UserVO.java
+++ src/main/java/com/takensoft/common/oauth/vo/CustomOAuth2UserVO.java
... | ... | @@ -38,7 +38,6 @@ |
38 | 38 |
return oAuth2UserInfoVO.getName(); |
39 | 39 |
} |
40 | 40 |
|
41 |
- // 추가 getter 메서드들 |
|
42 | 41 |
public String getId() { |
43 | 42 |
return oAuth2UserInfoVO.getId(); |
44 | 43 |
} |
--- src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
+++ src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
... | ... | @@ -65,19 +65,16 @@ |
65 | 65 |
String userAgent = httpRequestUtil.getUserAgent(request); |
66 | 66 |
|
67 | 67 |
try { |
68 |
- // 1. Provider 유효성 검증 |
|
68 |
+ // Provider 유효성 검증 |
|
69 | 69 |
validateProvider(provider); |
70 | 70 |
|
71 |
- // 2. 보안 검증 (필요시 추가) |
|
72 |
- validateSecurity(request); |
|
73 |
- |
|
74 |
- // 3. CORS 헤더 설정 (브라우저 보안 문제 해결) |
|
71 |
+ // CORS 헤더 설정 (브라우저 보안 문제 해결) |
|
75 | 72 |
response.setHeader("Access-Control-Allow-Origin", FRONT_URL); |
76 | 73 |
response.setHeader("Access-Control-Allow-Credentials", "true"); |
77 | 74 |
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); |
78 | 75 |
response.setHeader("Access-Control-Allow-Headers", "*"); |
79 | 76 |
|
80 |
- // 4. OAuth2 Authorization Server로 리다이렉트 |
|
77 |
+ // OAuth2 Authorization Server로 리다이렉트 |
|
81 | 78 |
String redirectUrl = "/oauth2/authorization/" + provider; |
82 | 79 |
|
83 | 80 |
response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); |
... | ... | @@ -101,87 +98,137 @@ |
101 | 98 |
// 로그인 모드 확인 |
102 | 99 |
String loginMode = loginModeService.getLoginMode(); |
103 | 100 |
|
104 |
- // 현재 로그인한 사용자 ID 조회 |
|
105 |
- String currentUserId = null; |
|
106 |
- MberVO mberInfo = null; |
|
107 |
- |
|
108 | 101 |
if ("S".equals(loginMode)) { |
109 |
- // 세션 모드 - 세션에서 직접 조회 |
|
110 |
- HttpSession session = request.getSession(false); |
|
111 |
- if (session != null) { |
|
112 |
- currentUserId = (String) session.getAttribute("mbrId"); |
|
113 |
- |
|
114 |
- if (currentUserId != null) { |
|
115 |
- // 세션에 저장된 정보로 응답 생성 |
|
116 |
- HashMap<String, Object> result = new HashMap<>(); |
|
117 |
- result.put("mbrId", session.getAttribute("mbrId")); |
|
118 |
- result.put("mbrNm", session.getAttribute("mbrNm")); |
|
119 |
- result.put("roles", session.getAttribute("roles")); |
|
120 |
- |
|
121 |
- return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); |
|
122 |
- } |
|
123 |
- } else { |
|
124 |
- return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
125 |
- } |
|
102 |
+ // 세션 모드 처리 |
|
103 |
+ return handleSessionModeUserInfo(request); |
|
126 | 104 |
} else { |
127 |
- // JWT 모드 - 토큰에서 조회 |
|
128 |
- String authHeader = request.getHeader("Authorization"); |
|
129 |
- System.out.println("Authorization Header: " + authHeader); |
|
130 |
- |
|
131 |
- String token = null; |
|
132 |
- |
|
133 |
- if (authHeader != null && !authHeader.isEmpty()) { |
|
134 |
- // Authorization 헤더에서 토큰 추출 |
|
135 |
- token = jwtUtil.extractToken(authHeader); |
|
136 |
- } else { |
|
137 |
- // Authorization 헤더가 없으면 OAuth 전용 쿠키에서 확인 |
|
138 |
- token = getTokenFromOAuthCookie(request); |
|
139 |
- } |
|
140 |
- |
|
141 |
- if (token == null || token.isEmpty()) { |
|
142 |
- return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
143 |
- } |
|
144 |
- |
|
145 |
- try { |
|
146 |
- currentUserId = (String) jwtUtil.getClaim(token, "mbrId"); |
|
147 |
- |
|
148 |
- // JWT 모드에서는 DB에서 최신 정보 조회 |
|
149 |
- HashMap<String, Object> params = new HashMap<>(); |
|
150 |
- params.put("mbrId", currentUserId); |
|
151 |
- mberInfo = mberService.findByMbr(params); |
|
152 |
- |
|
153 |
- if (mberInfo == null) { |
|
154 |
- return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
155 |
- } |
|
156 |
- |
|
157 |
- // 응답 데이터 구성 |
|
158 |
- HashMap<String, Object> result = new HashMap<>(); |
|
159 |
- result.put("mbrId", mberInfo.getMbrId()); |
|
160 |
- result.put("mbrNm", mberInfo.getMbrNm()); |
|
161 |
- result.put("roles", mberInfo.getAuthorList()); |
|
162 |
- |
|
163 |
- // 토큰도 함께 전달 |
|
164 |
- if (authHeader != null && !authHeader.isEmpty()) { |
|
165 |
- result.put("token", authHeader); |
|
166 |
- } else { |
|
167 |
- // 쿠키에서 가져온 토큰이면 Bearer 형태로 반환 |
|
168 |
- String oauthToken = getTokenFromOAuthCookie(request); |
|
169 |
- if (oauthToken != null) { |
|
170 |
- result.put("token", "Bearer " + oauthToken); |
|
171 |
- } |
|
172 |
- } |
|
173 |
- return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); |
|
174 |
- |
|
175 |
- } catch (IllegalArgumentException e) { |
|
176 |
- return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
177 |
- } |
|
105 |
+ // JWT 모드 처리 |
|
106 |
+ return handleJWTModeUserInfo(request); |
|
178 | 107 |
} |
179 |
- // 여기까지 왔다면 사용자를 찾을 수 없음 |
|
180 |
- return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
181 | 108 |
|
182 | 109 |
} catch (Exception e) { |
183 | 110 |
return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); |
184 | 111 |
} |
112 |
+ } |
|
113 |
+ |
|
114 |
+ /** |
|
115 |
+ * 세션 모드 사용자 정보 처리 |
|
116 |
+ */ |
|
117 |
+ private ResponseEntity<?> handleSessionModeUserInfo(HttpServletRequest request) { |
|
118 |
+ try { |
|
119 |
+ HttpSession session = request.getSession(false); |
|
120 |
+ if (session == null) { |
|
121 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
122 |
+ } |
|
123 |
+ |
|
124 |
+ String currentUserId = (String) session.getAttribute("mbrId"); |
|
125 |
+ if (currentUserId == null) { |
|
126 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
127 |
+ } |
|
128 |
+ |
|
129 |
+ // 세션에서 정보 조회 및 최신 권한 정보 가져오기 |
|
130 |
+ HashMap<String, Object> params = new HashMap<>(); |
|
131 |
+ params.put("mbrId", currentUserId); |
|
132 |
+ MberVO mberInfo = mberService.findByMbr(params); |
|
133 |
+ |
|
134 |
+ if (mberInfo == null) { |
|
135 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
136 |
+ } |
|
137 |
+ |
|
138 |
+ // 응답 데이터 구성 |
|
139 |
+ HashMap<String, Object> result = new HashMap<>(); |
|
140 |
+ result.put("mbrId", mberInfo.getMbrId()); |
|
141 |
+ result.put("mbrNm", mberInfo.getMbrNm()); |
|
142 |
+ result.put("roles", mberInfo.getAuthorList()); |
|
143 |
+ result.put("loginMode", "S"); |
|
144 |
+ |
|
145 |
+ return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); |
|
146 |
+ |
|
147 |
+ } catch (Exception e) { |
|
148 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
149 |
+ } |
|
150 |
+ } |
|
151 |
+ |
|
152 |
+ /** |
|
153 |
+ * JWT 모드 사용자 정보 처리 |
|
154 |
+ */ |
|
155 |
+ private ResponseEntity<?> handleJWTModeUserInfo(HttpServletRequest request) { |
|
156 |
+ try { |
|
157 |
+ String token = extractToken(request); |
|
158 |
+ if (token == null) { |
|
159 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
160 |
+ } |
|
161 |
+ |
|
162 |
+ // 토큰에서 사용자 ID 추출 |
|
163 |
+ String currentUserId; |
|
164 |
+ try { |
|
165 |
+ currentUserId = (String) jwtUtil.getClaim(token, "mbrId"); |
|
166 |
+ if (currentUserId == null || currentUserId.isEmpty()) { |
|
167 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
168 |
+ } |
|
169 |
+ } catch (Exception e) { |
|
170 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
171 |
+ } |
|
172 |
+ |
|
173 |
+ // DB에서 최신 사용자 정보 조회 |
|
174 |
+ HashMap<String, Object> params = new HashMap<>(); |
|
175 |
+ params.put("mbrId", currentUserId); |
|
176 |
+ MberVO mberInfo = mberService.findByMbr(params); |
|
177 |
+ |
|
178 |
+ if (mberInfo == null) { |
|
179 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
180 |
+ } |
|
181 |
+ |
|
182 |
+ // 응답 데이터 구성 |
|
183 |
+ HashMap<String, Object> result = new HashMap<>(); |
|
184 |
+ result.put("mbrId", mberInfo.getMbrId()); |
|
185 |
+ result.put("mbrNm", mberInfo.getMbrNm()); |
|
186 |
+ result.put("roles", mberInfo.getAuthorList()); |
|
187 |
+ result.put("loginMode", "J"); |
|
188 |
+ |
|
189 |
+ // JWT 토큰도 함께 전달 |
|
190 |
+ String authHeader = request.getHeader("Authorization"); |
|
191 |
+ if (authHeader != null && !authHeader.isEmpty()) { |
|
192 |
+ result.put("token", authHeader); |
|
193 |
+ } else { |
|
194 |
+ // 쿠키에서 가져온 토큰이면 Bearer 형태로 반환 |
|
195 |
+ result.put("token", "Bearer " + token); |
|
196 |
+ } |
|
197 |
+ |
|
198 |
+ return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); |
|
199 |
+ |
|
200 |
+ } catch (Exception e) { |
|
201 |
+ return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); |
|
202 |
+ } |
|
203 |
+ } |
|
204 |
+ |
|
205 |
+ |
|
206 |
+ /** |
|
207 |
+ * 토큰 추출 로직 통합 및 개선 |
|
208 |
+ */ |
|
209 |
+ private String extractToken(HttpServletRequest request) { |
|
210 |
+ // Authorization 헤더에서 토큰 추출 시도 |
|
211 |
+ String authHeader = request.getHeader("Authorization"); |
|
212 |
+ if (authHeader != null && !authHeader.isEmpty()) { |
|
213 |
+ return jwtUtil.extractToken(authHeader); |
|
214 |
+ } |
|
215 |
+ |
|
216 |
+ // OAuth 전용 쿠키에서 토큰 추출 시도 |
|
217 |
+ String oauthToken = getTokenFromOAuthCookie(request); |
|
218 |
+ if (oauthToken != null && !oauthToken.isEmpty()) { |
|
219 |
+ return oauthToken; |
|
220 |
+ } |
|
221 |
+ |
|
222 |
+ // 일반 Authorization 쿠키에서 토큰 추출 시도 |
|
223 |
+ if (request.getCookies() != null) { |
|
224 |
+ for (Cookie cookie : request.getCookies()) { |
|
225 |
+ if ("Authorization".equals(cookie.getName())) { |
|
226 |
+ return jwtUtil.extractToken(cookie.getValue()); |
|
227 |
+ } |
|
228 |
+ } |
|
229 |
+ } |
|
230 |
+ |
|
231 |
+ return null; |
|
185 | 232 |
} |
186 | 233 |
|
187 | 234 |
/** |
... | ... | @@ -190,8 +237,10 @@ |
190 | 237 |
private String getTokenFromOAuthCookie(HttpServletRequest request) { |
191 | 238 |
if (request.getCookies() != null) { |
192 | 239 |
for (Cookie cookie : request.getCookies()) { |
193 |
- if ("oauth_access_token".equals(cookie.getName())) { |
|
194 |
- return cookie.getValue(); |
|
240 |
+ if ("Authorization".equals(cookie.getName()) || |
|
241 |
+ "refresh".equals(cookie.getName())) { |
|
242 |
+ String token = cookie.getValue(); |
|
243 |
+ return token.startsWith("Bearer ") ? token.substring(7) : token; |
|
195 | 244 |
} |
196 | 245 |
} |
197 | 246 |
} |
... | ... | @@ -216,17 +265,6 @@ |
216 | 265 |
|
217 | 266 |
if (!SUPPORTED_PROVIDERS.contains(provider.toLowerCase())) { |
218 | 267 |
throw new IllegalArgumentException("지원하지 않는 OAuth 제공자입니다: " + provider); |
219 |
- } |
|
220 |
- } |
|
221 |
- |
|
222 |
- /** |
|
223 |
- * 보안 검증 (필요시 확장) |
|
224 |
- */ |
|
225 |
- private void validateSecurity(HttpServletRequest request) { |
|
226 |
- String clientIP = httpRequestUtil.getIp(request); |
|
227 |
- // 예시: 로컬 개발 환경이 아닌 경우 추가 검증 |
|
228 |
- if (!"127.0.0.1".equals(clientIP) && !"::1".equals(clientIP)) { |
|
229 |
- // 운영 환경 보안 검증 로직 |
|
230 | 268 |
} |
231 | 269 |
} |
232 | 270 |
|
--- src/main/java/com/takensoft/common/util/LoginUtil.java
+++ src/main/java/com/takensoft/common/util/LoginUtil.java
... | ... | @@ -12,19 +12,33 @@ |
12 | 12 |
import jakarta.servlet.http.HttpServletResponse; |
13 | 13 |
import jakarta.servlet.http.HttpSession; |
14 | 14 |
import lombok.RequiredArgsConstructor; |
15 |
+import lombok.extern.slf4j.Slf4j; |
|
15 | 16 |
import org.springframework.beans.factory.annotation.Value; |
16 | 17 |
import org.springframework.data.redis.core.RedisTemplate; |
17 | 18 |
import org.springframework.http.HttpStatus; |
18 | 19 |
import org.springframework.stereotype.Component; |
19 | 20 |
|
20 | 21 |
import java.io.IOException; |
22 |
+import java.time.Duration; |
|
21 | 23 |
import java.util.HashMap; |
22 | 24 |
import java.util.List; |
23 | 25 |
import java.util.Map; |
24 | 26 |
import java.util.concurrent.TimeUnit; |
25 | 27 |
|
28 |
+/** |
|
29 |
+ * @author takensoft |
|
30 |
+ * @since 2025.03.21 |
|
31 |
+ * @modification |
|
32 |
+ * since | author | description |
|
33 |
+ * 2025.03.21 | takensoft | 최초 등록 |
|
34 |
+ * 2025.05.28 | takensoft | 통합 로그인 적용, 문제 해결 |
|
35 |
+ * 2025.05.29 | takensoft | Redis 통합 중복로그인 관리 |
|
36 |
+ * |
|
37 |
+ * 통합 로그인 유틸리티 - Redis 통합 중복로그인 관리 |
|
38 |
+ */ |
|
26 | 39 |
@Component |
27 | 40 |
@RequiredArgsConstructor |
41 |
+@Slf4j |
|
28 | 42 |
public class LoginUtil { |
29 | 43 |
private final LgnHstryService lgnHstryService; |
30 | 44 |
private final HttpRequestUtil httpRequestUtil; |
... | ... | @@ -36,77 +50,169 @@ |
36 | 50 |
private final RedisTemplate<String, String> redisTemplate; |
37 | 51 |
|
38 | 52 |
@Value("${jwt.accessTime}") |
39 |
- private long JWT_ACCESSTIME; // access 토큰 유지 시간 |
|
53 |
+ private long JWT_ACCESSTIME; |
|
40 | 54 |
@Value("${jwt.refreshTime}") |
41 |
- private long JWT_REFRESHTIME; // refresh 토큰 유지 시간 |
|
55 |
+ private long JWT_REFRESHTIME; |
|
42 | 56 |
@Value("${cookie.time}") |
43 |
- private int COOKIE_TIME; // 쿠키 유지 시간 |
|
57 |
+ private int COOKIE_TIME; |
|
44 | 58 |
|
59 |
+ /** |
|
60 |
+ * 통합 로그인 성공 처리 - Redis 기반 중복로그인 관리 |
|
61 |
+ */ |
|
45 | 62 |
public void successLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) { |
46 | 63 |
try { |
47 | 64 |
// 로그인 이력 등록 |
48 |
- LgnHstryVO lgnHstryVO = new LgnHstryVO(); |
|
49 |
- lgnHstryVO.setLgnId(mber.getLgnId()); |
|
50 |
- lgnHstryVO.setLgnType(mber.getAuthorities().stream().anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1"); |
|
51 |
- lgnHstryVO.setCntnIp(httpRequestUtil.getIp(req)); |
|
52 |
- lgnHstryVO.setCntnOperSys(httpRequestUtil.getOS(httpRequestUtil.getUserAgent(req))); |
|
53 |
- lgnHstryVO.setDeviceNm(httpRequestUtil.getDevice(httpRequestUtil.getUserAgent(req))); |
|
54 |
- lgnHstryVO.setBrwsrNm(httpRequestUtil.getBrowser(httpRequestUtil.getUserAgent(req))); |
|
55 |
- lgnHstryService.LgnHstrySave(lgnHstryVO); |
|
56 |
- |
|
57 |
- // 로그인 방식 확인 JWT or SESSION |
|
58 |
- String loginType = loginModeService.getLoginMode(); |
|
59 |
- |
|
60 |
- // 토큰 생성(access, refresh) |
|
61 |
- String accessToken = jwtUtil.createJwt("Authorization", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME); |
|
62 |
- String refreshToken = jwtUtil.createJwt("refresh", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_REFRESHTIME); |
|
63 |
- |
|
64 |
- // refreshToken이 현재 IP와 계정으로 등록되어 있는지 확인 |
|
65 |
- RefreshTknVO refresh = new RefreshTknVO(); |
|
66 |
- refresh.setMbrId(mber.getMbrId()); |
|
67 |
- |
|
68 |
- // refresh 토큰이 현재 아이피와 아이디로 DB에 등록 되어 있다면 |
|
69 |
- if (refreshTokenService.findByCheckRefresh(req, refresh)) { |
|
70 |
- refreshTokenService.delete(req, refresh); |
|
65 |
+ String loginType = (String) req.getAttribute("loginType"); |
|
66 |
+ if (!"OAUTH2".equals(loginType)) { |
|
67 |
+ saveLoginHistory(mber, req); |
|
71 | 68 |
} |
72 |
- // refreshToken DB 저장 |
|
73 |
- refresh.setToken(refreshToken); |
|
74 | 69 |
|
75 |
- if ("S".equals(loginType)) { |
|
76 |
- HttpSession session = req.getSession(true); |
|
77 |
- session.setAttribute("JWT_TOKEN", accessToken); |
|
70 |
+ // 로그인 방식 확인 |
|
71 |
+ String loginMode = loginModeService.getLoginMode(); |
|
72 |
+ log.info("통합 로그인 모드: {}, 사용자: {}", loginMode, mber.getMbrId()); |
|
78 | 73 |
|
79 |
- // 중복 로그인 비허용일 때 기존 세션 만료 |
|
80 |
- if (!loginPolicyService.getPolicy()) { |
|
81 |
- sessionUtil.registerSession(mber.getMbrId(), session); |
|
82 |
- } |
|
83 |
- Map<String, Object> result = new HashMap<>(); |
|
84 |
- result.put("mbrId", mber.getMbrId()); |
|
85 |
- result.put("mbrNm", mber.getMbrNm()); |
|
86 |
- result.put("roles", mber.getAuthorList()); |
|
87 |
- |
|
88 |
- res.setContentType("application/json;charset=UTF-8"); |
|
89 |
- res.setStatus(HttpStatus.OK.value()); |
|
90 |
- new ObjectMapper().writeValue(res.getOutputStream(), result); |
|
74 |
+ if ("S".equals(loginMode)) { |
|
75 |
+ // Redis 기반 중복로그인 관리 적용 |
|
76 |
+ handleSessionLogin(mber, req, res); |
|
91 | 77 |
} else { |
92 |
- res.setHeader("Authorization", accessToken); |
|
93 |
- res.addCookie(jwtUtil.createCookie("refresh", refreshToken, COOKIE_TIME)); |
|
94 |
- |
|
95 |
- // 중복 로그인 비허용일 때 Redis 저장 |
|
96 |
- if (!loginPolicyService.getPolicy()) { |
|
97 |
- redisTemplate.delete("jwt:" + mber.getMbrId()); |
|
98 |
- redisTemplate.opsForValue().set("jwt:" + mber.getMbrId(), accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS); |
|
99 |
- } |
|
78 |
+ // 기존 Redis 기반 관리 유지 |
|
79 |
+ handleJwtLogin(mber, req, res); |
|
100 | 80 |
} |
101 | 81 |
|
102 |
- refreshTokenService.saveRefreshToken(req, res, refresh, JWT_REFRESHTIME); |
|
103 |
- res.setHeader("login-type", loginType); |
|
82 |
+ res.setHeader("login-type", loginMode); |
|
83 |
+ log.info("통합 로그인 성공 처리 완료: {}, 모드: {}", mber.getMbrId(), loginMode); |
|
104 | 84 |
} |
105 | 85 |
catch (IOException ioe) { |
86 |
+ log.error("로그인 응답 처리 중 IO 오류", ioe); |
|
106 | 87 |
throw new RuntimeException(ioe); |
107 | 88 |
} |
108 | 89 |
catch (Exception e) { |
90 |
+ log.error("로그인 처리 중 오류 발생", e); |
|
109 | 91 |
throw e; |
110 | 92 |
} |
111 | 93 |
} |
112 |
-} |
|
94 |
+ |
|
95 |
+ /** |
|
96 |
+ * 세션 모드 로그인 처리 - Redis 기반 중복로그인 관리 |
|
97 |
+ */ |
|
98 |
+ private void handleSessionLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException { |
|
99 |
+ log.info("세션 모드 로그인 처리 (Redis 통합): {}", mber.getMbrId()); |
|
100 |
+ |
|
101 |
+ // JWT 토큰은 생성하되 세션에만 저장 |
|
102 |
+ String accessToken = jwtUtil.createJwt("Authorization", |
|
103 |
+ mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), |
|
104 |
+ (List) mber.getAuthorities(), JWT_ACCESSTIME); |
|
105 |
+ |
|
106 |
+ // 세션 생성 및 정보 저장 |
|
107 |
+ HttpSession session = req.getSession(true); |
|
108 |
+ session.setAttribute("JWT_TOKEN", accessToken); |
|
109 |
+ session.setAttribute("mbrId", mber.getMbrId()); |
|
110 |
+ session.setAttribute("mbrNm", mber.getMbrNm()); |
|
111 |
+ session.setAttribute("lgnId", mber.getLgnId()); |
|
112 |
+ session.setAttribute("roles", mber.getAuthorList()); |
|
113 |
+ session.setAttribute("loginType", req.getAttribute("loginType") != null ? |
|
114 |
+ req.getAttribute("loginType") : "S"); |
|
115 |
+ |
|
116 |
+ //중복 로그인 비허용을 Redis로 통합 관리 |
|
117 |
+ if (!loginPolicyService.getPolicy()) { |
|
118 |
+ handleDuplicateSessionLogin(mber.getMbrId(), session); |
|
119 |
+ } |
|
120 |
+ |
|
121 |
+ // 응답 데이터 구성 (OAuth2는 JSON 응답 없이 리다이렉트만) |
|
122 |
+ String loginType = (String) req.getAttribute("loginType"); |
|
123 |
+ if (!"OAUTH2".equals(loginType)) { |
|
124 |
+ Map<String, Object> result = new HashMap<>(); |
|
125 |
+ result.put("mbrId", mber.getMbrId()); |
|
126 |
+ result.put("mbrNm", mber.getMbrNm()); |
|
127 |
+ result.put("roles", mber.getAuthorList()); |
|
128 |
+ |
|
129 |
+ res.setContentType("application/json;charset=UTF-8"); |
|
130 |
+ res.setStatus(HttpStatus.OK.value()); |
|
131 |
+ new ObjectMapper().writeValue(res.getOutputStream(), result); |
|
132 |
+ } |
|
133 |
+ } |
|
134 |
+ |
|
135 |
+ /** |
|
136 |
+ * Redis 기반 세션 중복로그인 관리 |
|
137 |
+ */ |
|
138 |
+ private void handleDuplicateSessionLogin(String mbrId, HttpSession newSession) { |
|
139 |
+ try { |
|
140 |
+ String sessionKey = "session:" + mbrId; |
|
141 |
+ |
|
142 |
+ // 기존 세션 확인 및 무효화 |
|
143 |
+ String oldSessionId = redisTemplate.opsForValue().get(sessionKey); |
|
144 |
+ if (oldSessionId != null && !oldSessionId.equals(newSession.getId())) { |
|
145 |
+ // 기존 세션 무효화 |
|
146 |
+ sessionUtil.invalidateSessionById(oldSessionId); |
|
147 |
+ } |
|
148 |
+ |
|
149 |
+ // 새 세션 정보를 Redis에 저장 |
|
150 |
+ redisTemplate.opsForValue().set(sessionKey, newSession.getId(), |
|
151 |
+ Duration.ofSeconds(newSession.getMaxInactiveInterval())); |
|
152 |
+ |
|
153 |
+ // 기존 SessionUtil에도 등록 (호환성 유지) |
|
154 |
+ sessionUtil.registerSession(mbrId, newSession); |
|
155 |
+ } catch (Exception e) { |
|
156 |
+ // 실패해도 로그인은 계속 진행 |
|
157 |
+ } |
|
158 |
+ } |
|
159 |
+ |
|
160 |
+ /** |
|
161 |
+ * JWT 모드 로그인 처리 - 기존 방식 유지 |
|
162 |
+ */ |
|
163 |
+ private void handleJwtLogin(MberVO mber, HttpServletRequest req, HttpServletResponse res) throws IOException { |
|
164 |
+ // JWT 토큰 생성 |
|
165 |
+ String accessToken = jwtUtil.createJwt("Authorization", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_ACCESSTIME); |
|
166 |
+ String refreshToken = jwtUtil.createJwt("refresh", mber.getMbrId(), mber.getLgnId(), mber.getMbrNm(), (List) mber.getAuthorities(), JWT_REFRESHTIME); |
|
167 |
+ |
|
168 |
+ // Refresh 토큰 처리 |
|
169 |
+ RefreshTknVO refresh = new RefreshTknVO(); |
|
170 |
+ refresh.setMbrId(mber.getMbrId()); |
|
171 |
+ |
|
172 |
+ // 기존 refresh 토큰 삭제 |
|
173 |
+ if (refreshTokenService.findByCheckRefresh(req, refresh)) { |
|
174 |
+ refreshTokenService.delete(req, refresh); |
|
175 |
+ } |
|
176 |
+ refresh.setToken(refreshToken); |
|
177 |
+ |
|
178 |
+ // 응답 헤더 및 쿠키 설정 |
|
179 |
+ res.setHeader("Authorization", accessToken); |
|
180 |
+ res.addCookie(jwtUtil.createCookie("refresh", refreshToken, COOKIE_TIME)); |
|
181 |
+ |
|
182 |
+ // 중복 로그인 비허용일 때 Redis 저장 |
|
183 |
+ if (!loginPolicyService.getPolicy()) { |
|
184 |
+ redisTemplate.delete("jwt:" + mber.getMbrId()); |
|
185 |
+ redisTemplate.opsForValue().set("jwt:" + mber.getMbrId(), accessToken, JWT_ACCESSTIME, TimeUnit.MILLISECONDS); |
|
186 |
+ } |
|
187 |
+ |
|
188 |
+ // Refresh 토큰 저장 |
|
189 |
+ refreshTokenService.saveRefreshToken(req, res, refresh, JWT_REFRESHTIME); |
|
190 |
+ |
|
191 |
+ // OAuth2가 아닌 경우만 상태 코드 설정 |
|
192 |
+ String loginType = (String) req.getAttribute("loginType"); |
|
193 |
+ if (!"OAUTH2".equals(loginType)) { |
|
194 |
+ res.setStatus(HttpStatus.OK.value()); |
|
195 |
+ } |
|
196 |
+ } |
|
197 |
+ |
|
198 |
+ /** |
|
199 |
+ * 로그인 이력 저장 |
|
200 |
+ */ |
|
201 |
+ private void saveLoginHistory(MberVO mber, HttpServletRequest req) { |
|
202 |
+ try { |
|
203 |
+ String userAgent = httpRequestUtil.getUserAgent(req); |
|
204 |
+ |
|
205 |
+ LgnHstryVO lgnHstryVO = new LgnHstryVO(); |
|
206 |
+ lgnHstryVO.setLgnId(mber.getLgnId()); |
|
207 |
+ lgnHstryVO.setLgnType(mber.getAuthorities().stream() |
|
208 |
+ .anyMatch(role -> role.getAuthority().equals("ROLE_ADMIN")) ? "0" : "1"); |
|
209 |
+ lgnHstryVO.setCntnIp(httpRequestUtil.getIp(req)); |
|
210 |
+ lgnHstryVO.setCntnOperSys(httpRequestUtil.getOS(userAgent)); |
|
211 |
+ lgnHstryVO.setDeviceNm(httpRequestUtil.getDevice(userAgent)); |
|
212 |
+ lgnHstryVO.setBrwsrNm(httpRequestUtil.getBrowser(userAgent)); |
|
213 |
+ |
|
214 |
+ lgnHstryService.LgnHstrySave(lgnHstryVO); |
|
215 |
+ } catch (Exception e) { |
|
216 |
+ } |
|
217 |
+ } |
|
218 |
+}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/util/SessionUtil.java
+++ src/main/java/com/takensoft/common/util/SessionUtil.java
... | ... | @@ -1,63 +1,179 @@ |
1 | 1 |
package com.takensoft.common.util; |
2 | 2 |
|
3 | 3 |
import jakarta.servlet.http.HttpSession; |
4 |
-import org.springframework.dao.DataAccessException; |
|
4 |
+import org.springframework.data.redis.core.RedisTemplate; |
|
5 | 5 |
import org.springframework.stereotype.Component; |
6 | 6 |
|
7 |
+import java.time.Duration; |
|
7 | 8 |
import java.util.Map; |
8 | 9 |
import java.util.concurrent.ConcurrentHashMap; |
9 | 10 |
|
10 | 11 |
/** |
11 |
- * @author : takensoft |
|
12 |
- * @since : 2025.03.21 |
|
12 |
+ * @author takensoft |
|
13 |
+ * @since 2025.03.21 |
|
13 | 14 |
* @modification |
14 | 15 |
* since | author | description |
15 | 16 |
* 2025.03.21 | takensoft | 최초 등록 |
17 |
+ * 2025.05.29 | takensoft | Redis 통합 중복로그인 관리 |
|
16 | 18 |
* |
17 |
- * 세션 로그인 방식의 유틸리티 |
|
19 |
+ * 세션 로그인 방식의 유틸리티 - Redis 통합 |
|
18 | 20 |
*/ |
19 | 21 |
@Component |
20 | 22 |
public class SessionUtil { |
21 | 23 |
|
22 |
- private final Map<String, HttpSession> sessionMap = new ConcurrentHashMap<>(); |
|
24 |
+ private final Map<String, HttpSession> sessionMap = new ConcurrentHashMap<>(); |
|
25 |
+ private final RedisTemplate<String, String> redisTemplate; |
|
26 |
+ |
|
27 |
+ public SessionUtil(RedisTemplate<String, String> redisTemplate) { |
|
28 |
+ this.redisTemplate = redisTemplate; |
|
29 |
+ } |
|
23 | 30 |
|
24 | 31 |
/** |
25 |
- * @param mbrId - 사용자 Id |
|
26 |
- * @param newSession - HTTP 세션 |
|
27 |
- * |
|
28 |
- * 기존 세션 있으면 강제 로그아웃 |
|
32 |
+ * 세션 등록 - Redis 연동 |
|
33 |
+ * 기존 세션 있으면 강제 로그아웃 후 새 세션 등록 |
|
29 | 34 |
*/ |
30 |
- public synchronized void registerSession(String mbrId, HttpSession newSession) { |
|
35 |
+ public synchronized void registerSession(String mbrId, HttpSession newSession) { |
|
36 |
+ try { |
|
37 |
+ // 1. 기존 메모리 세션 처리 |
|
31 | 38 |
HttpSession oldSession = sessionMap.get(mbrId); |
32 | 39 |
if (oldSession != null && oldSession != newSession) { |
33 |
- oldSession.invalidate(); |
|
34 |
- } |
|
35 |
- sessionMap.put(mbrId, newSession); |
|
36 |
- } |
|
37 |
- /** |
|
38 |
- * @param mbrId - 사용자 Id |
|
39 |
- * |
|
40 |
- * 로그아웃 |
|
41 |
- */ |
|
42 |
- public void removeSession(String mbrId) { |
|
43 |
- HttpSession session = sessionMap.get(mbrId); |
|
44 |
- if (session != null) { |
|
45 |
- session.invalidate(); // 세션 무효화 |
|
46 |
- } |
|
47 |
- sessionMap.remove(mbrId); // 이후 맵에서 제거 |
|
48 |
- } |
|
49 |
- |
|
50 |
- /** |
|
51 |
- * |
|
52 |
- * 전체 로그아웃 |
|
53 |
- */ |
|
54 |
- public void invalidateAllSessions() { |
|
55 |
- for (HttpSession session : sessionMap.values()) { |
|
56 |
- if (session != null) { |
|
57 |
- session.invalidate(); |
|
40 |
+ try { |
|
41 |
+ oldSession.invalidate(); |
|
42 |
+ } catch (IllegalStateException e) { |
|
58 | 43 |
} |
59 | 44 |
} |
60 |
- sessionMap.clear(); // 전체 초기화 |
|
45 |
+ |
|
46 |
+ // 2. 새 세션을 메모리에 등록 |
|
47 |
+ sessionMap.put(mbrId, newSession); |
|
48 |
+ |
|
49 |
+ // 3. Redis에 세션 정보 저장 (중복로그인 관리용) |
|
50 |
+ String sessionKey = "session:" + mbrId; |
|
51 |
+ redisTemplate.opsForValue().set(sessionKey, newSession.getId(), |
|
52 |
+ Duration.ofSeconds(newSession.getMaxInactiveInterval())); |
|
53 |
+ } catch (Exception e) { |
|
54 |
+ e.printStackTrace(); |
|
55 |
+ } |
|
56 |
+ } |
|
57 |
+ |
|
58 |
+ /** |
|
59 |
+ * 세션 ID로 세션 무효화 |
|
60 |
+ */ |
|
61 |
+ public void invalidateSessionById(String sessionId) { |
|
62 |
+ try { |
|
63 |
+ boolean found = false; |
|
64 |
+ // 메모리에서 해당 세션 ID를 가진 세션 찾아서 무효화 |
|
65 |
+ sessionMap.entrySet().removeIf(entry -> { |
|
66 |
+ HttpSession session = entry.getValue(); |
|
67 |
+ if (session != null && session.getId().equals(sessionId)) { |
|
68 |
+ try { |
|
69 |
+ session.invalidate(); |
|
70 |
+ return true; |
|
71 |
+ } catch (IllegalStateException e) { |
|
72 |
+ return true; |
|
73 |
+ } |
|
74 |
+ } |
|
75 |
+ return false; |
|
76 |
+ }); |
|
77 |
+ |
|
78 |
+ } catch (Exception e) { |
|
79 |
+ e.printStackTrace(); |
|
80 |
+ } |
|
81 |
+ } |
|
82 |
+ |
|
83 |
+ /** |
|
84 |
+ * 사용자별 세션 제거 - Redis 연동 |
|
85 |
+ */ |
|
86 |
+ public void removeSession(String mbrId) { |
|
87 |
+ try { |
|
88 |
+ // 1. 메모리 세션 무효화 |
|
89 |
+ HttpSession session = sessionMap.get(mbrId); |
|
90 |
+ if (session != null) { |
|
91 |
+ try { |
|
92 |
+ session.invalidate(); |
|
93 |
+ } catch (IllegalStateException e) { |
|
94 |
+ e.printStackTrace(); |
|
95 |
+ } |
|
96 |
+ } |
|
97 |
+ sessionMap.remove(mbrId); |
|
98 |
+ |
|
99 |
+ // 2. Redis에서도 제거 |
|
100 |
+ String sessionKey = "session:" + mbrId; |
|
101 |
+ redisTemplate.delete(sessionKey); |
|
102 |
+ |
|
103 |
+ } catch (Exception e) { |
|
104 |
+ e.printStackTrace(); |
|
105 |
+ } |
|
106 |
+ } |
|
107 |
+ |
|
108 |
+ /** |
|
109 |
+ * 전체 세션 무효화 - Redis 연동 |
|
110 |
+ */ |
|
111 |
+ public void invalidateAllSessions() { |
|
112 |
+ try { |
|
113 |
+ // 1. 모든 메모리 세션 무효화 |
|
114 |
+ for (Map.Entry<String, HttpSession> entry : sessionMap.entrySet()) { |
|
115 |
+ HttpSession session = entry.getValue(); |
|
116 |
+ if (session != null) { |
|
117 |
+ try { |
|
118 |
+ session.invalidate(); |
|
119 |
+ } catch (IllegalStateException e) { |
|
120 |
+ e.printStackTrace(); |
|
121 |
+ } |
|
122 |
+ } |
|
123 |
+ } |
|
124 |
+ sessionMap.clear(); |
|
125 |
+ |
|
126 |
+ // 2. Redis에서 모든 세션 키 삭제 |
|
127 |
+ try { |
|
128 |
+ var sessionKeys = redisTemplate.keys("session:*"); |
|
129 |
+ if (sessionKeys != null && !sessionKeys.isEmpty()) { |
|
130 |
+ redisTemplate.delete(sessionKeys); |
|
131 |
+ } |
|
132 |
+ } catch (Exception e) { |
|
133 |
+ e.printStackTrace(); |
|
134 |
+ } |
|
135 |
+ } catch (Exception e) { |
|
136 |
+ e.printStackTrace(); |
|
137 |
+ } |
|
138 |
+ } |
|
139 |
+ |
|
140 |
+ /** |
|
141 |
+ * 현재 활성 세션 수 조회 |
|
142 |
+ */ |
|
143 |
+ public int getActiveSessionCount() { |
|
144 |
+ return sessionMap.size(); |
|
145 |
+ } |
|
146 |
+ |
|
147 |
+ /** |
|
148 |
+ * 특정 사용자의 세션 존재 여부 확인 |
|
149 |
+ */ |
|
150 |
+ public boolean hasActiveSession(String mbrId) { |
|
151 |
+ HttpSession session = sessionMap.get(mbrId); |
|
152 |
+ if (session == null) { |
|
153 |
+ return false; |
|
61 | 154 |
} |
62 | 155 |
|
63 |
-} |
|
156 |
+ try { |
|
157 |
+ // 세션이 유효한지 확인 |
|
158 |
+ session.getAttribute("mbrId"); |
|
159 |
+ return true; |
|
160 |
+ } catch (IllegalStateException e) { |
|
161 |
+ // 세션이 무효화됨 |
|
162 |
+ sessionMap.remove(mbrId); |
|
163 |
+ return false; |
|
164 |
+ } |
|
165 |
+ } |
|
166 |
+ |
|
167 |
+ /** |
|
168 |
+ * Redis에서 세션 정보 확인 |
|
169 |
+ */ |
|
170 |
+ public boolean isValidSessionInRedis(String mbrId, String sessionId) { |
|
171 |
+ try { |
|
172 |
+ String sessionKey = "session:" + mbrId; |
|
173 |
+ String storedSessionId = redisTemplate.opsForValue().get(sessionKey); |
|
174 |
+ return storedSessionId != null && storedSessionId.equals(sessionId); |
|
175 |
+ } catch (Exception e) { |
|
176 |
+ return false; |
|
177 |
+ } |
|
178 |
+ } |
|
179 |
+}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/resources/mybatis/mapper/mber/mber-SQL.xml
+++ src/main/resources/mybatis/mapper/mber/mber-SQL.xml
... | ... | @@ -46,6 +46,25 @@ |
46 | 46 |
<result property="regDt" column="reg_dt" /> |
47 | 47 |
</resultMap> |
48 | 48 |
|
49 |
+ <!-- 소셜 계정 정보 resultMap --> |
|
50 |
+ <resultMap id="socialAccountMap" type="MberSocialAccountVO"> |
|
51 |
+ <result property="id" column="id" /> |
|
52 |
+ <result property="mbrId" column="mbr_id" /> |
|
53 |
+ <result property="providerType" column="provider_type" /> |
|
54 |
+ <result property="socialId" column="social_id" /> |
|
55 |
+ <result property="loginId" column="login_id" /> |
|
56 |
+ <result property="socialEmail" column="social_email" /> |
|
57 |
+ <result property="socialName" column="social_name" /> |
|
58 |
+ <result property="isPrimaryProfile" column="is_primary_profile" /> |
|
59 |
+ <result property="isActive" column="is_active" /> |
|
60 |
+ <result property="linkedDt" column="linked_dt" /> |
|
61 |
+ <result property="unlinkedDt" column="unlinked_dt" /> |
|
62 |
+ <result property="rgtr" column="rgtr" /> |
|
63 |
+ <result property="regDt" column="reg_dt" /> |
|
64 |
+ <result property="mdfr" column="mdfr" /> |
|
65 |
+ <result property="mdfcnDt" column="mdfcn_dt" /> |
|
66 |
+ </resultMap> |
|
67 |
+ |
|
49 | 68 |
<sql id="selectMber"> |
50 | 69 |
SELECT mi.mbr_id |
51 | 70 |
, mi.lgn_id |
... | ... | @@ -78,25 +97,59 @@ |
78 | 97 |
|
79 | 98 |
<!-- |
80 | 99 |
작성자 : takensoft |
81 |
- 작성일 : 2024.04.03 |
|
82 |
- 내 용 : 회원정보 조회 [security 용] |
|
100 |
+ 내 용 : 회원정보 조회 [security 용] - 통합 로그인 대응 |
|
83 | 101 |
--> |
84 | 102 |
<select id="findByMberSecurity" parameterType="String" resultMap="mberMap"> |
85 | 103 |
<include refid="selectMber" /> |
86 |
- WHERE mi.lgn_id = #{lgnId} |
|
104 |
+ WHERE EXISTS ( |
|
105 |
+ SELECT 1 FROM mbr_social_accounts msa |
|
106 |
+ WHERE msa.mbr_id = mi.mbr_id |
|
107 |
+ AND ( |
|
108 |
+ (msa.provider_type = 'S' AND msa.login_id = #{lgnId}) |
|
109 |
+ OR (msa.provider_type != 'S' AND msa.social_id = #{lgnId}) |
|
110 |
+ ) |
|
111 |
+ AND msa.is_active = true |
|
112 |
+ ) |
|
87 | 113 |
AND mi.use_yn = 'Y' |
114 |
+ LIMIT 1 |
|
115 |
+ </select> |
|
116 |
+ |
|
117 |
+ <!-- |
|
118 |
+ 통합 로그인: 제공자별 사용자 조회 |
|
119 |
+ --> |
|
120 |
+ <select id="findByUnifiedLogin" parameterType="map" resultMap="mberMap"> |
|
121 |
+ <include refid="selectMber" /> |
|
122 |
+ WHERE EXISTS ( |
|
123 |
+ SELECT 1 FROM mbr_social_accounts msa |
|
124 |
+ WHERE msa.mbr_id = mi.mbr_id |
|
125 |
+ <if test="providerType == 'S'"> |
|
126 |
+ AND msa.provider_type = 'S' |
|
127 |
+ AND msa.login_id = #{identifier} |
|
128 |
+ </if> |
|
129 |
+ <if test="providerType != 'S'"> |
|
130 |
+ AND msa.provider_type = #{providerType} |
|
131 |
+ AND msa.social_id = #{identifier} |
|
132 |
+ </if> |
|
133 |
+ AND msa.is_active = true |
|
134 |
+ ) |
|
135 |
+ AND mi.use_yn = 'Y' |
|
136 |
+ AND mi.mbr_stts = '1' |
|
137 |
+ LIMIT 1 |
|
88 | 138 |
</select> |
89 | 139 |
|
90 | 140 |
<!-- |
91 | 141 |
작성자 : takensoft |
92 | 142 |
작성일 : 2024.04.03 |
93 |
- 내 용 : 로그인 아이디 중복 확인 |
|
143 |
+ 내 용 : 로그인 아이디 중복 확인 - 통합 로그인 대응 |
|
94 | 144 |
--> |
95 | 145 |
<select id="findByCheckLoginId" parameterType="String" resultType="boolean"> |
96 |
- SELECT COUNT(lgn_id) |
|
97 |
- FROM mbr_info |
|
98 |
- WHERE lgn_id = #{lgnId} |
|
99 |
- AND use_yn = 'Y' |
|
146 |
+ SELECT COUNT(*) > 0 |
|
147 |
+ FROM mbr_social_accounts msa |
|
148 |
+ JOIN mbr_info mi ON msa.mbr_id = mi.mbr_id |
|
149 |
+ WHERE msa.provider_type = 'S' |
|
150 |
+ AND msa.login_id = #{lgnId} |
|
151 |
+ AND msa.is_active = true |
|
152 |
+ AND mi.use_yn = 'Y' |
|
100 | 153 |
</select> |
101 | 154 |
|
102 | 155 |
<!-- |
... | ... | @@ -215,7 +268,6 @@ |
215 | 268 |
WHERE mai.mbr_id = #{mbrId} |
216 | 269 |
</select> |
217 | 270 |
|
218 |
- |
|
219 | 271 |
<!-- 이메일로만 사용자 조회 --> |
220 | 272 |
<select id="findByEmail" parameterType="String" resultType="MberVO"> |
221 | 273 |
SELECT |
... | ... | @@ -270,7 +322,7 @@ |
270 | 322 |
WHERE mai.mbr_id = #{mbrId} |
271 | 323 |
</select> |
272 | 324 |
|
273 |
- <!-- OAuth2 사용자 저장 --> |
|
325 |
+ <!-- OAuth2 사용자 저장 --> |
|
274 | 326 |
<insert id="saveOAuthUser" parameterType="MberVO"> |
275 | 327 |
INSERT INTO mbr_info ( |
276 | 328 |
mbr_id, |
... | ... | @@ -305,8 +357,8 @@ |
305 | 357 |
) |
306 | 358 |
</insert> |
307 | 359 |
|
308 |
- <!-- OAuth2 사용자 정보 업데이트 --> |
|
309 |
- <update id="updateOAuthUser" parameterType="MberVO"> |
|
360 |
+ <!-- OAuth2 사용자 정보 업데이트 --> |
|
361 |
+ <update id="updateOAuthUser" parameterType="MberVO"> |
|
310 | 362 |
UPDATE mbr_info |
311 | 363 |
SET |
312 | 364 |
mbr_nm = #{mbrNm}, |
... | ... | @@ -316,8 +368,8 @@ |
316 | 368 |
WHERE mbr_id = #{mbrId} |
317 | 369 |
</update> |
318 | 370 |
|
319 |
- <!-- 기존 계정에 OAuth2 정보 연동 --> |
|
320 |
- <update id="linkOAuth2Account" parameterType="MberVO"> |
|
371 |
+ <!-- 기존 계정에 OAuth2 정보 연동 --> |
|
372 |
+ <update id="linkOAuth2Account" parameterType="MberVO"> |
|
321 | 373 |
UPDATE mbr_info |
322 | 374 |
SET |
323 | 375 |
mbr_type = #{mbrType}, |
... | ... | @@ -325,4 +377,139 @@ |
325 | 377 |
mdfcn_dt = NOW() |
326 | 378 |
WHERE mbr_id = #{mbrId} |
327 | 379 |
</update> |
380 |
+ |
|
381 |
+ <!-- 소셜 계정 정보 저장 --> |
|
382 |
+ <insert id="saveSocialAccount" parameterType="MberSocialAccountVO"> |
|
383 |
+ INSERT INTO mbr_social_accounts ( |
|
384 |
+ mbr_id, |
|
385 |
+ provider_type, |
|
386 |
+ social_id, |
|
387 |
+ login_id, |
|
388 |
+ social_email, |
|
389 |
+ social_name, |
|
390 |
+ is_primary_profile, |
|
391 |
+ is_active, |
|
392 |
+ rgtr |
|
393 |
+ ) VALUES ( |
|
394 |
+ #{mbrId}, |
|
395 |
+ #{providerType}, |
|
396 |
+ #{socialId}, |
|
397 |
+ #{loginId}, |
|
398 |
+ #{socialEmail}, |
|
399 |
+ #{socialName}, |
|
400 |
+ #{isPrimaryProfile}, |
|
401 |
+ #{isActive}, |
|
402 |
+ #{rgtr} |
|
403 |
+ ) |
|
404 |
+ </insert> |
|
405 |
+ |
|
406 |
+ <!-- 이메일로 모든 연동 계정 조회 --> |
|
407 |
+ <select id="findAllAccountsByEmail" parameterType="String" resultMap="mberMap"> |
|
408 |
+ <include refid="selectMber" /> |
|
409 |
+ WHERE mi.eml = #{email} |
|
410 |
+ AND mi.use_yn = 'Y' |
|
411 |
+ AND mi.mbr_stts = '1' |
|
412 |
+ LIMIT 1 |
|
413 |
+ </select> |
|
414 |
+ |
|
415 |
+ <!-- 회원 ID로 소셜 계정 목록 조회 --> |
|
416 |
+ <select id="findSocialAccountsByMbrId" parameterType="String" resultMap="socialAccountMap"> |
|
417 |
+ SELECT id, mbr_id, provider_type, social_id, login_id, social_email, social_name, |
|
418 |
+ is_primary_profile, is_active, linked_dt, unlinked_dt, rgtr, reg_dt, mdfr, mdfcn_dt |
|
419 |
+ FROM mbr_social_accounts |
|
420 |
+ WHERE mbr_id = #{mbrId} |
|
421 |
+ AND is_active = true |
|
422 |
+ ORDER BY is_primary_profile DESC, linked_dt ASC |
|
423 |
+ </select> |
|
424 |
+ |
|
425 |
+ <!-- 특정 제공자로 소셜 계정 조회 --> |
|
426 |
+ <select id="findSocialAccountByProvider" parameterType="map" resultMap="socialAccountMap"> |
|
427 |
+ SELECT id, mbr_id, provider_type, social_id, login_id, social_email, social_name, |
|
428 |
+ is_primary_profile, is_active, linked_dt, unlinked_dt, rgtr, reg_dt, mdfr, mdfcn_dt |
|
429 |
+ FROM mbr_social_accounts |
|
430 |
+ WHERE mbr_id = #{mbrId} |
|
431 |
+ AND provider_type = #{providerType} |
|
432 |
+ AND is_active = true |
|
433 |
+ </select> |
|
434 |
+ |
|
435 |
+ <!-- 소셜 계정 연동 --> |
|
436 |
+ <insert id="linkSocialAccount" parameterType="MberSocialAccountVO"> |
|
437 |
+ INSERT INTO mbr_social_accounts ( |
|
438 |
+ mbr_id, |
|
439 |
+ provider_type, |
|
440 |
+ social_id, |
|
441 |
+ login_id, |
|
442 |
+ social_email, |
|
443 |
+ social_name, |
|
444 |
+ is_primary_profile, |
|
445 |
+ is_active, |
|
446 |
+ rgtr |
|
447 |
+ ) VALUES ( |
|
448 |
+ #{mbrId}, |
|
449 |
+ #{providerType}, |
|
450 |
+ #{socialId}, |
|
451 |
+ #{loginId}, |
|
452 |
+ #{socialEmail}, |
|
453 |
+ #{socialName}, |
|
454 |
+ #{isPrimaryProfile}, |
|
455 |
+ #{isActive}, |
|
456 |
+ #{rgtr} |
|
457 |
+ ) |
|
458 |
+ ON CONFLICT (mbr_id, provider_type) |
|
459 |
+ DO UPDATE SET |
|
460 |
+ social_id = #{socialId}, |
|
461 |
+ login_id = #{loginId}, |
|
462 |
+ social_email = #{socialEmail}, |
|
463 |
+ social_name = #{socialName}, |
|
464 |
+ is_active = true, |
|
465 |
+ unlinked_dt = NULL, |
|
466 |
+ mdfr = #{rgtr}, |
|
467 |
+ mdfcn_dt = NOW() |
|
468 |
+ </insert> |
|
469 |
+ |
|
470 |
+ <!-- 소셜 계정 연동 해지 --> |
|
471 |
+ <update id="unlinkSocialAccount" parameterType="map"> |
|
472 |
+ UPDATE mbr_social_accounts |
|
473 |
+ SET is_active = false, |
|
474 |
+ unlinked_dt = NOW(), |
|
475 |
+ mdfr = #{mdfr}, |
|
476 |
+ mdfcn_dt = NOW() |
|
477 |
+ WHERE mbr_id = #{mbrId} |
|
478 |
+ AND provider_type = #{providerType} |
|
479 |
+ </update> |
|
480 |
+ |
|
481 |
+ <!-- 메인 프로필 설정 --> |
|
482 |
+ <update id="setPrimaryProfile" parameterType="map"> |
|
483 |
+ <!-- 기존 메인 프로필 해제 --> |
|
484 |
+ UPDATE mbr_social_accounts |
|
485 |
+ SET is_primary_profile = false, |
|
486 |
+ mdfr = #{mdfr}, |
|
487 |
+ mdfcn_dt = NOW() |
|
488 |
+ WHERE mbr_id = #{mbrId} |
|
489 |
+ AND is_primary_profile = true; |
|
490 |
+ |
|
491 |
+ <!-- 새로운 메인 프로필 설정 --> |
|
492 |
+ UPDATE mbr_social_accounts |
|
493 |
+ SET is_primary_profile = true, |
|
494 |
+ mdfr = #{mdfr}, |
|
495 |
+ mdfcn_dt = NOW() |
|
496 |
+ WHERE mbr_id = #{mbrId} |
|
497 |
+ AND provider_type = #{providerType} |
|
498 |
+ AND is_active = true; |
|
499 |
+ </update> |
|
500 |
+ |
|
501 |
+ <!-- 연동 가능한 계정 조회 (이메일로 검색, 다른 제공자 제외) --> |
|
502 |
+ <select id="findLinkableAccount" parameterType="map" resultMap="mberMap"> |
|
503 |
+ <include refid="selectMber" /> |
|
504 |
+ WHERE mi.eml = #{email} |
|
505 |
+ AND mi.use_yn = 'Y' |
|
506 |
+ AND mi.mbr_stts = '1' |
|
507 |
+ AND NOT EXISTS ( |
|
508 |
+ SELECT 1 FROM mbr_social_accounts msa |
|
509 |
+ WHERE msa.mbr_id = mi.mbr_id |
|
510 |
+ AND msa.provider_type = #{providerType} |
|
511 |
+ AND msa.is_active = true |
|
512 |
+ ) |
|
513 |
+ LIMIT 1 |
|
514 |
+ </select> |
|
328 | 515 |
</mapper>(파일 끝에 줄바꿈 문자 없음) |
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?