
Merge branch 'master' of http://210.180.118.83/jhpark/cms_backend
# Conflicts: # src/main/java/com/takensoft/common/config/SecurityConfig.java
@972ddb0f307e996d4dc3f309e5fe18d9a0feb0a1
--- build.gradle
+++ build.gradle
... | ... | @@ -47,6 +47,7 @@ |
47 | 47 |
implementation 'org.springframework.boot:spring-boot-starter-data-redis' |
48 | 48 |
implementation 'org.springframework.boot:spring-boot-starter-cache' |
49 | 49 |
|
50 |
+ |
|
50 | 51 |
implementation ('org.egovframe.rte:org.egovframe.rte.ptl.mvc:4.2.0'){ |
51 | 52 |
exclude group: 'commons-logging', module: 'commons-logging' |
52 | 53 |
} |
... | ... | @@ -83,6 +84,8 @@ |
83 | 84 |
implementation 'org.jsoup:jsoup:1.19.1' |
84 | 85 |
// Gmail SMTP |
85 | 86 |
implementation 'org.springframework.boot:spring-boot-starter-mail' |
87 |
+ //oauth2 |
|
88 |
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' |
|
86 | 89 |
|
87 | 90 |
testImplementation 'org.springframework.boot:spring-boot-starter-test' |
88 | 91 |
testImplementation 'org.springframework.security:spring-security-test' |
--- src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
+++ src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
... | ... | @@ -66,4 +66,53 @@ |
66 | 66 |
* 사용자 정보 조회 |
67 | 67 |
*/ |
68 | 68 |
MberVO findByMber(String mbrId); |
69 |
+ |
|
70 |
+ /** |
|
71 |
+ * @param email - 이메일 |
|
72 |
+ * @return MberVO - 사용자 정보 |
|
73 |
+ * |
|
74 |
+ * 이메일로만 사용자 조회 (provider 무관) |
|
75 |
+ */ |
|
76 |
+ MberVO findByEmail(String email); |
|
77 |
+ |
|
78 |
+ /** |
|
79 |
+ * @param email - 이메일 |
|
80 |
+ * @param mbrType - OAuth2 회원 유형 (K, N, G, F, S) |
|
81 |
+ * @return MberVO - OAuth2 사용자 정보 |
|
82 |
+ * |
|
83 |
+ * 이메일과 제공자로 사용자 조회 |
|
84 |
+ */ |
|
85 |
+ MberVO findByEmailAndProvider(String email, String mbrType); |
|
86 |
+ |
|
87 |
+ /** |
|
88 |
+ * @param mberVO - OAuth2 사용자 정보 |
|
89 |
+ * @return int - OAuth2 사용자 저장 결과 |
|
90 |
+ * |
|
91 |
+ * OAuth2 사용자 저장 |
|
92 |
+ */ |
|
93 |
+ int saveOAuthUser(MberVO mberVO); |
|
94 |
+ |
|
95 |
+ /** |
|
96 |
+ * @param mberVO - OAuth2 사용자 정보 |
|
97 |
+ * @return int - OAuth2 사용자 업데이트 결과 |
|
98 |
+ * |
|
99 |
+ * OAuth2 사용자 정보 업데이트 |
|
100 |
+ */ |
|
101 |
+ int updateOAuthUser(MberVO mberVO); |
|
102 |
+ |
|
103 |
+ /** |
|
104 |
+ * @param mberVO - OAuth2 연동 정보 |
|
105 |
+ * @return int - 연동 결과 |
|
106 |
+ * |
|
107 |
+ * 기존 계정에 OAuth2 정보 연동 |
|
108 |
+ */ |
|
109 |
+ int linkOAuth2Account(MberVO mberVO); |
|
110 |
+ |
|
111 |
+ /** |
|
112 |
+ * @param mbrId - 회원 ID |
|
113 |
+ * @return List<MberAuthorVO> - 회원 권한 목록 |
|
114 |
+ * |
|
115 |
+ * 회원 ID로 권한 목록 조회 |
|
116 |
+ */ |
|
117 |
+ List<MberAuthorVO> findAuthoritiesByMbrId(String mbrId); |
|
69 | 118 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/cms/mber/service/Impl/AdmMbrServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/AdmMbrServiceImpl.java
... | ... | @@ -98,7 +98,8 @@ |
98 | 98 |
@Override |
99 | 99 |
public AdmMbrDTO mbrDetail(String mbrId){ |
100 | 100 |
try { |
101 |
- AdmMbrDTO admMbrDTO = new AdmMbrDTO(); |
|
101 |
+ verificationService.verifyAccess(mbrId); // 접근 검증 |
|
102 |
+ AdmMbrDTO admMbrDTO = new AdmMbrDTO(); |
|
102 | 103 |
|
103 | 104 |
// mbrId가 있는 경우 |
104 | 105 |
if (mbrId != null) { |
--- src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
... | ... | @@ -23,7 +23,10 @@ |
23 | 23 |
|
24 | 24 |
import jakarta.servlet.http.HttpServletRequest; |
25 | 25 |
|
26 |
+import java.util.ArrayList; |
|
26 | 27 |
import java.util.HashMap; |
28 |
+import java.util.List; |
|
29 |
+ |
|
27 | 30 |
/** |
28 | 31 |
* @author takensoft |
29 | 32 |
* @since 2024.04.01 |
... | ... | @@ -57,9 +60,9 @@ |
57 | 60 |
@Transactional(readOnly = true) |
58 | 61 |
public UserDetails loadUserByUsername(String username){ |
59 | 62 |
try { |
60 |
- UserDetails userDetails = mberDAO.findByMberSecurity(username); |
|
63 |
+ UserDetails userDetails = mberDAO.findByMberSecurity(username); |
|
61 | 64 |
|
62 |
- return userDetails; |
|
65 |
+ return userDetails; |
|
63 | 66 |
} catch (UsernameNotFoundException Unfe) { |
64 | 67 |
throw Unfe; |
65 | 68 |
} catch (Exception e) { |
... | ... | @@ -107,15 +110,21 @@ |
107 | 110 |
@Transactional(rollbackFor = Exception.class) |
108 | 111 |
public HashMap<String, Object> userJoin(HttpServletRequest req, JoinDTO joinDTO){ |
109 | 112 |
try { |
110 |
- // 회원 아이디 생성 |
|
111 |
- String mbrId = mberIdgn.getNextStringId(); |
|
112 |
- joinDTO.setMbrId(mbrId); |
|
113 |
+ // 회원 아이디 생성 (이미 설정된 경우 건너뛰기) |
|
114 |
+ if (joinDTO.getMbrId() == null || joinDTO.getMbrId().isEmpty()) { |
|
115 |
+ String mbrId = mberIdgn.getNextStringId(); |
|
116 |
+ joinDTO.setMbrId(mbrId); |
|
117 |
+ } |
|
113 | 118 |
|
114 | 119 |
// 아이디 소문자 변환 |
115 |
- joinDTO.setLgnId(joinDTO.getLgnId().toLowerCase()); |
|
120 |
+ if (joinDTO.getLgnId() != null && !joinDTO.getLgnId().isEmpty()) { |
|
121 |
+ joinDTO.setLgnId(joinDTO.getLgnId().toLowerCase()); |
|
122 |
+ } |
|
116 | 123 |
|
117 |
- // 비밀번호 암호화 |
|
118 |
- joinDTO.setPswd(bCryptPasswordEncoder.encode(joinDTO.getPswd())); |
|
124 |
+ // 비밀번호 암호화 (OAuth2는 비밀번호 없음) |
|
125 |
+ if (joinDTO.getPswd() != null && !joinDTO.getPswd().isEmpty()) { |
|
126 |
+ joinDTO.setPswd(bCryptPasswordEncoder.encode(joinDTO.getPswd())); |
|
127 |
+ } |
|
119 | 128 |
|
120 | 129 |
// 연락처 암호화 |
121 | 130 |
if(joinDTO.getMblTelno() != null && !joinDTO.getMblTelno().equals("")) { |
... | ... | @@ -126,15 +135,21 @@ |
126 | 135 |
} |
127 | 136 |
|
128 | 137 |
// 아이피 조회 및 등록 |
129 |
- joinDTO.setFrstRegIp(httpRequestUtil.getIp(req)); |
|
138 |
+ if (req != null) { |
|
139 |
+ joinDTO.setFrstRegIp(httpRequestUtil.getIp(req)); |
|
140 |
+ } else { |
|
141 |
+ joinDTO.setFrstRegIp("0.0.0.0"); // OAuth2의 경우 기본값 |
|
142 |
+ } |
|
130 | 143 |
|
131 | 144 |
// 등록된 토큰에서 사용자 정보 조회 |
132 |
- String writer = verificationService.getCurrentUserId(); |
|
145 |
+ String writer = joinDTO.getRgtr(); |
|
133 | 146 |
if (writer == null || writer.isEmpty()) { |
134 |
- throw new CustomNotFoundException("사용자 정보 조회에 실패했습니다."); |
|
147 |
+ writer = verificationService.getCurrentUserId(); |
|
148 |
+ if (writer == null || writer.isEmpty()) { |
|
149 |
+ throw new CustomNotFoundException("사용자 정보 조회에 실패했습니다."); |
|
150 |
+ } |
|
151 |
+ joinDTO.setRgtr(writer); |
|
135 | 152 |
} |
136 |
- // 작성자 등록 |
|
137 |
- joinDTO.setRgtr(writer); |
|
138 | 153 |
|
139 | 154 |
// 회원정보 등록 |
140 | 155 |
HashMap<String, Object> result = new HashMap<>(); |
... | ... | @@ -143,7 +158,7 @@ |
143 | 158 |
throw new CustomInsertFailException("회원 정보 등록에 실패했습니다."); |
144 | 159 |
} |
145 | 160 |
|
146 |
- result.put("mbrId", mbrId); |
|
161 |
+ result.put("mbrId", joinDTO.getMbrId()); |
|
147 | 162 |
|
148 | 163 |
// 권한 등록 |
149 | 164 |
int authorResult = 0; |
... | ... | @@ -265,4 +280,183 @@ |
265 | 280 |
throw e; |
266 | 281 |
} |
267 | 282 |
} |
283 |
+ |
|
284 |
+ |
|
285 |
+ |
|
286 |
+ // OAuth2 관련 메서드 구현 |
|
287 |
+ |
|
288 |
+ /** |
|
289 |
+ * @param email - 이메일 |
|
290 |
+ * @return MberVO - 사용자 정보 |
|
291 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
292 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
293 |
+ * |
|
294 |
+ * 이메일로만 사용자 조회 (provider 무관) |
|
295 |
+ */ |
|
296 |
+ @Override |
|
297 |
+ @Transactional(readOnly = true) |
|
298 |
+ public MberVO findByEmail(String email) { |
|
299 |
+ try { |
|
300 |
+ return mberDAO.findByEmail(email); |
|
301 |
+ } catch (DataAccessException dae) { |
|
302 |
+ throw dae; |
|
303 |
+ } catch (Exception e) { |
|
304 |
+ throw e; |
|
305 |
+ } |
|
306 |
+ } |
|
307 |
+ |
|
308 |
+ /** |
|
309 |
+ * @param email - 이메일 |
|
310 |
+ * @param mbrType - 회원 유형 (K, N, G, F, S) |
|
311 |
+ * @return MberVO - 사용자 정보 |
|
312 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
313 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
314 |
+ * |
|
315 |
+ * 이메일과 회원 유형으로 사용자 조회 |
|
316 |
+ */ |
|
317 |
+ @Override |
|
318 |
+ @Transactional(readOnly = true) |
|
319 |
+ public MberVO findByEmailAndProvider(String email, String mbrType) { |
|
320 |
+ try { |
|
321 |
+ MberVO user = mberDAO.findByEmailAndProvider(email, mbrType); // 파라미터 직접 전달 |
|
322 |
+ |
|
323 |
+ // 권한 정보도 함께 조회 |
|
324 |
+ if (user != null) { |
|
325 |
+ List<MberAuthorVO> authorities = mberDAO.findAuthoritiesByMbrId(user.getMbrId()); |
|
326 |
+ user.setAuthorList(authorities); |
|
327 |
+ } |
|
328 |
+ return user; |
|
329 |
+ } catch (DataAccessException dae) { |
|
330 |
+ throw dae; |
|
331 |
+ } catch (Exception e) { |
|
332 |
+ throw e; |
|
333 |
+ } |
|
334 |
+ } |
|
335 |
+ |
|
336 |
+ /** |
|
337 |
+ * @param user - OAuth2 사용자 정보 |
|
338 |
+ * @return MberVO - 저장된 사용자 정보 |
|
339 |
+ * @throws CustomInsertFailException - 사용자 저장 실패 예외 발생 시 |
|
340 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
341 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
342 |
+ * |
|
343 |
+ * OAuth2 사용자 저장 - JoinDTO를 활용하여 기존 로직 재사용 |
|
344 |
+ */ |
|
345 |
+ @Override |
|
346 |
+ @Transactional(rollbackFor = Exception.class) |
|
347 |
+ public MberVO saveOAuthUser(MberVO user) { |
|
348 |
+ try { |
|
349 |
+ // OAuth2 사용자를 JoinDTO로 변환하여 기존 검증된 로직 활용 |
|
350 |
+ JoinDTO oauthJoinDTO = createOAuthJoinDTO(user); |
|
351 |
+ |
|
352 |
+ // 기존 userJoin 메서드 활용 (검증된 로직) |
|
353 |
+ HashMap<String, Object> result = userJoin(null, oauthJoinDTO); |
|
354 |
+ |
|
355 |
+ // 생성된 회원 ID 설정 |
|
356 |
+ user.setMbrId(result.get("mbrId").toString()); |
|
357 |
+ |
|
358 |
+ return user; |
|
359 |
+ |
|
360 |
+ } catch (DataAccessException dae) { |
|
361 |
+ throw dae; |
|
362 |
+ } catch (Exception e) { |
|
363 |
+ throw e; |
|
364 |
+ } |
|
365 |
+ } |
|
366 |
+ |
|
367 |
+ /** |
|
368 |
+ * OAuth2 사용자 정보를 JoinDTO로 변환 |
|
369 |
+ * @param user OAuth2 사용자 정보 |
|
370 |
+ * @return JoinDTO 변환된 회원가입 정보 |
|
371 |
+ */ |
|
372 |
+ private JoinDTO createOAuthJoinDTO(MberVO user) { |
|
373 |
+ // 기본 권한 설정 |
|
374 |
+ List<MberAuthorVO> defaultAuthorities = createDefaultAuthorities(); |
|
375 |
+ |
|
376 |
+ JoinDTO joinDTO = new JoinDTO(); |
|
377 |
+ joinDTO.setMbrNm(user.getMbrNm()); |
|
378 |
+ joinDTO.setNcnm(user.getNcnm() != null ? user.getNcnm() : user.getMbrNm()); // 닉네임이 없으면 이름 사용 |
|
379 |
+ joinDTO.setLgnId(user.getEml().toLowerCase()); // 이메일을 로그인 ID로 사용 |
|
380 |
+ joinDTO.setEml(user.getEml()); |
|
381 |
+ joinDTO.setPswd("OAUTH2_NO_PASSWORD"); // OAuth2 사용자용 더미 비밀번호 (암호화됨) |
|
382 |
+ joinDTO.setMbrStts("1"); // 승인 상태 |
|
383 |
+ joinDTO.setUseYn("Y"); // 사용 (문자열) |
|
384 |
+ joinDTO.setSmsRcptnAgreYn("N"); // SMS 수신 거부 (기본값) |
|
385 |
+ joinDTO.setEmlRcptnAgreYn("N"); // 이메일 수신 거부 (기본값) |
|
386 |
+ joinDTO.setPrvcRlsYn("N"); // 개인정보 공개 거부 (기본값) |
|
387 |
+ joinDTO.setMbrType(user.getMbrType()); // OAuth2 제공자 타입 (K, N, G, F) |
|
388 |
+ joinDTO.setSysPvsnYn("1"); // 사용자 데이터 (문자열) |
|
389 |
+ joinDTO.setRgtr("OAUTH2_SYSTEM"); // OAuth2 시스템 등록자 |
|
390 |
+ joinDTO.setAuthorList(user.getAuthorList() != null ? user.getAuthorList() : defaultAuthorities); |
|
391 |
+ |
|
392 |
+ return joinDTO; |
|
393 |
+ } |
|
394 |
+ |
|
395 |
+ /** |
|
396 |
+ * OAuth2 사용자 기본 권한 생성 |
|
397 |
+ * @return List<MberAuthorVO> 기본 권한 목록 |
|
398 |
+ */ |
|
399 |
+ private List<MberAuthorVO> createDefaultAuthorities() { |
|
400 |
+ List<MberAuthorVO> authorities = new ArrayList<>(); |
|
401 |
+ MberAuthorVO userRole = new MberAuthorVO(); |
|
402 |
+ userRole.setAuthrtCd("ROLE_USER"); |
|
403 |
+ authorities.add(userRole); |
|
404 |
+ return authorities; |
|
405 |
+ } |
|
406 |
+ |
|
407 |
+ /** |
|
408 |
+ * @param user - OAuth2 사용자 정보 |
|
409 |
+ * @return MberVO - 업데이트된 사용자 정보 |
|
410 |
+ * @throws CustomUpdateFailException - 사용자 업데이트 실패 예외 발생 시 |
|
411 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
412 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
413 |
+ * |
|
414 |
+ * OAuth2 사용자 정보 업데이트 |
|
415 |
+ */ |
|
416 |
+ @Override |
|
417 |
+ @Transactional(rollbackFor = Exception.class) |
|
418 |
+ public MberVO updateOAuthUser(MberVO user) { |
|
419 |
+ try { |
|
420 |
+ user.setMdfr("OAUTH2_SYSTEM"); |
|
421 |
+ |
|
422 |
+ int result = mberDAO.updateOAuthUser(user); |
|
423 |
+ if(result == 0) { |
|
424 |
+ throw new CustomUpdateFailException("OAuth2 사용자 정보 업데이트에 실패했습니다."); |
|
425 |
+ } |
|
426 |
+ |
|
427 |
+ return user; |
|
428 |
+ } catch (DataAccessException dae) { |
|
429 |
+ throw dae; |
|
430 |
+ } catch (Exception e) { |
|
431 |
+ throw e; |
|
432 |
+ } |
|
433 |
+ } |
|
434 |
+ |
|
435 |
+ /** |
|
436 |
+ * @param user - OAuth2 연동 정보가 추가된 사용자 정보 |
|
437 |
+ * @return MberVO - 연동된 사용자 정보 |
|
438 |
+ * @throws CustomUpdateFailException - 계정 연동 실패 예외 발생 시 |
|
439 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
440 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
441 |
+ * |
|
442 |
+ * 기존 계정에 OAuth2 정보 연동 |
|
443 |
+ */ |
|
444 |
+ @Override |
|
445 |
+ @Transactional(rollbackFor = Exception.class) |
|
446 |
+ public MberVO linkOAuth2Account(MberVO user) { |
|
447 |
+ try { |
|
448 |
+ user.setMdfr("OAUTH2_LINK"); |
|
449 |
+ |
|
450 |
+ int result = mberDAO.linkOAuth2Account(user); |
|
451 |
+ if(result == 0) { |
|
452 |
+ throw new CustomUpdateFailException("OAuth2 계정 연동에 실패했습니다."); |
|
453 |
+ } |
|
454 |
+ |
|
455 |
+ return user; |
|
456 |
+ } catch (DataAccessException dae) { |
|
457 |
+ throw dae; |
|
458 |
+ } catch (Exception e) { |
|
459 |
+ throw e; |
|
460 |
+ } |
|
461 |
+ } |
|
268 | 462 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/cms/mber/service/MberService.java
+++ src/main/java/com/takensoft/cms/mber/service/MberService.java
... | ... | @@ -62,4 +62,45 @@ |
62 | 62 |
* 비밀번호 수정 |
63 | 63 |
*/ |
64 | 64 |
public int updatePassword(PasswordDTO passwordDTO); |
65 |
+ |
|
66 |
+ /** |
|
67 |
+ * @param email - 이메일 |
|
68 |
+ * @return MberVO - 사용자 정보 |
|
69 |
+ * |
|
70 |
+ * 이메일로만 사용자 조회 |
|
71 |
+ */ |
|
72 |
+ public MberVO findByEmail(String email); |
|
73 |
+ |
|
74 |
+ /** |
|
75 |
+ * @param email - 이메일 |
|
76 |
+ * @param mbrType - 회원 유형 (K, N, G, F, S) |
|
77 |
+ * @return MberVO - 사용자 정보 |
|
78 |
+ * |
|
79 |
+ * 이메일과 회원 유형으로 사용자 조회 |
|
80 |
+ */ |
|
81 |
+ public MberVO findByEmailAndProvider(String email, String mbrType); |
|
82 |
+ |
|
83 |
+ /** |
|
84 |
+ * @param user - OAuth2 사용자 정보 |
|
85 |
+ * @return MberVO - 저장된 사용자 정보 |
|
86 |
+ * |
|
87 |
+ * OAuth2 사용자 저장 |
|
88 |
+ */ |
|
89 |
+ public MberVO saveOAuthUser(MberVO user); |
|
90 |
+ |
|
91 |
+ /** |
|
92 |
+ * @param user - OAuth2 사용자 정보 |
|
93 |
+ * @return MberVO - 업데이트된 사용자 정보 |
|
94 |
+ * |
|
95 |
+ * OAuth2 사용자 정보 업데이트 |
|
96 |
+ */ |
|
97 |
+ public MberVO updateOAuthUser(MberVO user); |
|
98 |
+ |
|
99 |
+ /** |
|
100 |
+ * @param user - OAuth2 연동 정보가 추가된 사용자 정보 |
|
101 |
+ * @return MberVO - 연동된 사용자 정보 |
|
102 |
+ * |
|
103 |
+ * 기존 계정에 OAuth2 정보 연동 |
|
104 |
+ */ |
|
105 |
+ public MberVO linkOAuth2Account(MberVO user); |
|
65 | 106 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/config/SecurityConfig.java
+++ src/main/java/com/takensoft/common/config/SecurityConfig.java
... | ... | @@ -14,6 +14,10 @@ |
14 | 14 |
import com.takensoft.common.util.LoginUtil; |
15 | 15 |
import com.takensoft.common.util.SessionUtil; |
16 | 16 |
import com.takensoft.common.verify.service.Impl.EmailServiceImpl; |
17 |
+import com.takensoft.common.oauth.service.Impl.CustomOAuth2UserServiceImpl; |
|
18 |
+import com.takensoft.common.oauth.handler.OAuth2AuthenticationSuccessHandler; |
|
19 |
+import com.takensoft.common.oauth.handler.OAuth2AuthenticationFailureHandler; |
|
20 |
+import org.springframework.beans.factory.annotation.Autowired; |
|
17 | 21 |
import org.springframework.beans.factory.annotation.Value; |
18 | 22 |
import org.springframework.context.annotation.Bean; |
19 | 23 |
import org.springframework.context.annotation.Configuration; |
... | ... | @@ -24,6 +28,7 @@ |
24 | 28 |
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
25 | 29 |
import org.springframework.security.config.http.SessionCreationPolicy; |
26 | 30 |
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
31 |
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; |
|
27 | 32 |
import org.springframework.security.web.SecurityFilterChain; |
28 | 33 |
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
29 | 34 |
import org.springframework.security.web.context.SecurityContextPersistenceFilter; |
... | ... | @@ -39,6 +44,8 @@ |
39 | 44 |
* @modification |
40 | 45 |
* since | author | description |
41 | 46 |
* 2025.01.22 | takensoft | 최초 등록 |
47 |
+ * 2025.05.22 | takensoft | OAuth2 로그인 추가 |
|
48 |
+ * 2025.05.26 | 하석형 | 로그인 유틸 추가 |
|
42 | 49 |
* |
43 | 50 |
* Spring Security 설정을 위한 Config |
44 | 51 |
*/ |
... | ... | @@ -63,6 +70,15 @@ |
63 | 70 |
private final SessionUtil sessionUtil; |
64 | 71 |
private final LoginUtil loginUtil; |
65 | 72 |
|
73 |
+ @Autowired |
|
74 |
+ private CustomOAuth2UserServiceImpl customOAuth2UserServiceImpl; |
|
75 |
+ |
|
76 |
+ @Autowired |
|
77 |
+ private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; |
|
78 |
+ |
|
79 |
+ @Autowired |
|
80 |
+ private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; |
|
81 |
+ |
|
66 | 82 |
private static String FRONT_URL; // 프론트 접근 허용 URL |
67 | 83 |
private static long JWT_ACCESSTIME; // access 토큰 유지 시간 |
68 | 84 |
private static long JWT_REFRESHTIME; // refresh 토큰 유지 시간 |
... | ... | @@ -71,16 +87,7 @@ |
71 | 87 |
private final RedisTemplate<String, String> redisTemplate; |
72 | 88 |
|
73 | 89 |
/** |
74 |
- * @param authenticationConfiguration - 인증 구성 객체 |
|
75 |
- * @param jwtUtil - JWT 유틸리티 객체 |
|
76 |
- * @param authenticationEntryPoint - 인증 실패 시 처리 엔트리 포인트 |
|
77 |
- * @param accessDenieHandler - 접근 거부 처리 핸들러 |
|
78 |
- * @param fUrl - 프론트엔드 URL (application.yml에서 값을 읽어 옴) |
|
79 |
- * @param aTime - JWT 접근 토큰 유효 시간 (application.yml에서 값을 읽어 옴) |
|
80 |
- * @param rTime - JWT 리프레시 토큰 유효 시간 (application.yml에서 값을 읽어 옴) |
|
81 |
- * @param ctime - 쿠키 유효 시간 (application.yml에서 값을 읽어 옴) |
|
82 |
- * @param redisTemplate |
|
83 |
-* |
|
90 |
+ * SecurityConfig 생성자 |
|
84 | 91 |
*/ |
85 | 92 |
public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, RefreshTokenService refreshTokenService, CntxtPthService cntxtPthService, AccesCtrlService accesCtrlService, AppConfig appConfig, |
86 | 93 |
LgnHstryService lgnHstryService, CustomAuthenticationEntryPoint authenticationEntryPoint, CustomAccessDenieHandler accessDenieHandler, HttpRequestUtil httpRequestUtil, |
... | ... | @@ -99,14 +106,12 @@ |
99 | 106 |
this.appConfig = appConfig; |
100 | 107 |
this.loginModeService = loginModeService; |
101 | 108 |
this.loginPolicyService = loginPolicyService; |
102 |
- this.emailServiceImpl = emailServiceImpl; |
|
103 | 109 |
this.sessionUtil = sessionUtil; |
104 | 110 |
this.FRONT_URL = fUrl; |
105 | 111 |
this.JWT_ACCESSTIME = aTime; |
106 | 112 |
this.JWT_REFRESHTIME = rTime; |
107 | 113 |
this.COOKIE_TIME = ctime; |
108 | 114 |
this.redisTemplate = redisTemplate; |
109 |
- this.loginUtil = loginUtil; |
|
110 | 115 |
} |
111 | 116 |
|
112 | 117 |
/** |
... | ... | @@ -147,7 +152,7 @@ |
147 | 152 |
CorsConfiguration configuration = new CorsConfiguration(); |
148 | 153 |
configuration.setAllowedOrigins(Collections.singletonList(FRONT_URL)); // 허용할 프론트 포트 포함 경로 입력 |
149 | 154 |
configuration.setAllowedMethods(Collections.singletonList("*")); // 허용할 메소드(GET, POST, PUT 등) |
150 |
- configuration.setAllowedHeaders(Collections.singletonList("*")); // 허용할 헤드 |
|
155 |
+ configuration.setAllowedHeaders(Collections.singletonList("*")); // 허용할 헤더 |
|
151 | 156 |
configuration.setAllowCredentials(true); // 프론트에서 credentials 설정하면 true |
152 | 157 |
configuration.setMaxAge(3600L); // 허용을 물고 있을 시간 |
153 | 158 |
configuration.setExposedHeaders(Collections.singletonList("Authorization")); // 서버에서 JWT를 Authorization에 담아 보내기 위해 허용을 함 |
... | ... | @@ -155,6 +160,7 @@ |
155 | 160 |
} |
156 | 161 |
}) |
157 | 162 |
); |
163 |
+ |
|
158 | 164 |
// csrf disable |
159 | 165 |
http.csrf((auth) -> auth.disable()); |
160 | 166 |
// formLogin disable |
... | ... | @@ -170,10 +176,24 @@ |
170 | 176 |
); |
171 | 177 |
|
172 | 178 |
http.authorizeHttpRequests((auth) -> auth |
173 |
- .requestMatchers("/", "/mbr/**", "/refresh/**", "/sys/**", "/editFileUpload/**", "/fileUpload/**").permitAll() // 회원, 토큰, 시스템 제공, 파일 접근 모두 허용 |
|
179 |
+ .requestMatchers("/", "/mbr/**", "/refresh/**", "/sys/**", "/editFileUpload/**", "/fileUpload/**", "/oauth2/**", "/login/oauth2/**", "/.well-known/**").permitAll() // 회원, 토큰, 시스템 제공, 파일, OAuth2 접근 모두 허용 |
|
174 | 180 |
.requestMatchers("/admin/**").hasRole("ADMIN") // 관리자 페이지는 ADMIN 권한을 가진 사용자만 접근 가능 |
175 | 181 |
.anyRequest().authenticated() // 그 외에는 로그인한 사용자만 접근 가능 |
176 |
-// .anyRequest().permitAll() // 모든 사용자 접근 가능 |
|
182 |
+ ); |
|
183 |
+ |
|
184 |
+ // OAuth2 로그인 설정 |
|
185 |
+ http.oauth2Login(oauth2 -> oauth2 |
|
186 |
+ .authorizationEndpoint(authorization -> authorization |
|
187 |
+ .baseUri("/oauth2/authorization") |
|
188 |
+ ) |
|
189 |
+ .redirectionEndpoint(redirection -> redirection |
|
190 |
+ .baseUri("/login/oauth2/code/*") |
|
191 |
+ ) |
|
192 |
+ .userInfoEndpoint(userInfo -> userInfo |
|
193 |
+ .userService(customOAuth2UserServiceImpl) |
|
194 |
+ ) |
|
195 |
+ .successHandler(oAuth2AuthenticationSuccessHandler) |
|
196 |
+ .failureHandler(oAuth2AuthenticationFailureHandler) |
|
177 | 197 |
); |
178 | 198 |
|
179 | 199 |
// Context Path 검증 필터 |
... | ... | @@ -190,6 +210,7 @@ |
190 | 210 |
loginModeService, loginPolicyService, emailServiceImpl, sessionUtil, JWT_ACCESSTIME, JWT_REFRESHTIME, COOKIE_TIME, redisTemplate, |
191 | 211 |
loginUtil), UsernamePasswordAuthenticationFilter.class); |
192 | 212 |
|
213 |
+ |
|
193 | 214 |
return http.build(); |
194 | 215 |
} |
195 | 216 |
}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/java/com/takensoft/common/filter/AccesFilter.java
+++ src/main/java/com/takensoft/common/filter/AccesFilter.java
... | ... | @@ -58,6 +58,14 @@ |
58 | 58 |
*/ |
59 | 59 |
@Override |
60 | 60 |
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { |
61 |
+ |
|
62 |
+ String requestURI = request.getRequestURI(); |
|
63 |
+ |
|
64 |
+ // OAuth2 관련 경로는 접근 제어 제외 |
|
65 |
+ if (isOAuth2Request(requestURI)) { |
|
66 |
+ filterChain.doFilter(request, response); |
|
67 |
+ return; |
|
68 |
+ } |
|
61 | 69 |
try { |
62 | 70 |
// 아이피 정보 |
63 | 71 |
String ipAdrs = httpRequestUtil.getIp(request); |
... | ... | @@ -105,6 +113,15 @@ |
105 | 113 |
} |
106 | 114 |
|
107 | 115 |
/** |
116 |
+ * OAuth2 관련 요청인지 확인 |
|
117 |
+ */ |
|
118 |
+ private boolean isOAuth2Request(String requestURI) { |
|
119 |
+ return requestURI.startsWith("/oauth2/") || |
|
120 |
+ requestURI.startsWith("/login/oauth2/") || |
|
121 |
+ requestURI.startsWith("/oauth/"); |
|
122 |
+ } |
|
123 |
+ |
|
124 |
+ /** |
|
108 | 125 |
* @param accesCtrlList 접근 제어 정보 리스트 |
109 | 126 |
* @param req HttpServletRequest 객체 |
110 | 127 |
* @return boolean 요청 URI에 따른 접근 제어 여부 |
--- src/main/java/com/takensoft/common/filter/JWTFilter.java
+++ src/main/java/com/takensoft/common/filter/JWTFilter.java
... | ... | @@ -71,6 +71,13 @@ |
71 | 71 |
*/ |
72 | 72 |
@Override |
73 | 73 |
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { |
74 |
+ String requestURI = request.getRequestURI(); |
|
75 |
+ |
|
76 |
+ // OAuth2 관련 경로는 JWT 검증 제외 |
|
77 |
+ if (isOAuth2Request(requestURI)) { |
|
78 |
+ filterChain.doFilter(request, response); |
|
79 |
+ return; |
|
80 |
+ } |
|
74 | 81 |
try { |
75 | 82 |
String loginMode = loginModeService.getLoginMode(); |
76 | 83 |
String accessToken = resolveToken(request, loginMode); |
... | ... | @@ -120,6 +127,16 @@ |
120 | 127 |
} |
121 | 128 |
} |
122 | 129 |
|
130 |
+ /** |
|
131 |
+ * OAuth2 관련 요청인지 확인 |
|
132 |
+ */ |
|
133 |
+ private boolean isOAuth2Request(String requestURI) { |
|
134 |
+ return requestURI.startsWith("/oauth2/") || |
|
135 |
+ requestURI.startsWith("/login/oauth2/") || |
|
136 |
+ requestURI.startsWith("/oauth/"); |
|
137 |
+ } |
|
138 |
+ |
|
139 |
+ |
|
123 | 140 |
private boolean isTokenValid(String mbrId, String accessToken) { |
124 | 141 |
String storedToken = redisTemplate.opsForValue().get("jwt:" + mbrId); |
125 | 142 |
return storedToken == null || storedToken.equals(accessToken); |
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationFailureHandler.java
... | ... | @@ -0,0 +1,70 @@ |
1 | +package com.takensoft.common.oauth.handler; | |
2 | + | |
3 | +import jakarta.servlet.ServletException; | |
4 | +import jakarta.servlet.http.HttpServletRequest; | |
5 | +import jakarta.servlet.http.HttpServletResponse; | |
6 | +import lombok.RequiredArgsConstructor; | |
7 | +import lombok.extern.slf4j.Slf4j; | |
8 | +import org.springframework.beans.factory.annotation.Value; | |
9 | +import org.springframework.security.core.AuthenticationException; | |
10 | +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; | |
11 | +import org.springframework.stereotype.Component; | |
12 | + | |
13 | +import java.io.IOException; | |
14 | +import java.net.URLEncoder; | |
15 | +import java.nio.charset.StandardCharsets; | |
16 | + | |
17 | +/** | |
18 | + * @author takensoft | |
19 | + * @since 2025.05.22 | |
20 | + * @modification | |
21 | + * since | author | description | |
22 | + * 2025.05.22 | takensoft | 최초 등록 | |
23 | + * | |
24 | + * OAuth2 로그인 실패 핸들러 | |
25 | + */ | |
26 | +@Slf4j | |
27 | +@Component | |
28 | +@RequiredArgsConstructor | |
29 | +public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { | |
30 | + | |
31 | + @Value("${front.url}") | |
32 | + private String FRONT_URL; | |
33 | + | |
34 | + @Override | |
35 | + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, | |
36 | + AuthenticationException exception) throws IOException, ServletException { | |
37 | + | |
38 | + log.error("OAuth2 로그인 실패: {}", exception.getMessage()); | |
39 | + | |
40 | + String errorMessage = mapErrorMessage(exception); | |
41 | + String encodedMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8); | |
42 | + | |
43 | + String redirectUrl = String.format("%s/login?error=oauth2_failed&message=%s", | |
44 | + FRONT_URL, encodedMessage); | |
45 | + | |
46 | + getRedirectStrategy().sendRedirect(request, response, redirectUrl); | |
47 | + } | |
48 | + | |
49 | + /** | |
50 | + * 에러 메시지 매핑 | |
51 | + */ | |
52 | + private String mapErrorMessage(AuthenticationException exception) { | |
53 | + String message = exception.getMessage(); | |
54 | + | |
55 | + if (message.contains("access_denied")) { | |
56 | + return "사용자가 인증을 취소했습니다."; | |
57 | + } | |
58 | + if (message.contains("invalid_request")) { | |
59 | + return "잘못된 OAuth2 요청입니다."; | |
60 | + } | |
61 | + if (message.contains("unauthorized_client")) { | |
62 | + return "인증되지 않은 클라이언트입니다."; | |
63 | + } | |
64 | + if (message.contains("server_error")) { | |
65 | + return "OAuth2 서버 오류가 발생했습니다."; | |
66 | + } | |
67 | + | |
68 | + return "OAuth2 로그인에 실패했습니다."; | |
69 | + } | |
70 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
... | ... | @@ -0,0 +1,176 @@ |
1 | +package com.takensoft.common.oauth.handler; | |
2 | + | |
3 | +import com.takensoft.cms.loginPolicy.service.LoginModeService; | |
4 | +import com.takensoft.cms.loginPolicy.service.LoginPolicyService; | |
5 | +import com.takensoft.cms.mber.service.LgnHstryService; | |
6 | +import com.takensoft.cms.mber.vo.LgnHstryVO; | |
7 | +import com.takensoft.common.oauth.vo.CustomOAuth2UserVO; | |
8 | +import com.takensoft.common.util.HttpRequestUtil; | |
9 | +import com.takensoft.common.util.JWTUtil; | |
10 | +import com.takensoft.common.util.SessionUtil; | |
11 | +import jakarta.servlet.ServletException; | |
12 | +import jakarta.servlet.http.HttpServletRequest; | |
13 | +import jakarta.servlet.http.HttpServletResponse; | |
14 | +import jakarta.servlet.http.HttpSession; | |
15 | +import lombok.RequiredArgsConstructor; | |
16 | +import lombok.extern.slf4j.Slf4j; | |
17 | +import org.springframework.beans.factory.annotation.Value; | |
18 | +import org.springframework.context.ApplicationEventPublisher; | |
19 | +import org.springframework.data.redis.core.RedisTemplate; | |
20 | +import org.springframework.security.core.Authentication; | |
21 | +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; | |
22 | +import org.springframework.stereotype.Component; | |
23 | + | |
24 | +import java.io.IOException; | |
25 | +import java.util.concurrent.TimeUnit; | |
26 | + | |
27 | +/** | |
28 | + * @author takensoft | |
29 | + * @since 2025.05.22 | |
30 | + * @modification | |
31 | + * since | author | description | |
32 | + * 2025.05.22 | takensoft | 최초 등록 | |
33 | + * | |
34 | + * OAuth2 로그인 성공 핸들러 | |
35 | + */ | |
36 | +@Slf4j | |
37 | +@Component | |
38 | +@RequiredArgsConstructor | |
39 | +public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { | |
40 | + | |
41 | + private final LgnHstryService lgnHstryService; | |
42 | + private final HttpRequestUtil httpRequestUtil; | |
43 | + private final LoginModeService loginModeService; | |
44 | + private final LoginPolicyService loginPolicyService; | |
45 | + private final SessionUtil sessionUtil; | |
46 | + private final JWTUtil jwtUtil; | |
47 | + private final RedisTemplate<String, String> redisTemplate; | |
48 | + private final ApplicationEventPublisher eventPublisher; | |
49 | + | |
50 | + @Value("${jwt.accessTime}") | |
51 | + private long JWT_ACCESSTIME; | |
52 | + | |
53 | + @Value("${front.url}") | |
54 | + private String FRONT_URL; | |
55 | + | |
56 | + @Override | |
57 | + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, | |
58 | + Authentication authentication) throws IOException, ServletException { | |
59 | + | |
60 | + CustomOAuth2UserVO oAuth2User = (CustomOAuth2UserVO) authentication.getPrincipal(); | |
61 | + | |
62 | + try { | |
63 | + log.info("OAuth2 로그인 성공 - Provider: {}, Email: {}", | |
64 | + oAuth2User.getProvider(), oAuth2User.getEmail()); | |
65 | + | |
66 | + // 1. 비동기로 사용자 정보 저장/업데이트 처리 | |
67 | + eventPublisher.publishEvent(new OAuth2UserSaveEvent(oAuth2User)); | |
68 | + | |
69 | + // 2. 로그인 이력 저장 | |
70 | + saveLoginHistory(request, oAuth2User); | |
71 | + | |
72 | + // 3. 로그인 모드에 따른 토큰/세션 처리 | |
73 | + String loginMode = loginModeService.getLoginMode(); | |
74 | + String tempUserId = createTempUserId(oAuth2User); | |
75 | + processLoginByMode(request, response, oAuth2User, tempUserId, loginMode); | |
76 | + | |
77 | + // 4. 캐시 방지 헤더 설정 | |
78 | + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); | |
79 | + response.setHeader("Pragma", "no-cache"); | |
80 | + response.setHeader("Expires", "0"); | |
81 | + | |
82 | + // 5. 프론트엔드 로그인 페이지로 리다이렉트 (OAuth 성공 파라미터 + 타임스탬프) | |
83 | + long timestamp = System.currentTimeMillis(); | |
84 | + String redirectUrl = FRONT_URL + "/login.page?oauth_success=true&t=" + timestamp; | |
85 | + getRedirectStrategy().sendRedirect(request, response, redirectUrl); | |
86 | + | |
87 | + } catch (Exception e) { | |
88 | + log.error("OAuth2 로그인 처리 중 오류 발생", e); | |
89 | + String errorUrl = FRONT_URL + "/login.page?error=oauth2_processing_failed"; | |
90 | + getRedirectStrategy().sendRedirect(request, response, errorUrl); | |
91 | + } | |
92 | + } | |
93 | + | |
94 | + /** | |
95 | + * 로그인 이력 저장 | |
96 | + */ | |
97 | + private void saveLoginHistory(HttpServletRequest request, CustomOAuth2UserVO oAuth2User) { | |
98 | + try { | |
99 | + LgnHstryVO lgnHstryVO = new LgnHstryVO(); | |
100 | + lgnHstryVO.setLgnId(oAuth2User.getEmail()); | |
101 | + lgnHstryVO.setLgnType("1"); // 일반 사용자 | |
102 | + lgnHstryVO.setCntnIp(httpRequestUtil.getIp(request)); | |
103 | + lgnHstryVO.setCntnOperSys(httpRequestUtil.getOS(httpRequestUtil.getUserAgent(request))); | |
104 | + lgnHstryVO.setDeviceNm(httpRequestUtil.getDevice(httpRequestUtil.getUserAgent(request))); | |
105 | + lgnHstryVO.setBrwsrNm(httpRequestUtil.getBrowser(httpRequestUtil.getUserAgent(request))); | |
106 | + | |
107 | + lgnHstryService.LgnHstrySave(lgnHstryVO); | |
108 | + } catch (Exception e) { | |
109 | + log.error("OAuth2 로그인 이력 저장 실패", e); | |
110 | + } | |
111 | + } | |
112 | + | |
113 | + /** | |
114 | + * 임시 사용자 ID 생성 | |
115 | + */ | |
116 | + private String createTempUserId(CustomOAuth2UserVO oAuth2User) { | |
117 | + return oAuth2User.getProvider() + "_" + oAuth2User.getId(); | |
118 | + } | |
119 | + | |
120 | + /** | |
121 | + * 로그인 모드에 따른 처리 | |
122 | + */ | |
123 | + private void processLoginByMode(HttpServletRequest request, HttpServletResponse response, | |
124 | + CustomOAuth2UserVO oAuth2User, String tempUserId, String loginMode) | |
125 | + throws IOException { | |
126 | + | |
127 | + // JWT 토큰 생성 | |
128 | + String accessToken = jwtUtil.createJwt( | |
129 | + "Authorization", | |
130 | + tempUserId, | |
131 | + oAuth2User.getEmail(), | |
132 | + oAuth2User.getName(), | |
133 | + null, // roles는 DB 저장 후 갱신 예정 | |
134 | + JWT_ACCESSTIME | |
135 | + ); | |
136 | + | |
137 | + if ("S".equals(loginMode)) { | |
138 | + // 세션 모드 처리 | |
139 | + processSessionMode(request, tempUserId, accessToken, oAuth2User); | |
140 | + } else { | |
141 | + // JWT 모드 처리 (기본값) | |
142 | + processJwtMode(response, tempUserId, accessToken); | |
143 | + } | |
144 | + | |
145 | + response.setHeader("login-type", loginMode); | |
146 | + } | |
147 | + | |
148 | + /** | |
149 | + * 세션 모드 처리 | |
150 | + */ | |
151 | + private void processSessionMode(HttpServletRequest request, String tempUserId, | |
152 | + String accessToken, CustomOAuth2UserVO oAuth2User) { | |
153 | + HttpSession session = request.getSession(true); | |
154 | + session.setAttribute("JWT_TOKEN", accessToken); | |
155 | + session.setAttribute("oauth2User", oAuth2User); | |
156 | + | |
157 | + // 중복 로그인 정책 확인 | |
158 | + if (!loginPolicyService.getPolicy()) { | |
159 | + sessionUtil.registerSession(tempUserId, session); | |
160 | + } | |
161 | + } | |
162 | + | |
163 | + /** | |
164 | + * JWT 모드 처리 | |
165 | + */ | |
166 | + private void processJwtMode(HttpServletResponse response, String tempUserId, String accessToken) { | |
167 | + response.setHeader("Authorization", accessToken); | |
168 | + | |
169 | + // 중복 로그인 정책 확인 | |
170 | + if (!loginPolicyService.getPolicy()) { | |
171 | + redisTemplate.delete("jwt:" + tempUserId); | |
172 | + redisTemplate.opsForValue().set("jwt:" + tempUserId, accessToken, | |
173 | + JWT_ACCESSTIME, TimeUnit.MILLISECONDS); | |
174 | + } | |
175 | + } | |
176 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2UserSaveEvent.java
... | ... | @@ -0,0 +1,25 @@ |
1 | +package com.takensoft.common.oauth.handler; | |
2 | + | |
3 | +import com.takensoft.common.oauth.vo.CustomOAuth2UserVO; | |
4 | +import lombok.Getter; | |
5 | +import org.springframework.context.ApplicationEvent; | |
6 | + | |
7 | +/** | |
8 | + * @author takensoft | |
9 | + * @since 2025.05.22 | |
10 | + * @modification | |
11 | + * since | author | description | |
12 | + * 2025.05.22 | takensoft | 최초 등록 | |
13 | + * | |
14 | + * OAuth2 사용자 저장 이벤트 | |
15 | + */ | |
16 | +@Getter | |
17 | +public class OAuth2UserSaveEvent extends ApplicationEvent { | |
18 | + | |
19 | + private final CustomOAuth2UserVO oAuth2User; | |
20 | + | |
21 | + public OAuth2UserSaveEvent(CustomOAuth2UserVO oAuth2User) { | |
22 | + super(oAuth2User); | |
23 | + this.oAuth2User = oAuth2User; | |
24 | + } | |
25 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2UserSaveEventListener.java
... | ... | @@ -0,0 +1,119 @@ |
1 | +package com.takensoft.common.oauth.handler; | |
2 | + | |
3 | +import com.takensoft.cms.mber.service.MberService; | |
4 | +import com.takensoft.cms.mber.vo.MberAuthorVO; | |
5 | +import com.takensoft.cms.mber.vo.MberVO; | |
6 | +import com.takensoft.common.oauth.vo.CustomOAuth2UserVO; | |
7 | +import lombok.RequiredArgsConstructor; | |
8 | +import lombok.extern.slf4j.Slf4j; | |
9 | +import org.springframework.context.event.EventListener; | |
10 | +import org.springframework.scheduling.annotation.Async; | |
11 | +import org.springframework.stereotype.Component; | |
12 | + | |
13 | +import java.util.ArrayList; | |
14 | +import java.util.List; | |
15 | + | |
16 | +/** | |
17 | + * @author takensoft | |
18 | + * @since 2025.05.22 | |
19 | + * @modification | |
20 | + * since | author | description | |
21 | + * 2025.05.22 | takensoft | 최초 등록 | |
22 | + * | |
23 | + * OAuth2 사용자 저장 이벤트 리스너 | |
24 | + */ | |
25 | +@Component | |
26 | +@RequiredArgsConstructor | |
27 | +@Slf4j | |
28 | +public class OAuth2UserSaveEventListener { | |
29 | + | |
30 | + private final MberService mberService; | |
31 | + | |
32 | + @EventListener | |
33 | + @Async | |
34 | + public void handleOAuth2UserSaveEvent(OAuth2UserSaveEvent event) { | |
35 | + try { | |
36 | + CustomOAuth2UserVO oAuth2User = event.getOAuth2User(); | |
37 | + processOAuth2User(oAuth2User); | |
38 | + } catch (Exception e) { | |
39 | + log.error("OAuth2 사용자 저장 이벤트 처리 실패", e); | |
40 | + } | |
41 | + } | |
42 | + | |
43 | + /** | |
44 | + * OAuth2 사용자 처리 (저장 또는 업데이트) | |
45 | + */ | |
46 | + private void processOAuth2User(CustomOAuth2UserVO oAuth2User) { | |
47 | + String mbrType = convertProviderToMbrType(oAuth2User.getProvider()); | |
48 | + | |
49 | + // 이메일과 제공자로 기존 사용자 조회 | |
50 | + MberVO existingUser = mberService.findByEmailAndProvider(oAuth2User.getEmail(), mbrType); | |
51 | + | |
52 | + if (existingUser != null) { | |
53 | + updateExistingUser(existingUser, oAuth2User); | |
54 | + } else { | |
55 | + createNewUser(oAuth2User, mbrType); | |
56 | + } | |
57 | + } | |
58 | + | |
59 | + /** | |
60 | + * 기존 사용자 정보 업데이트 | |
61 | + */ | |
62 | + private void updateExistingUser(MberVO existingUser, CustomOAuth2UserVO oAuth2User) { | |
63 | + try { | |
64 | + existingUser.setMbrNm(oAuth2User.getName()); | |
65 | + // 프로필 이미지 URL이 있다면 업데이트 | |
66 | + if (oAuth2User.getImageUrl() != null) { | |
67 | + // 필요시 프로필 이미지 필드 추가 | |
68 | + } | |
69 | + | |
70 | + mberService.updateOAuthUser(existingUser); | |
71 | + log.info("기존 OAuth2 사용자 정보 업데이트 완료: {}", existingUser.getEml()); | |
72 | + } catch (Exception e) { | |
73 | + log.error("기존 OAuth2 사용자 업데이트 실패: {}", oAuth2User.getEmail(), e); | |
74 | + } | |
75 | + } | |
76 | + | |
77 | + /** | |
78 | + * 새 OAuth2 사용자 생성 | |
79 | + */ | |
80 | + private void createNewUser(CustomOAuth2UserVO oAuth2User, String mbrType) { | |
81 | + try { | |
82 | + MberVO newUser = new MberVO(); | |
83 | + newUser.setEml(oAuth2User.getEmail()); | |
84 | + newUser.setMbrNm(oAuth2User.getName()); | |
85 | + newUser.setNcnm(oAuth2User.getName()); | |
86 | + newUser.setMbrType(mbrType); | |
87 | + newUser.setAuthorList(createDefaultAuthorities()); | |
88 | + | |
89 | + mberService.saveOAuthUser(newUser); | |
90 | + log.info("새 OAuth2 사용자 생성 완료: {}", newUser.getEml()); | |
91 | + } catch (Exception e) { | |
92 | + log.error("새 OAuth2 사용자 생성 실패: {}", oAuth2User.getEmail(), e); | |
93 | + } | |
94 | + } | |
95 | + | |
96 | + /** | |
97 | + * 기본 권한 생성 | |
98 | + */ | |
99 | + private List<MberAuthorVO> createDefaultAuthorities() { | |
100 | + List<MberAuthorVO> authorities = new ArrayList<>(); | |
101 | + MberAuthorVO userRole = new MberAuthorVO(); | |
102 | + userRole.setAuthrtCd("ROLE_USER"); | |
103 | + authorities.add(userRole); | |
104 | + return authorities; | |
105 | + } | |
106 | + | |
107 | + /** | |
108 | + * OAuth2 제공자를 회원 타입으로 변환 | |
109 | + */ | |
110 | + private String convertProviderToMbrType(String provider) { | |
111 | + return switch (provider.toLowerCase()) { | |
112 | + case "kakao" -> "K"; | |
113 | + case "naver" -> "N"; | |
114 | + case "google" -> "G"; | |
115 | + case "facebook" -> "F"; | |
116 | + default -> "S"; // 일반 회원 | |
117 | + }; | |
118 | + } | |
119 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/common/oauth/service/CustomOAuth2UserService.java
... | ... | @@ -0,0 +1,27 @@ |
1 | +package com.takensoft.common.oauth.service; | |
2 | + | |
3 | +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; | |
4 | +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; | |
5 | +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; | |
6 | +import org.springframework.security.oauth2.core.user.OAuth2User; | |
7 | + | |
8 | +/** | |
9 | + * @author takensoft | |
10 | + * @since 2025.05.22 | |
11 | + * @modification | |
12 | + * since | author | description | |
13 | + * 2025.05.22 | takensoft | 최초 등록 | |
14 | + * | |
15 | + * OAuth2 사용자 정보 서비스 인터페이스 | |
16 | + */ | |
17 | +public interface CustomOAuth2UserService extends OAuth2UserService<OAuth2UserRequest, OAuth2User> { | |
18 | + | |
19 | + /** | |
20 | + * @param userRequest - OAuth2 사용자 요청 | |
21 | + * @return OAuth2User - OAuth2 사용자 정보 | |
22 | + * @throws OAuth2AuthenticationException - OAuth2 인증 예외 | |
23 | + * | |
24 | + * OAuth2 사용자 정보 로드 | |
25 | + */ | |
26 | + OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException; | |
27 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/common/oauth/service/Impl/CustomOAuth2UserServiceImpl.java
... | ... | @@ -0,0 +1,86 @@ |
1 | +package com.takensoft.common.oauth.service.Impl; | |
2 | + | |
3 | +import com.takensoft.common.oauth.service.CustomOAuth2UserService; | |
4 | +import com.takensoft.common.oauth.vo.CustomOAuth2UserVO; | |
5 | +import com.takensoft.common.oauth.vo.OAuth2UserInfoVO; | |
6 | +import lombok.RequiredArgsConstructor; | |
7 | +import lombok.extern.slf4j.Slf4j; | |
8 | +import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; | |
9 | +import org.springframework.dao.DataAccessException; | |
10 | +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; | |
11 | +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; | |
12 | +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; | |
13 | +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; | |
14 | +import org.springframework.security.oauth2.core.user.OAuth2User; | |
15 | +import org.springframework.stereotype.Service; | |
16 | + | |
17 | +/** | |
18 | + * @author takensoft | |
19 | + * @since 2025.05.22 | |
20 | + * @modification | |
21 | + * since | author | description | |
22 | + * 2025.05.22 | takensoft | 최초 등록 | |
23 | + * | |
24 | + * OAuth2 사용자 정보 서비스 구현체 | |
25 | + * EgovAbstractServiceImpl : 전자정부 상속 | |
26 | + * CustomOAuth2UserService : OAuth2 사용자 정보 인터페이스 상속 | |
27 | + * OAuth2UserService : Spring Security OAuth2 인터페이스 상속 | |
28 | + */ | |
29 | +@Service("customOAuth2UserService") | |
30 | +@RequiredArgsConstructor | |
31 | +@Slf4j | |
32 | +public class CustomOAuth2UserServiceImpl extends EgovAbstractServiceImpl | |
33 | + implements CustomOAuth2UserService, OAuth2UserService<OAuth2UserRequest, OAuth2User> { | |
34 | + | |
35 | + private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); | |
36 | + | |
37 | + /** | |
38 | + * @param userRequest - OAuth2 사용자 요청 | |
39 | + * @return OAuth2User - OAuth2 사용자 정보 | |
40 | + * @throws OAuth2AuthenticationException - OAuth2 인증 예외 발생 시 | |
41 | + * @throws DataAccessException - db 관련 예외 발생 시 | |
42 | + * @throws Exception - 그 외 예외 발생 시 | |
43 | + * | |
44 | + * OAuth2 사용자 정보 로드 (최소 처리) | |
45 | + */ | |
46 | + @Override | |
47 | + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { | |
48 | + try { | |
49 | + OAuth2User oAuth2User = delegate.loadUser(userRequest); | |
50 | + return processOAuth2User(userRequest, oAuth2User); | |
51 | + } catch (DataAccessException dae) { | |
52 | + log.error("OAuth2 사용자 처리 중 DB 오류 발생", dae); | |
53 | + throw new OAuth2AuthenticationException("OAuth2 사용자 처리 실패 - DB 오류"); | |
54 | + } catch (Exception e) { | |
55 | + log.error("OAuth2 사용자 처리 중 오류 발생", e); | |
56 | + throw new OAuth2AuthenticationException("OAuth2 사용자 처리 실패"); | |
57 | + } | |
58 | + } | |
59 | + | |
60 | + /** | |
61 | + * @param userRequest - OAuth2 사용자 요청 | |
62 | + * @param oAuth2User - OAuth2 사용자 정보 | |
63 | + * @return OAuth2User - 처리된 OAuth2 사용자 정보 | |
64 | + * @throws DataAccessException - db 관련 예외 발생 시 | |
65 | + * @throws Exception - 그 외 예외 발생 시 | |
66 | + * | |
67 | + * OAuth2 사용자 정보 처리 (단순히 CustomOAuth2User 객체만 생성) | |
68 | + */ | |
69 | + private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oAuth2User) { | |
70 | + try { | |
71 | + // 제공업체 정보 추출 | |
72 | + String registrationId = userRequest.getClientRegistration().getRegistrationId(); | |
73 | + | |
74 | + // 통합 OAuth2UserInfo 생성 | |
75 | + OAuth2UserInfoVO oAuth2UserInfo = new OAuth2UserInfoVO(registrationId, oAuth2User.getAttributes()); | |
76 | + | |
77 | + // DB 작업 없이 단순히 CustomOAuth2User 객체만 반환 | |
78 | + // 실제 사용자 저장/업데이트는 OAuth2SuccessHandler에서 처리 | |
79 | + return new CustomOAuth2UserVO(oAuth2UserInfo, oAuth2User.getAttributes()); | |
80 | + } catch (DataAccessException dae) { | |
81 | + throw dae; | |
82 | + } catch (Exception e) { | |
83 | + throw e; | |
84 | + } | |
85 | + } | |
86 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/common/oauth/vo/CustomOAuth2UserVO.java
... | ... | @@ -0,0 +1,57 @@ |
1 | +package com.takensoft.common.oauth.vo; | |
2 | + | |
3 | +import lombok.RequiredArgsConstructor; | |
4 | +import org.springframework.security.core.GrantedAuthority; | |
5 | +import org.springframework.security.oauth2.core.user.OAuth2User; | |
6 | + | |
7 | +import java.util.Collection; | |
8 | +import java.util.Collections; | |
9 | +import java.util.Map; | |
10 | + | |
11 | +/** | |
12 | + * @author takensoft | |
13 | + * @since 2025.05.22 | |
14 | + * @modification | |
15 | + * since | author | description | |
16 | + * 2025.05.22 | takensoft | 최초 등록 | |
17 | + * | |
18 | + * OAuth2 사용자 커스텀 클래스 | |
19 | + */ | |
20 | +@RequiredArgsConstructor | |
21 | +public class CustomOAuth2UserVO implements OAuth2User { | |
22 | + | |
23 | + private final OAuth2UserInfoVO oAuth2UserInfoVO; | |
24 | + private final Map<String, Object> attributes; | |
25 | + | |
26 | + @Override | |
27 | + public Map<String, Object> getAttributes() { | |
28 | + return attributes; | |
29 | + } | |
30 | + | |
31 | + @Override | |
32 | + public Collection<? extends GrantedAuthority> getAuthorities() { | |
33 | + return Collections.emptyList(); // 권한 설정이 필요한 경우 여기서 처리 | |
34 | + } | |
35 | + | |
36 | + @Override | |
37 | + public String getName() { | |
38 | + return oAuth2UserInfoVO.getName(); | |
39 | + } | |
40 | + | |
41 | + // 추가 getter 메서드들 | |
42 | + public String getId() { | |
43 | + return oAuth2UserInfoVO.getId(); | |
44 | + } | |
45 | + | |
46 | + public String getEmail() { | |
47 | + return oAuth2UserInfoVO.getEmail(); | |
48 | + } | |
49 | + | |
50 | + public String getImageUrl() { | |
51 | + return oAuth2UserInfoVO.getImageUrl(); | |
52 | + } | |
53 | + | |
54 | + public String getProvider() { | |
55 | + return oAuth2UserInfoVO.getProvider(); | |
56 | + } | |
57 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/common/oauth/vo/OAuth2UserInfoVO.java
... | ... | @@ -0,0 +1,75 @@ |
1 | +package com.takensoft.common.oauth.vo; | |
2 | + | |
3 | +import lombok.AllArgsConstructor; | |
4 | +import lombok.Getter; | |
5 | +import lombok.NoArgsConstructor; | |
6 | +import lombok.Setter; | |
7 | + | |
8 | +import java.util.Map; | |
9 | + | |
10 | +/** | |
11 | + * @author takensoft | |
12 | + * @since 2025.05.22 | |
13 | + * @modification | |
14 | + * since | author | description | |
15 | + * 2025.05.22 | takensoft | 최초 등록 | |
16 | + * | |
17 | + * OAuth2 사용자 정보 통합 VO | |
18 | + */ | |
19 | +@Getter | |
20 | +@Setter | |
21 | +@NoArgsConstructor | |
22 | +@AllArgsConstructor | |
23 | +public class OAuth2UserInfoVO { | |
24 | + | |
25 | + private Map<String, Object> attributes; // OAuth2 응답 전체 데이터 | |
26 | + private String provider; // 제공업체 (kakao, naver, google 등) | |
27 | + private String id; // 사용자 ID | |
28 | + private String name; // 사용자 이름 | |
29 | + private String email; // 이메일 | |
30 | + private String imageUrl; // 프로필 이미지 URL | |
31 | + | |
32 | + // 제공업체별 데이터를 통일된 형태로 변환하는 생성자 | |
33 | + public OAuth2UserInfoVO(String provider, Map<String, Object> attributes) { | |
34 | + this.provider = provider; | |
35 | + this.attributes = attributes; | |
36 | + | |
37 | + switch (provider.toLowerCase()) { | |
38 | + case "kakao": | |
39 | + setKakaoUserInfo(attributes); | |
40 | + break; | |
41 | + case "naver": | |
42 | + setNaverUserInfo(attributes); | |
43 | + break; | |
44 | + case "google": | |
45 | + setGoogleUserInfo(attributes); | |
46 | + break; | |
47 | + } | |
48 | + } | |
49 | + | |
50 | + private void setKakaoUserInfo(Map<String, Object> attributes) { | |
51 | + Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account"); | |
52 | + Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile"); | |
53 | + | |
54 | + this.id = String.valueOf(attributes.get("id")); | |
55 | + this.name = (String) profile.get("nickname"); | |
56 | + this.email = (String) kakaoAccount.get("email"); | |
57 | + this.imageUrl = (String) profile.get("profile_image_url"); | |
58 | + } | |
59 | + | |
60 | + private void setNaverUserInfo(Map<String, Object> attributes) { | |
61 | + Map<String, Object> response = (Map<String, Object>) attributes.get("response"); | |
62 | + | |
63 | + this.id = (String) response.get("id"); | |
64 | + this.name = (String) response.get("name"); | |
65 | + this.email = (String) response.get("email"); | |
66 | + this.imageUrl = (String) response.get("profile_image"); | |
67 | + } | |
68 | + | |
69 | + private void setGoogleUserInfo(Map<String, Object> attributes) { | |
70 | + this.id = (String) attributes.get("sub"); | |
71 | + this.name = (String) attributes.get("name"); | |
72 | + this.email = (String) attributes.get("email"); | |
73 | + this.imageUrl = (String) attributes.get("picture"); | |
74 | + } | |
75 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
... | ... | @@ -0,0 +1,235 @@ |
1 | +package com.takensoft.common.oauth.web; | |
2 | + | |
3 | +import com.takensoft.cms.mber.service.MberService; | |
4 | +import com.takensoft.cms.mber.vo.MberVO; | |
5 | +import com.takensoft.common.message.MessageCode; | |
6 | +import com.takensoft.common.service.VerificationService; | |
7 | +import com.takensoft.common.util.HttpRequestUtil; | |
8 | +import com.takensoft.common.util.ResponseUtil; | |
9 | +import jakarta.servlet.http.HttpServletRequest; | |
10 | +import jakarta.servlet.http.HttpServletResponse; | |
11 | +import jakarta.servlet.http.HttpSession; | |
12 | +import lombok.RequiredArgsConstructor; | |
13 | +import lombok.extern.slf4j.Slf4j; | |
14 | +import org.springframework.beans.factory.annotation.Value; | |
15 | +import org.springframework.http.ResponseEntity; | |
16 | +import org.springframework.web.bind.annotation.GetMapping; | |
17 | +import org.springframework.web.bind.annotation.RequestMapping; | |
18 | +import org.springframework.web.bind.annotation.RequestParam; | |
19 | +import org.springframework.web.bind.annotation.RestController; | |
20 | + | |
21 | +import java.io.IOException; | |
22 | +import java.util.Arrays; | |
23 | +import java.util.HashMap; | |
24 | +import java.util.List; | |
25 | +import java.util.Map; | |
26 | + | |
27 | +/** | |
28 | + * @author takensoft | |
29 | + * @since 2025.05.22 | |
30 | + * @modification | |
31 | + * since | author | description | |
32 | + * 2025.05.22 | takensoft | 최초 등록 | |
33 | + * 2025.05.26 | takensoft | OAuth 리다이렉트 기능 추가 | |
34 | + * | |
35 | + * OAuth2 관련 통합 컨트롤러 | |
36 | + */ | |
37 | +@RestController | |
38 | +@RequiredArgsConstructor | |
39 | +@Slf4j | |
40 | +@RequestMapping(value = "/oauth2") | |
41 | +public class OAuth2Controller { | |
42 | + | |
43 | + private final MberService mberService; | |
44 | + private final VerificationService verificationService; | |
45 | + private final ResponseUtil resUtil; | |
46 | + private final HttpRequestUtil httpRequestUtil; | |
47 | + | |
48 | + @Value("${front.url}") | |
49 | + private String FRONT_URL; | |
50 | + | |
51 | + // 지원하는 OAuth 제공자 목록 | |
52 | + private static final List<String> SUPPORTED_PROVIDERS = Arrays.asList("kakao", "naver", "google"); | |
53 | + | |
54 | + /** | |
55 | + * OAuth 로그인 리다이렉트 처리 | |
56 | + * 프론트엔드에서 provider 정보를 받아 검증 후 OAuth2 서버로 리다이렉트 | |
57 | + */ | |
58 | + @GetMapping("/login") | |
59 | + public void redirectToOAuth(@RequestParam String provider, | |
60 | + HttpServletRequest request, | |
61 | + HttpServletResponse response) throws IOException { | |
62 | + | |
63 | + String clientIP = httpRequestUtil.getIp(request); | |
64 | + String userAgent = httpRequestUtil.getUserAgent(request); | |
65 | + | |
66 | + log.info("=== OAuth 로그인 시도 ==="); | |
67 | + log.info("Provider: {}", provider); | |
68 | + log.info("Client IP: {}", clientIP); | |
69 | + log.info("User Agent: {}", userAgent); | |
70 | + | |
71 | + try { | |
72 | + // 1. Provider 유효성 검증 | |
73 | + validateProvider(provider); | |
74 | + | |
75 | + // 2. 보안 검증 (필요시 추가) | |
76 | + validateSecurity(request); | |
77 | + | |
78 | + // 3. CORS 헤더 설정 (브라우저 보안 문제 해결) | |
79 | + response.setHeader("Access-Control-Allow-Origin", FRONT_URL); | |
80 | + response.setHeader("Access-Control-Allow-Credentials", "true"); | |
81 | + response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); | |
82 | + response.setHeader("Access-Control-Allow-Headers", "*"); | |
83 | + | |
84 | + // 4. OAuth2 Authorization Server로 리다이렉트 | |
85 | + String redirectUrl = "/oauth2/authorization/" + provider; | |
86 | + log.info("OAuth 리다이렉트 URL: {}", redirectUrl); | |
87 | + | |
88 | + // 리다이렉트 상태 코드를 명시적으로 설정 | |
89 | + response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); | |
90 | + response.sendRedirect(redirectUrl); | |
91 | + | |
92 | + } catch (IllegalArgumentException e) { | |
93 | + log.error("OAuth 로그인 실패 - 잘못된 Provider: {}", provider); | |
94 | + handleError(response, "invalid_provider", e.getMessage()); | |
95 | + } catch (SecurityException e) { | |
96 | + log.error("OAuth 로그인 실패 - 보안 검증 실패: {}", e.getMessage()); | |
97 | + handleError(response, "security_check_failed", e.getMessage()); | |
98 | + } catch (Exception e) { | |
99 | + log.error("OAuth 로그인 실패 - 시스템 오류", e); | |
100 | + handleError(response, "system_error", "OAuth 로그인 중 오류가 발생했습니다."); | |
101 | + } | |
102 | + } | |
103 | + | |
104 | + /** | |
105 | + * OAuth2 로그인 후 사용자 정보 조회 | |
106 | + */ | |
107 | + @GetMapping(value = "/user-info") | |
108 | + public ResponseEntity<?> getUserInfo(HttpServletRequest request) { | |
109 | + try { | |
110 | + // 현재 로그인된 사용자 ID 추출 | |
111 | + String currentUserId = verificationService.getCurrentUserId(); | |
112 | + | |
113 | + if (currentUserId == null || currentUserId.isEmpty()) { | |
114 | + // 세션에서 OAuth2 정보 확인 (세션 모드인 경우) | |
115 | + HttpSession session = request.getSession(false); | |
116 | + if (session != null && session.getAttribute("oauth2User") != null) { | |
117 | + return handleSessionOAuth2User(session); | |
118 | + } | |
119 | + | |
120 | + return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); | |
121 | + } | |
122 | + | |
123 | + // DB에서 사용자 정보 조회 | |
124 | + HashMap<String, Object> params = new HashMap<>(); | |
125 | + params.put("mbrId", currentUserId); | |
126 | + MberVO userInfo = mberService.findByMbr(params); | |
127 | + | |
128 | + if (userInfo == null) { | |
129 | + return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); | |
130 | + } | |
131 | + | |
132 | + // 응답 데이터 구성 | |
133 | + HashMap<String, Object> response = createUserResponse(userInfo); | |
134 | + return resUtil.successRes(response, MessageCode.COMMON_SUCCESS); | |
135 | + | |
136 | + } catch (Exception e) { | |
137 | + log.error("사용자 정보 조회 실패", e); | |
138 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
139 | + } | |
140 | + } | |
141 | + | |
142 | + /** | |
143 | + * 지원하는 OAuth 제공자 목록 조회 API | |
144 | + */ | |
145 | + @GetMapping("/providers") | |
146 | + public ResponseEntity<?> getSupportedProviders() { | |
147 | + log.info("지원하는 OAuth 제공자 목록 조회"); | |
148 | + return resUtil.successRes(SUPPORTED_PROVIDERS, MessageCode.COMMON_SUCCESS); | |
149 | + } | |
150 | + | |
151 | + /** | |
152 | + * OAuth 제공자 유효성 검증 | |
153 | + */ | |
154 | + private void validateProvider(String provider) { | |
155 | + if (provider == null || provider.trim().isEmpty()) { | |
156 | + throw new IllegalArgumentException("OAuth 제공자가 지정되지 않았습니다."); | |
157 | + } | |
158 | + | |
159 | + if (!SUPPORTED_PROVIDERS.contains(provider.toLowerCase())) { | |
160 | + throw new IllegalArgumentException("지원하지 않는 OAuth 제공자입니다: " + provider); | |
161 | + } | |
162 | + | |
163 | + // TODO: 동적 설정으로 특정 제공자 비활성화 체크 | |
164 | + // if (!oauthConfigService.isProviderEnabled(provider)) { | |
165 | + // throw new IllegalArgumentException(provider + " 로그인이 일시 중단되었습니다."); | |
166 | + // } | |
167 | + } | |
168 | + | |
169 | + /** | |
170 | + * 보안 검증 (필요시 확장) | |
171 | + */ | |
172 | + private void validateSecurity(HttpServletRequest request) { | |
173 | + // TODO: 필요시 보안 검증 로직 추가 | |
174 | + // 예: IP 화이트리스트, Rate Limiting, 사용자 상태 체크 등 | |
175 | + | |
176 | + String clientIP = httpRequestUtil.getIp(request); | |
177 | + | |
178 | + // 예시: 로컬 개발 환경이 아닌 경우 추가 검증 | |
179 | + if (!"127.0.0.1".equals(clientIP) && !"::1".equals(clientIP)) { | |
180 | + // 운영 환경 보안 검증 로직 | |
181 | + } | |
182 | + } | |
183 | + | |
184 | + /** | |
185 | + * 에러 발생 시 프론트엔드로 리다이렉트 | |
186 | + */ | |
187 | + private void handleError(HttpServletResponse response, String errorCode, String errorMessage) throws IOException { | |
188 | + String encodedMessage = java.net.URLEncoder.encode(errorMessage, "UTF-8"); | |
189 | + String errorUrl = String.format("%s/login?error=%s&message=%s", FRONT_URL, errorCode, encodedMessage); | |
190 | + | |
191 | + log.info("에러 리다이렉트 URL: {}", errorUrl); | |
192 | + response.sendRedirect(errorUrl); | |
193 | + } | |
194 | + | |
195 | + /** | |
196 | + * 세션의 OAuth2 사용자 정보 처리 | |
197 | + */ | |
198 | + private ResponseEntity<?> handleSessionOAuth2User(HttpSession session) { | |
199 | + try { | |
200 | + // 세션에서 OAuth2 사용자 정보 추출 | |
201 | + // 이는 DB 저장이 완료되기 전의 임시 상태 | |
202 | + HashMap<String, Object> tempResponse = new HashMap<>(); | |
203 | + tempResponse.put("mbrId", "TEMP_OAUTH2"); | |
204 | + tempResponse.put("mbrNm", "OAuth2 User"); | |
205 | + tempResponse.put("roles", new String[]{"ROLE_USER"}); | |
206 | + tempResponse.put("isTemporary", true); | |
207 | + | |
208 | + return resUtil.successRes(tempResponse, MessageCode.COMMON_SUCCESS); | |
209 | + } catch (Exception e) { | |
210 | + log.error("세션 OAuth2 사용자 정보 처리 실패", e); | |
211 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
212 | + } | |
213 | + } | |
214 | + | |
215 | + /** | |
216 | + * 사용자 응답 데이터 생성 | |
217 | + */ | |
218 | + private HashMap<String, Object> createUserResponse(MberVO userInfo) { | |
219 | + HashMap<String, Object> response = new HashMap<>(); | |
220 | + response.put("mbrId", userInfo.getMbrId()); | |
221 | + response.put("mbrNm", userInfo.getMbrNm()); | |
222 | + response.put("eml", userInfo.getEml()); | |
223 | + response.put("ncnm", userInfo.getNcnm()); | |
224 | + response.put("mbrType", userInfo.getMbrType()); | |
225 | + | |
226 | + // 권한 정보 변환 | |
227 | + String[] roles = userInfo.getAuthorList().stream() | |
228 | + .map(auth -> auth.getAuthrtCd()) | |
229 | + .toArray(String[]::new); | |
230 | + response.put("roles", roles); | |
231 | + response.put("isTemporary", false); | |
232 | + | |
233 | + return response; | |
234 | + } | |
235 | +}(파일 끝에 줄바꿈 문자 없음) |
--- src/main/resources/application.yml
+++ src/main/resources/application.yml
... | ... | @@ -65,6 +65,55 @@ |
65 | 65 |
verifyTime: 600000 # 인증가능 시간: 10분 |
66 | 66 |
storeTime: 86400000 # 보관 시간: 24시간 |
67 | 67 |
|
68 |
+ # OAuth2 설정 추가 |
|
69 |
+ security: |
|
70 |
+ oauth2: |
|
71 |
+ client: |
|
72 |
+ registration: |
|
73 |
+ # 카카오 OAuth2 설정 |
|
74 |
+ kakao: |
|
75 |
+ client-id: ${KAKAO_CLIENT_ID:bbaf1cd4db69d4b4a9be5b6917361ac3} |
|
76 |
+ client-secret: ${KAKAO_CLIENT_SECRET:AvQ4lQ7HajbNa3AhuzOb979l5cMQDlEJ} |
|
77 |
+ redirect-uri: ${KAKAO_REDIRECT_URI:http://localhost:9090/login/oauth2/code/kakao} |
|
78 |
+ authorization-grant-type: authorization_code |
|
79 |
+ scope: profile_nickname,profile_image,account_email |
|
80 |
+ client-name: Kakao |
|
81 |
+ client-authentication-method: client_secret_post |
|
82 |
+ |
|
83 |
+ # 네이버 OAuth2 설정 |
|
84 |
+ naver: |
|
85 |
+ client-id: ${NAVER_CLIENT_ID:Q7HdlZZhdCNjazYZtEwp} |
|
86 |
+ client-secret: ${NAVER_CLIENT_SECRET:HkHD7Zedss} |
|
87 |
+ redirect-uri: ${NAVER_REDIRECT_URI:http://localhost:9090/login/oauth2/code/naver} |
|
88 |
+ authorization-grant-type: authorization_code |
|
89 |
+ scope: name,email,profile_image |
|
90 |
+ client-name: Naver |
|
91 |
+ |
|
92 |
+ # 구글 OAuth2 설정 |
|
93 |
+ google: |
|
94 |
+ client-id: ${GOOGLE_CLIENT_ID:AIzaSyB0FcOqHUlubnQzozH0G4fENpoq1pq3BxQ} |
|
95 |
+ client-secret: ${GOOGLE_CLIENT_SECRET:your_google_client_secret_here} |
|
96 |
+ redirect-uri: ${GOOGLE_REDIRECT_URI:http://localhost:9090/login/oauth2/code/google} |
|
97 |
+ authorization-grant-type: authorization_code |
|
98 |
+ scope: profile,email |
|
99 |
+ client-name: Google |
|
100 |
+ |
|
101 |
+ provider: |
|
102 |
+ # 카카오 제공업체 설정 |
|
103 |
+ kakao: |
|
104 |
+ authorization-uri: https://kauth.kakao.com/oauth/authorize |
|
105 |
+ token-uri: https://kauth.kakao.com/oauth/token |
|
106 |
+ user-info-uri: https://kapi.kakao.com/v2/user/me |
|
107 |
+ user-name-attribute: id |
|
108 |
+ |
|
109 |
+ # 네이버 제공업체 설정 |
|
110 |
+ naver: |
|
111 |
+ authorization-uri: https://nid.naver.com/oauth2.0/authorize |
|
112 |
+ token-uri: https://nid.naver.com/oauth2.0/token |
|
113 |
+ user-info-uri: https://openapi.naver.com/v1/nid/me |
|
114 |
+ user-name-attribute: response |
|
115 |
+ |
|
116 |
+ |
|
68 | 117 |
# Mybatis settings |
69 | 118 |
#mybatis: |
70 | 119 |
# type-aliases-package: com.takensoft.**.**.vo, com.takensoft.**.**.dto, com.takensoft.common |
--- src/main/resources/mybatis/mapper/mber/mber-SQL.xml
+++ src/main/resources/mybatis/mapper/mber/mber-SQL.xml
... | ... | @@ -214,4 +214,115 @@ |
214 | 214 |
FROM mbr_authrt_info mai |
215 | 215 |
WHERE mai.mbr_id = #{mbrId} |
216 | 216 |
</select> |
217 |
+ |
|
218 |
+ |
|
219 |
+ <!-- 이메일로만 사용자 조회 --> |
|
220 |
+ <select id="findByEmail" parameterType="String" resultType="MberVO"> |
|
221 |
+ SELECT |
|
222 |
+ m.mbr_id as mbrId, |
|
223 |
+ m.lgn_id as lgnId, |
|
224 |
+ m.mbr_nm as mbrNm, |
|
225 |
+ m.ncnm, |
|
226 |
+ m.pswd, |
|
227 |
+ m.mbl_telno as mblTelno, |
|
228 |
+ m.telno, |
|
229 |
+ m.eml, |
|
230 |
+ m.zip, |
|
231 |
+ m.addr, |
|
232 |
+ m.daddr, |
|
233 |
+ m.mbr_stts as mbrStts, |
|
234 |
+ m.use_yn as useYn, |
|
235 |
+ m.cntrl_dt as cntrlDt, |
|
236 |
+ m.cntrl_rsn as cntrlRsn, |
|
237 |
+ m.sms_rcptn_agre_yn as smsRcptnAgreYn, |
|
238 |
+ m.eml_rcptn_agre_yn as emlRcptnAgreYn, |
|
239 |
+ m.prvc_rls_yn as prvcRlsYn, |
|
240 |
+ m.mbr_type as mbrType, |
|
241 |
+ m.pswd_chg_dt as pswdChgDt, |
|
242 |
+ m.frst_reg_ip as frstRegIp, |
|
243 |
+ m.sys_pvsn_yn as sysPvsnYn, |
|
244 |
+ m.rgtr, |
|
245 |
+ m.reg_dt as regDt, |
|
246 |
+ m.mdfr, |
|
247 |
+ m.mdfcn_dt as mdfcnDt |
|
248 |
+ FROM mbr_info m |
|
249 |
+ WHERE m.eml = #{email} |
|
250 |
+ AND m.use_yn = 'Y' |
|
251 |
+ AND m.mbr_stts = '1' |
|
252 |
+ </select> |
|
253 |
+ |
|
254 |
+ <!-- 이메일과 회원 유형으로 사용자 조회 --> |
|
255 |
+ <select id="findByEmailAndProvider" parameterType="map" resultMap="mberMap"> |
|
256 |
+ <include refid="selectMber" /> |
|
257 |
+ WHERE mi.eml = #{email} |
|
258 |
+ AND mi.mbr_type = #{mbrType} |
|
259 |
+ AND mi.use_yn = 'Y' |
|
260 |
+ AND mi.mbr_stts = '1' |
|
261 |
+ </select> |
|
262 |
+ |
|
263 |
+ <!-- 회원 ID로 권한 목록 조회 --> |
|
264 |
+ <select id="findAuthoritiesByMbrId" parameterType="String" resultMap="authMap"> |
|
265 |
+ SELECT mai.mbr_id |
|
266 |
+ , mai.authrt_cd |
|
267 |
+ , mai.rgtr |
|
268 |
+ , TO_CHAR(mai.reg_dt, 'YYYY-MM-DD HH24:MI') AS reg_dt |
|
269 |
+ FROM mbr_authrt_info mai |
|
270 |
+ WHERE mai.mbr_id = #{mbrId} |
|
271 |
+ </select> |
|
272 |
+ |
|
273 |
+ <!-- OAuth2 사용자 저장 --> |
|
274 |
+ <insert id="saveOAuthUser" parameterType="MberVO"> |
|
275 |
+ INSERT INTO mbr_info ( |
|
276 |
+ mbr_id, |
|
277 |
+ lgn_id, |
|
278 |
+ mbr_nm, |
|
279 |
+ ncnm, |
|
280 |
+ eml, |
|
281 |
+ mbr_stts, |
|
282 |
+ use_yn, |
|
283 |
+ sms_rcptn_agre_yn, |
|
284 |
+ eml_rcptn_agre_yn, |
|
285 |
+ prvc_rls_yn, |
|
286 |
+ mbr_type, |
|
287 |
+ sys_pvsn_yn, |
|
288 |
+ rgtr, |
|
289 |
+ reg_dt |
|
290 |
+ ) VALUES ( |
|
291 |
+ #{mbrId}, |
|
292 |
+ #{lgnId}, |
|
293 |
+ #{mbrNm}, |
|
294 |
+ #{ncnm}, |
|
295 |
+ #{eml}, |
|
296 |
+ #{mbrStts}, |
|
297 |
+ #{useYn}, |
|
298 |
+ 'N', |
|
299 |
+ 'N', |
|
300 |
+ 'N', |
|
301 |
+ #{mbrType}, |
|
302 |
+ #{sysPvsnYn}, |
|
303 |
+ #{rgtr}, |
|
304 |
+ NOW() |
|
305 |
+ ) |
|
306 |
+ </insert> |
|
307 |
+ |
|
308 |
+ <!-- OAuth2 사용자 정보 업데이트 --> |
|
309 |
+ <update id="updateOAuthUser" parameterType="MberVO"> |
|
310 |
+ UPDATE mbr_info |
|
311 |
+ SET |
|
312 |
+ mbr_nm = #{mbrNm}, |
|
313 |
+ ncnm = #{ncnm}, |
|
314 |
+ mdfr = #{mdfr}, |
|
315 |
+ mdfcn_dt = NOW() |
|
316 |
+ WHERE mbr_id = #{mbrId} |
|
317 |
+ </update> |
|
318 |
+ |
|
319 |
+ <!-- 기존 계정에 OAuth2 정보 연동 --> |
|
320 |
+ <update id="linkOAuth2Account" parameterType="MberVO"> |
|
321 |
+ UPDATE mbr_info |
|
322 |
+ SET |
|
323 |
+ mbr_type = #{mbrType}, |
|
324 |
+ mdfr = #{mdfr}, |
|
325 |
+ mdfcn_dt = NOW() |
|
326 |
+ WHERE mbr_id = #{mbrId} |
|
327 |
+ </update> |
|
217 | 328 |
</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?