

250523 김혜민 oauth2 적용중
@c799e2456dedb220383007a572da4d20c2174141
--- 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 provider - OAuth2 제공자 |
|
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
... | ... | @@ -24,6 +24,8 @@ |
24 | 24 |
import jakarta.servlet.http.HttpServletRequest; |
25 | 25 |
|
26 | 26 |
import java.util.HashMap; |
27 |
+import java.util.List; |
|
28 |
+ |
|
27 | 29 |
/** |
28 | 30 |
* @author takensoft |
29 | 31 |
* @since 2024.04.01 |
... | ... | @@ -265,4 +267,165 @@ |
265 | 267 |
throw e; |
266 | 268 |
} |
267 | 269 |
} |
270 |
+ |
|
271 |
+ |
|
272 |
+ |
|
273 |
+ // OAuth2 관련 메서드 구현 |
|
274 |
+ |
|
275 |
+ /** |
|
276 |
+ * @param email - 이메일 |
|
277 |
+ * @return MberVO - 사용자 정보 |
|
278 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
279 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
280 |
+ * |
|
281 |
+ * 이메일로만 사용자 조회 (provider 무관) |
|
282 |
+ */ |
|
283 |
+ @Override |
|
284 |
+ @Transactional(readOnly = true) |
|
285 |
+ public MberVO findByEmail(String email) { |
|
286 |
+ try { |
|
287 |
+ return mberDAO.findByEmail(email); |
|
288 |
+ } catch (DataAccessException dae) { |
|
289 |
+ throw dae; |
|
290 |
+ } catch (Exception e) { |
|
291 |
+ throw e; |
|
292 |
+ } |
|
293 |
+ } |
|
294 |
+ |
|
295 |
+ /** |
|
296 |
+ * @param email - 이메일 |
|
297 |
+ * @param mbrType - 회원 유형 (K, N, G, F, S) |
|
298 |
+ * @return MberVO - 사용자 정보 |
|
299 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
300 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
301 |
+ * |
|
302 |
+ * 이메일과 회원 유형으로 사용자 조회 |
|
303 |
+ */ |
|
304 |
+ @Override |
|
305 |
+ @Transactional(readOnly = true) |
|
306 |
+ public MberVO findByEmailAndProvider(String email, String mbrType) { |
|
307 |
+ try { |
|
308 |
+ MberVO user = mberDAO.findByEmailAndProvider(email, mbrType); // 파라미터 직접 전달 |
|
309 |
+ |
|
310 |
+ // 권한 정보도 함께 조회 |
|
311 |
+ if (user != null) { |
|
312 |
+ List<MberAuthorVO> authorities = mberDAO.findAuthoritiesByMbrId(user.getMbrId()); |
|
313 |
+ user.setAuthorList(authorities); |
|
314 |
+ } |
|
315 |
+ return user; |
|
316 |
+ } catch (DataAccessException dae) { |
|
317 |
+ throw dae; |
|
318 |
+ } catch (Exception e) { |
|
319 |
+ throw e; |
|
320 |
+ } |
|
321 |
+ } |
|
322 |
+ |
|
323 |
+ /** |
|
324 |
+ * @param user - OAuth2 사용자 정보 |
|
325 |
+ * @return MberVO - 저장된 사용자 정보 |
|
326 |
+ * @throws CustomInsertFailException - 사용자 저장 실패 예외 발생 시 |
|
327 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
328 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
329 |
+ * |
|
330 |
+ * OAuth2 사용자 저장 |
|
331 |
+ */ |
|
332 |
+ @Override |
|
333 |
+ @Transactional(rollbackFor = Exception.class) |
|
334 |
+ public MberVO saveOAuthUser(MberVO user) { |
|
335 |
+ try { |
|
336 |
+ // 회원 아이디 생성 |
|
337 |
+ String mbrId = mberIdgn.getNextStringId(); |
|
338 |
+ user.setMbrId(mbrId); |
|
339 |
+ |
|
340 |
+ // 기본값 설정 |
|
341 |
+ user.setLgnId(user.getEml().toLowerCase()); |
|
342 |
+ user.setMbrStts("1"); // 승인 상태 |
|
343 |
+ user.setUseYn(true); |
|
344 |
+ user.setSmsRcptnAgreYn("N"); |
|
345 |
+ user.setEmlRcptnAgreYn("N"); |
|
346 |
+ user.setPrvcRlsYn("N"); |
|
347 |
+ user.setSysPvsnYn("1"); // 사용자 데이터 |
|
348 |
+ user.setRgtr("OAUTH2_SYSTEM"); |
|
349 |
+ |
|
350 |
+ // OAuth2 사용자 저장 |
|
351 |
+ int result = mberDAO.saveOAuthUser(user); |
|
352 |
+ if(result == 0) { |
|
353 |
+ throw new CustomInsertFailException("OAuth2 사용자 저장에 실패했습니다."); |
|
354 |
+ } |
|
355 |
+ |
|
356 |
+ // 권한 저장 |
|
357 |
+ if(user.getAuthorList() != null && !user.getAuthorList().isEmpty()) { |
|
358 |
+ for(MberAuthorVO authority : user.getAuthorList()) { |
|
359 |
+ authority.setMbrId(mbrId); |
|
360 |
+ authority.setRgtr("OAUTH2_SYSTEM"); |
|
361 |
+ int authorResult = mberDAO.authorSave(authority); |
|
362 |
+ if(authorResult == 0) { |
|
363 |
+ throw new CustomInsertFailException("OAuth2 사용자 권한 저장에 실패했습니다."); |
|
364 |
+ } |
|
365 |
+ } |
|
366 |
+ } |
|
367 |
+ |
|
368 |
+ return user; |
|
369 |
+ } catch (DataAccessException dae) { |
|
370 |
+ throw dae; |
|
371 |
+ } catch (Exception e) { |
|
372 |
+ throw e; |
|
373 |
+ } |
|
374 |
+ } |
|
375 |
+ |
|
376 |
+ /** |
|
377 |
+ * @param user - OAuth2 사용자 정보 |
|
378 |
+ * @return MberVO - 업데이트된 사용자 정보 |
|
379 |
+ * @throws CustomUpdateFailException - 사용자 업데이트 실패 예외 발생 시 |
|
380 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
381 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
382 |
+ * |
|
383 |
+ * OAuth2 사용자 정보 업데이트 |
|
384 |
+ */ |
|
385 |
+ @Override |
|
386 |
+ @Transactional(rollbackFor = Exception.class) |
|
387 |
+ public MberVO updateOAuthUser(MberVO user) { |
|
388 |
+ try { |
|
389 |
+ user.setMdfr("OAUTH2_SYSTEM"); |
|
390 |
+ |
|
391 |
+ int result = mberDAO.updateOAuthUser(user); |
|
392 |
+ if(result == 0) { |
|
393 |
+ throw new CustomUpdateFailException("OAuth2 사용자 정보 업데이트에 실패했습니다."); |
|
394 |
+ } |
|
395 |
+ |
|
396 |
+ return user; |
|
397 |
+ } catch (DataAccessException dae) { |
|
398 |
+ throw dae; |
|
399 |
+ } catch (Exception e) { |
|
400 |
+ throw e; |
|
401 |
+ } |
|
402 |
+ } |
|
403 |
+ |
|
404 |
+ /** |
|
405 |
+ * @param user - OAuth2 연동 정보가 추가된 사용자 정보 |
|
406 |
+ * @return MberVO - 연동된 사용자 정보 |
|
407 |
+ * @throws CustomUpdateFailException - 계정 연동 실패 예외 발생 시 |
|
408 |
+ * @throws DataAccessException - db 관련 예외 발생 시 |
|
409 |
+ * @throws Exception - 그 외 예외 발생 시 |
|
410 |
+ * |
|
411 |
+ * 기존 계정에 OAuth2 정보 연동 |
|
412 |
+ */ |
|
413 |
+ @Override |
|
414 |
+ @Transactional(rollbackFor = Exception.class) |
|
415 |
+ public MberVO linkOAuth2Account(MberVO user) { |
|
416 |
+ try { |
|
417 |
+ user.setMdfr("OAUTH2_LINK"); |
|
418 |
+ |
|
419 |
+ int result = mberDAO.linkOAuth2Account(user); |
|
420 |
+ if(result == 0) { |
|
421 |
+ throw new CustomUpdateFailException("OAuth2 계정 연동에 실패했습니다."); |
|
422 |
+ } |
|
423 |
+ |
|
424 |
+ return user; |
|
425 |
+ } catch (DataAccessException dae) { |
|
426 |
+ throw dae; |
|
427 |
+ } catch (Exception e) { |
|
428 |
+ throw e; |
|
429 |
+ } |
|
430 |
+ } |
|
268 | 431 |
}(파일 끝에 줄바꿈 문자 없음) |
--- 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
... | ... | @@ -12,6 +12,10 @@ |
12 | 12 |
import com.takensoft.common.exception.CustomAuthenticationEntryPoint; |
13 | 13 |
import com.takensoft.common.util.JWTUtil; |
14 | 14 |
import com.takensoft.common.util.SessionUtil; |
15 |
+import com.takensoft.common.oauth.service.Impl.CustomOAuth2UserServiceImpl; |
|
16 |
+import com.takensoft.common.oauth.handler.OAuth2AuthenticationSuccessHandler; |
|
17 |
+import com.takensoft.common.oauth.handler.OAuth2AuthenticationFailureHandler; |
|
18 |
+import org.springframework.beans.factory.annotation.Autowired; |
|
15 | 19 |
import org.springframework.beans.factory.annotation.Value; |
16 | 20 |
import org.springframework.context.annotation.Bean; |
17 | 21 |
import org.springframework.context.annotation.Configuration; |
... | ... | @@ -22,6 +26,7 @@ |
22 | 26 |
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
23 | 27 |
import org.springframework.security.config.http.SessionCreationPolicy; |
24 | 28 |
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
29 |
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; |
|
25 | 30 |
import org.springframework.security.web.SecurityFilterChain; |
26 | 31 |
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
27 | 32 |
import org.springframework.security.web.context.SecurityContextPersistenceFilter; |
... | ... | @@ -37,6 +42,7 @@ |
37 | 42 |
* @modification |
38 | 43 |
* since | author | description |
39 | 44 |
* 2025.01.22 | takensoft | 최초 등록 |
45 |
+ * 2025.05.22 | takensoft | OAuth2 로그인 추가 |
|
40 | 46 |
* |
41 | 47 |
* Spring Security 설정을 위한 Config |
42 | 48 |
*/ |
... | ... | @@ -59,6 +65,15 @@ |
59 | 65 |
private final LoginPolicyService loginPolicyService; |
60 | 66 |
private final SessionUtil sessionUtil; |
61 | 67 |
|
68 |
+ @Autowired |
|
69 |
+ private CustomOAuth2UserServiceImpl customOAuth2UserServiceImpl; |
|
70 |
+ |
|
71 |
+ @Autowired |
|
72 |
+ private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; |
|
73 |
+ |
|
74 |
+ @Autowired |
|
75 |
+ private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; |
|
76 |
+ |
|
62 | 77 |
private static String FRONT_URL; // 프론트 접근 허용 URL |
63 | 78 |
private static long JWT_ACCESSTIME; // access 토큰 유지 시간 |
64 | 79 |
private static long JWT_REFRESHTIME; // refresh 토큰 유지 시간 |
... | ... | @@ -67,16 +82,7 @@ |
67 | 82 |
private final RedisTemplate<String, String> redisTemplate; |
68 | 83 |
|
69 | 84 |
/** |
70 |
- * @param authenticationConfiguration - 인증 구성 객체 |
|
71 |
- * @param jwtUtil - JWT 유틸리티 객체 |
|
72 |
- * @param authenticationEntryPoint - 인증 실패 시 처리 엔트리 포인트 |
|
73 |
- * @param accessDenieHandler - 접근 거부 처리 핸들러 |
|
74 |
- * @param fUrl - 프론트엔드 URL (application.yml에서 값을 읽어 옴) |
|
75 |
- * @param aTime - JWT 접근 토큰 유효 시간 (application.yml에서 값을 읽어 옴) |
|
76 |
- * @param rTime - JWT 리프레시 토큰 유효 시간 (application.yml에서 값을 읽어 옴) |
|
77 |
- * @param ctime - 쿠키 유효 시간 (application.yml에서 값을 읽어 옴) |
|
78 |
- * @param redisTemplate |
|
79 |
-* |
|
85 |
+ * SecurityConfig 생성자 |
|
80 | 86 |
*/ |
81 | 87 |
public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, RefreshTokenService refreshTokenService, CntxtPthService cntxtPthService, AccesCtrlService accesCtrlService, AppConfig appConfig, |
82 | 88 |
LgnHstryService lgnHstryService, CustomAuthenticationEntryPoint authenticationEntryPoint, CustomAccessDenieHandler accessDenieHandler, HttpRequestUtil httpRequestUtil, |
... | ... | @@ -140,7 +146,7 @@ |
140 | 146 |
CorsConfiguration configuration = new CorsConfiguration(); |
141 | 147 |
configuration.setAllowedOrigins(Collections.singletonList(FRONT_URL)); // 허용할 프론트 포트 포함 경로 입력 |
142 | 148 |
configuration.setAllowedMethods(Collections.singletonList("*")); // 허용할 메소드(GET, POST, PUT 등) |
143 |
- configuration.setAllowedHeaders(Collections.singletonList("*")); // 허용할 헤드 |
|
149 |
+ configuration.setAllowedHeaders(Collections.singletonList("*")); // 허용할 헤더 |
|
144 | 150 |
configuration.setAllowCredentials(true); // 프론트에서 credentials 설정하면 true |
145 | 151 |
configuration.setMaxAge(3600L); // 허용을 물고 있을 시간 |
146 | 152 |
configuration.setExposedHeaders(Collections.singletonList("Authorization")); // 서버에서 JWT를 Authorization에 담아 보내기 위해 허용을 함 |
... | ... | @@ -148,6 +154,7 @@ |
148 | 154 |
} |
149 | 155 |
}) |
150 | 156 |
); |
157 |
+ |
|
151 | 158 |
// csrf disable |
152 | 159 |
http.csrf((auth) -> auth.disable()); |
153 | 160 |
// formLogin disable |
... | ... | @@ -163,10 +170,9 @@ |
163 | 170 |
); |
164 | 171 |
|
165 | 172 |
http.authorizeHttpRequests((auth) -> auth |
166 |
- .requestMatchers("/", "/mbr/**", "/refresh/**", "/sys/**", "/editFileUpload/**", "/fileUpload/**").permitAll() // 회원, 토큰, 시스템 제공, 파일 접근 모두 허용 |
|
173 |
+ .requestMatchers("/", "/mbr/**", "/refresh/**", "/sys/**", "/editFileUpload/**", "/fileUpload/**", "/oauth2/**", "/login/oauth2/**").permitAll() // 회원, 토큰, 시스템 제공, 파일, OAuth2 접근 모두 허용 |
|
167 | 174 |
.requestMatchers("/admin/**").hasRole("ADMIN") // 관리자 페이지는 ADMIN 권한을 가진 사용자만 접근 가능 |
168 | 175 |
.anyRequest().authenticated() // 그 외에는 로그인한 사용자만 접근 가능 |
169 |
-// .anyRequest().permitAll() // 모든 사용자 접근 가능 |
|
170 | 176 |
); |
171 | 177 |
|
172 | 178 |
// Context Path 검증 필터 |
... | ... | @@ -180,7 +186,16 @@ |
180 | 186 |
|
181 | 187 |
// 로그인 필터 |
182 | 188 |
http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshTokenService, lgnHstryService, httpRequestUtil, |
183 |
- loginModeService, loginPolicyService, sessionUtil, JWT_ACCESSTIME, JWT_REFRESHTIME, COOKIE_TIME, redisTemplate), UsernamePasswordAuthenticationFilter.class); |
|
189 |
+ loginModeService, loginPolicyService, sessionUtil, JWT_ACCESSTIME, JWT_REFRESHTIME, COOKIE_TIME, redisTemplate), UsernamePasswordAuthenticationFilter.class); |
|
190 |
+ |
|
191 |
+ // OAuth2 로그인 설정 |
|
192 |
+ http.oauth2Login(oauth2 -> oauth2 |
|
193 |
+ .userInfoEndpoint(userInfo -> userInfo |
|
194 |
+ .userService(customOAuth2UserServiceImpl) |
|
195 |
+ ) |
|
196 |
+ .successHandler(oAuth2AuthenticationSuccessHandler) |
|
197 |
+ .failureHandler(oAuth2AuthenticationFailureHandler) |
|
198 |
+ ); |
|
184 | 199 |
|
185 | 200 |
return http.build(); |
186 | 201 |
} |
+++ 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,166 @@ |
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, Authentication authentication) throws IOException{ | |
58 | + | |
59 | + CustomOAuth2UserVO oAuth2User = (CustomOAuth2UserVO) authentication.getPrincipal(); | |
60 | + | |
61 | + try { | |
62 | + log.info("OAuth2 로그인 성공 - Provider: {}, Email: {}", | |
63 | + oAuth2User.getProvider(), oAuth2User.getEmail()); | |
64 | + | |
65 | + // 1. 비동기로 사용자 정보 저장/업데이트 처리 | |
66 | + eventPublisher.publishEvent(new OAuth2UserSaveEvent(oAuth2User)); | |
67 | + | |
68 | + // 2. 로그인 이력 저장 | |
69 | + saveLoginHistory(request, oAuth2User); | |
70 | + | |
71 | + // 3. 로그인 모드에 따른 토큰/세션 처리 | |
72 | + String loginMode = loginModeService.getLoginMode(); | |
73 | + String tempUserId = createTempUserId(oAuth2User); | |
74 | + processLoginByMode(request, response, oAuth2User, tempUserId, loginMode); | |
75 | + | |
76 | + // 4. 프론트엔드 OAuth 콜백 페이지로 리다이렉트 | |
77 | + String redirectUrl = FRONT_URL + "/oauth/callback"; | |
78 | + getRedirectStrategy().sendRedirect(request, response, redirectUrl); | |
79 | + | |
80 | + } catch (Exception e) { | |
81 | + log.error("OAuth2 로그인 처리 중 오류 발생", e); | |
82 | + String errorUrl = FRONT_URL + "/login?error=oauth2_processing_failed"; | |
83 | + getRedirectStrategy().sendRedirect(request, response, errorUrl); | |
84 | + } | |
85 | + } | |
86 | + | |
87 | + /** | |
88 | + * 로그인 이력 저장 | |
89 | + */ | |
90 | + private void saveLoginHistory(HttpServletRequest request, CustomOAuth2UserVO oAuth2User) { | |
91 | + try { | |
92 | + LgnHstryVO lgnHstryVO = new LgnHstryVO(); | |
93 | + lgnHstryVO.setLgnId(oAuth2User.getEmail()); | |
94 | + lgnHstryVO.setLgnType("1"); // 일반 사용자 | |
95 | + lgnHstryVO.setCntnIp(httpRequestUtil.getIp(request)); | |
96 | + lgnHstryVO.setCntnOperSys(httpRequestUtil.getOS(httpRequestUtil.getUserAgent(request))); | |
97 | + lgnHstryVO.setDeviceNm(httpRequestUtil.getDevice(httpRequestUtil.getUserAgent(request))); | |
98 | + lgnHstryVO.setBrwsrNm(httpRequestUtil.getBrowser(httpRequestUtil.getUserAgent(request))); | |
99 | + | |
100 | + lgnHstryService.LgnHstrySave(lgnHstryVO); | |
101 | + } catch (Exception e) { | |
102 | + log.error("OAuth2 로그인 이력 저장 실패", e); | |
103 | + } | |
104 | + } | |
105 | + | |
106 | + /** | |
107 | + * 임시 사용자 ID 생성 | |
108 | + */ | |
109 | + private String createTempUserId(CustomOAuth2UserVO oAuth2User) { | |
110 | + return oAuth2User.getProvider() + "_" + oAuth2User.getId(); | |
111 | + } | |
112 | + | |
113 | + /** | |
114 | + * 로그인 모드에 따른 처리 | |
115 | + */ | |
116 | + private void processLoginByMode(HttpServletRequest request, HttpServletResponse response, CustomOAuth2UserVO oAuth2User, String tempUserId, String loginMode){ | |
117 | + | |
118 | + // JWT 토큰 생성 | |
119 | + String accessToken = jwtUtil.createJwt( | |
120 | + "Authorization", | |
121 | + tempUserId, | |
122 | + oAuth2User.getEmail(), | |
123 | + oAuth2User.getName(), | |
124 | + null, // roles는 DB 저장 후 갱신 예정 | |
125 | + JWT_ACCESSTIME | |
126 | + ); | |
127 | + | |
128 | + if ("S".equals(loginMode)) { | |
129 | + // 세션 모드 처리 | |
130 | + processSessionMode(request, tempUserId, accessToken, oAuth2User); | |
131 | + } else { | |
132 | + // JWT 모드 처리 (기본값) | |
133 | + processJwtMode(response, tempUserId, accessToken); | |
134 | + } | |
135 | + | |
136 | + response.setHeader("login-type", loginMode); | |
137 | + } | |
138 | + | |
139 | + /** | |
140 | + * 세션 모드 처리 | |
141 | + */ | |
142 | + private void processSessionMode(HttpServletRequest request, String tempUserId,String accessToken, CustomOAuth2UserVO oAuth2User) { | |
143 | + HttpSession session = request.getSession(true); | |
144 | + session.setAttribute("JWT_TOKEN", accessToken); | |
145 | + session.setAttribute("oauth2User", oAuth2User); | |
146 | + | |
147 | + // 중복 로그인 정책 확인 | |
148 | + if (!loginPolicyService.getPolicy()) { | |
149 | + sessionUtil.registerSession(tempUserId, session); | |
150 | + } | |
151 | + } | |
152 | + | |
153 | + /** | |
154 | + * JWT 모드 처리 | |
155 | + */ | |
156 | + private void processJwtMode(HttpServletResponse response, String tempUserId, String accessToken) { | |
157 | + response.setHeader("Authorization", accessToken); | |
158 | + | |
159 | + // 중복 로그인 정책 확인 | |
160 | + if (!loginPolicyService.getPolicy()) { | |
161 | + redisTemplate.delete("jwt:" + tempUserId); | |
162 | + redisTemplate.opsForValue().set("jwt:" + tempUserId, accessToken, | |
163 | + JWT_ACCESSTIME, TimeUnit.MILLISECONDS); | |
164 | + } | |
165 | + } | |
166 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ 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,117 @@ |
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.ResponseUtil; | |
8 | +import jakarta.servlet.http.HttpServletRequest; | |
9 | +import jakarta.servlet.http.HttpSession; | |
10 | +import lombok.RequiredArgsConstructor; | |
11 | +import lombok.extern.slf4j.Slf4j; | |
12 | +import org.springframework.http.ResponseEntity; | |
13 | +import org.springframework.web.bind.annotation.GetMapping; | |
14 | +import org.springframework.web.bind.annotation.RequestMapping; | |
15 | +import org.springframework.web.bind.annotation.RestController; | |
16 | + | |
17 | +import java.util.HashMap; | |
18 | +import java.util.Map; | |
19 | + | |
20 | +/** | |
21 | + * @author takensoft | |
22 | + * @since 2025.05.22 | |
23 | + * @modification | |
24 | + * since | author | description | |
25 | + * 2025.05.22 | takensoft | 최초 등록 | |
26 | + * | |
27 | + * OAuth2 관련 컨트롤러 | |
28 | + */ | |
29 | +@RestController | |
30 | +@RequiredArgsConstructor | |
31 | +@Slf4j | |
32 | +@RequestMapping(value = "/oauth2") | |
33 | +public class OAuth2Controller { | |
34 | + | |
35 | + private final MberService mberService; | |
36 | + private final VerificationService verificationService; | |
37 | + private final ResponseUtil resUtil; | |
38 | + | |
39 | + /** | |
40 | + * OAuth2 로그인 후 사용자 정보 조회 | |
41 | + */ | |
42 | + @GetMapping(value = "/user-info") | |
43 | + public ResponseEntity<?> getUserInfo(HttpServletRequest request) { | |
44 | + try { | |
45 | + // 현재 로그인된 사용자 ID 추출 | |
46 | + String currentUserId = verificationService.getCurrentUserId(); | |
47 | + | |
48 | + if (currentUserId == null || currentUserId.isEmpty()) { | |
49 | + // 세션에서 OAuth2 정보 확인 (세션 모드인 경우) | |
50 | + HttpSession session = request.getSession(false); | |
51 | + if (session != null && session.getAttribute("oauth2User") != null) { | |
52 | + return handleSessionOAuth2User(session); | |
53 | + } | |
54 | + | |
55 | + return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); | |
56 | + } | |
57 | + | |
58 | + // DB에서 사용자 정보 조회 | |
59 | + HashMap<String, Object> params = new HashMap<>(); | |
60 | + params.put("mbrId", currentUserId); | |
61 | + MberVO userInfo = mberService.findByMbr(params); | |
62 | + | |
63 | + if (userInfo == null) { | |
64 | + return resUtil.errorRes(MessageCode.LOGIN_USER_NOT_FOUND); | |
65 | + } | |
66 | + | |
67 | + // 응답 데이터 구성 | |
68 | + Map<String, Object> response = createUserResponse(userInfo); | |
69 | + return resUtil.successRes(response, MessageCode.COMMON_SUCCESS); | |
70 | + | |
71 | + } catch (Exception e) { | |
72 | + log.error("사용자 정보 조회 실패", e); | |
73 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
74 | + } | |
75 | + } | |
76 | + | |
77 | + /** | |
78 | + * 세션의 OAuth2 사용자 정보 처리 | |
79 | + */ | |
80 | + private ResponseEntity<?> handleSessionOAuth2User(HttpSession session) { | |
81 | + try { | |
82 | + // 세션에서 OAuth2 사용자 정보 추출 | |
83 | + // 이는 DB 저장이 완료되기 전의 임시 상태 | |
84 | + Map<String, Object> tempResponse = new HashMap<>(); | |
85 | + tempResponse.put("mbrId", "TEMP_OAUTH2"); | |
86 | + tempResponse.put("mbrNm", "OAuth2 User"); | |
87 | + tempResponse.put("roles", new String[]{"ROLE_USER"}); | |
88 | + tempResponse.put("isTemporary", true); | |
89 | + | |
90 | + return resUtil.successRes(tempResponse, MessageCode.COMMON_SUCCESS); | |
91 | + } catch (Exception e) { | |
92 | + log.error("세션 OAuth2 사용자 정보 처리 실패", e); | |
93 | + return resUtil.errorRes(MessageCode.COMMON_UNKNOWN_ERROR); | |
94 | + } | |
95 | + } | |
96 | + | |
97 | + /** | |
98 | + * 사용자 응답 데이터 생성 | |
99 | + */ | |
100 | + private Map<String, Object> createUserResponse(MberVO userInfo) { | |
101 | + Map<String, Object> response = new HashMap<>(); | |
102 | + response.put("mbrId", userInfo.getMbrId()); | |
103 | + response.put("mbrNm", userInfo.getMbrNm()); | |
104 | + response.put("eml", userInfo.getEml()); | |
105 | + response.put("ncnm", userInfo.getNcnm()); | |
106 | + response.put("mbrType", userInfo.getMbrType()); | |
107 | + | |
108 | + // 권한 정보 변환 | |
109 | + String[] roles = userInfo.getAuthorList().stream() | |
110 | + .map(auth -> auth.getAuthrtCd()) | |
111 | + .toArray(String[]::new); | |
112 | + response.put("roles", roles); | |
113 | + response.put("isTemporary", false); | |
114 | + | |
115 | + return response; | |
116 | + } | |
117 | +}(파일 끝에 줄바꿈 문자 없음) |
--- 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,132 @@ |
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" resultType="MberVO"> |
|
256 |
+ SELECT |
|
257 |
+ m.mbr_id as mbrId, |
|
258 |
+ m.lgn_id as lgnId, |
|
259 |
+ m.mbr_nm as mbrNm, |
|
260 |
+ m.ncnm, |
|
261 |
+ m.pswd, |
|
262 |
+ m.mbl_telno as mblTelno, |
|
263 |
+ m.telno, |
|
264 |
+ m.eml, |
|
265 |
+ m.zip, |
|
266 |
+ m.addr, |
|
267 |
+ m.daddr, |
|
268 |
+ m.mbr_stts as mbrStts, |
|
269 |
+ m.use_yn as useYn, |
|
270 |
+ m.cntrl_dt as cntrlDt, |
|
271 |
+ m.cntrl_rsn as cntrlRsn, |
|
272 |
+ m.sms_rcptn_agre_yn as smsRcptnAgreYn, |
|
273 |
+ m.eml_rcptn_agre_yn as emlRcptnAgreYn, |
|
274 |
+ m.prvc_rls_yn as prvcRlsYn, |
|
275 |
+ m.mbr_type as mbrType, |
|
276 |
+ m.pswd_chg_dt as pswdChgDt, |
|
277 |
+ m.frst_reg_ip as frstRegIp, |
|
278 |
+ m.sys_pvsn_yn as sysPvsnYn, |
|
279 |
+ m.rgtr, |
|
280 |
+ m.reg_dt as regDt, |
|
281 |
+ m.mdfr, |
|
282 |
+ m.mdfcn_dt as mdfcnDt |
|
283 |
+ FROM mbr_info m |
|
284 |
+ WHERE m.eml = #{0} |
|
285 |
+ AND m.mbr_type = #{1} |
|
286 |
+ AND m.use_yn = 'Y' |
|
287 |
+ AND m.mbr_stts = '1' |
|
288 |
+ </select> |
|
289 |
+ |
|
290 |
+ <!-- OAuth2 사용자 저장 --> |
|
291 |
+ <insert id="saveOAuthUser" parameterType="MberVO"> |
|
292 |
+ INSERT INTO mbr_info ( |
|
293 |
+ mbr_id, |
|
294 |
+ lgn_id, |
|
295 |
+ mbr_nm, |
|
296 |
+ ncnm, |
|
297 |
+ eml, |
|
298 |
+ mbr_stts, |
|
299 |
+ use_yn, |
|
300 |
+ sms_rcptn_agre_yn, |
|
301 |
+ eml_rcptn_agre_yn, |
|
302 |
+ prvc_rls_yn, |
|
303 |
+ mbr_type, |
|
304 |
+ sys_pvsn_yn, |
|
305 |
+ rgtr, |
|
306 |
+ reg_dt |
|
307 |
+ ) VALUES ( |
|
308 |
+ #{mbrId}, |
|
309 |
+ #{lgnId}, |
|
310 |
+ #{mbrNm}, |
|
311 |
+ #{ncnm}, |
|
312 |
+ #{eml}, |
|
313 |
+ #{mbrStts}, |
|
314 |
+ #{useYn}, |
|
315 |
+ 'N', |
|
316 |
+ 'N', |
|
317 |
+ 'N', |
|
318 |
+ #{mbrType}, |
|
319 |
+ #{sysPvsnYn}, |
|
320 |
+ #{rgtr}, |
|
321 |
+ NOW() |
|
322 |
+ ) |
|
323 |
+ </insert> |
|
324 |
+ |
|
325 |
+ <!-- OAuth2 사용자 정보 업데이트 --> |
|
326 |
+ <update id="updateOAuthUser" parameterType="MberVO"> |
|
327 |
+ UPDATE mbr_info |
|
328 |
+ SET |
|
329 |
+ mbr_nm = #{mbrNm}, |
|
330 |
+ ncnm = #{ncnm}, |
|
331 |
+ mdfr = #{mdfr}, |
|
332 |
+ mdfcn_dt = NOW() |
|
333 |
+ WHERE mbr_id = #{mbrId} |
|
334 |
+ </update> |
|
335 |
+ |
|
336 |
+ <!-- 기존 계정에 OAuth2 정보 연동 --> |
|
337 |
+ <update id="linkOAuth2Account" parameterType="MberVO"> |
|
338 |
+ UPDATE mbr_info |
|
339 |
+ SET |
|
340 |
+ mbr_type = #{mbrType}, |
|
341 |
+ mdfr = #{mdfr}, |
|
342 |
+ mdfcn_dt = NOW() |
|
343 |
+ WHERE mbr_id = #{mbrId} |
|
344 |
+ </update> |
|
217 | 345 |
</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?