

250526 김혜민 oauth2 수정
@9e42556164f5d1da32dc50757f9feb48782155f5
--- src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
+++ src/main/java/com/takensoft/cms/mber/dao/MberDAO.java
... | ... | @@ -77,7 +77,7 @@ |
77 | 77 |
|
78 | 78 |
/** |
79 | 79 |
* @param email - 이메일 |
80 |
- * @param provider - OAuth2 제공자 |
|
80 |
+ * @param mbrType - OAuth2 회원 유형 (K, N, G, F, S) |
|
81 | 81 |
* @return MberVO - OAuth2 사용자 정보 |
82 | 82 |
* |
83 | 83 |
* 이메일과 제공자로 사용자 조회 |
--- src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
+++ src/main/java/com/takensoft/cms/mber/service/Impl/MberServiceImpl.java
... | ... | @@ -23,6 +23,7 @@ |
23 | 23 |
|
24 | 24 |
import jakarta.servlet.http.HttpServletRequest; |
25 | 25 |
|
26 |
+import java.util.ArrayList; |
|
26 | 27 |
import java.util.HashMap; |
27 | 28 |
import java.util.List; |
28 | 29 |
|
... | ... | @@ -59,9 +60,9 @@ |
59 | 60 |
@Transactional(readOnly = true) |
60 | 61 |
public UserDetails loadUserByUsername(String username){ |
61 | 62 |
try { |
62 |
- UserDetails userDetails = mberDAO.findByMberSecurity(username); |
|
63 |
+ UserDetails userDetails = mberDAO.findByMberSecurity(username); |
|
63 | 64 |
|
64 |
- return userDetails; |
|
65 |
+ return userDetails; |
|
65 | 66 |
} catch (UsernameNotFoundException Unfe) { |
66 | 67 |
throw Unfe; |
67 | 68 |
} catch (Exception e) { |
... | ... | @@ -109,15 +110,21 @@ |
109 | 110 |
@Transactional(rollbackFor = Exception.class) |
110 | 111 |
public HashMap<String, Object> userJoin(HttpServletRequest req, JoinDTO joinDTO){ |
111 | 112 |
try { |
112 |
- // 회원 아이디 생성 |
|
113 |
- String mbrId = mberIdgn.getNextStringId(); |
|
114 |
- joinDTO.setMbrId(mbrId); |
|
113 |
+ // 회원 아이디 생성 (이미 설정된 경우 건너뛰기) |
|
114 |
+ if (joinDTO.getMbrId() == null || joinDTO.getMbrId().isEmpty()) { |
|
115 |
+ String mbrId = mberIdgn.getNextStringId(); |
|
116 |
+ joinDTO.setMbrId(mbrId); |
|
117 |
+ } |
|
115 | 118 |
|
116 | 119 |
// 아이디 소문자 변환 |
117 |
- joinDTO.setLgnId(joinDTO.getLgnId().toLowerCase()); |
|
120 |
+ if (joinDTO.getLgnId() != null && !joinDTO.getLgnId().isEmpty()) { |
|
121 |
+ joinDTO.setLgnId(joinDTO.getLgnId().toLowerCase()); |
|
122 |
+ } |
|
118 | 123 |
|
119 |
- // 비밀번호 암호화 |
|
120 |
- joinDTO.setPswd(bCryptPasswordEncoder.encode(joinDTO.getPswd())); |
|
124 |
+ // 비밀번호 암호화 (OAuth2는 비밀번호 없음) |
|
125 |
+ if (joinDTO.getPswd() != null && !joinDTO.getPswd().isEmpty()) { |
|
126 |
+ joinDTO.setPswd(bCryptPasswordEncoder.encode(joinDTO.getPswd())); |
|
127 |
+ } |
|
121 | 128 |
|
122 | 129 |
// 연락처 암호화 |
123 | 130 |
if(joinDTO.getMblTelno() != null && !joinDTO.getMblTelno().equals("")) { |
... | ... | @@ -128,15 +135,21 @@ |
128 | 135 |
} |
129 | 136 |
|
130 | 137 |
// 아이피 조회 및 등록 |
131 |
- 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 |
+ } |
|
132 | 143 |
|
133 | 144 |
// 등록된 토큰에서 사용자 정보 조회 |
134 |
- String writer = verificationService.getCurrentUserId(); |
|
145 |
+ String writer = joinDTO.getRgtr(); |
|
135 | 146 |
if (writer == null || writer.isEmpty()) { |
136 |
- throw new CustomNotFoundException("사용자 정보 조회에 실패했습니다."); |
|
147 |
+ writer = verificationService.getCurrentUserId(); |
|
148 |
+ if (writer == null || writer.isEmpty()) { |
|
149 |
+ throw new CustomNotFoundException("사용자 정보 조회에 실패했습니다."); |
|
150 |
+ } |
|
151 |
+ joinDTO.setRgtr(writer); |
|
137 | 152 |
} |
138 |
- // 작성자 등록 |
|
139 |
- joinDTO.setRgtr(writer); |
|
140 | 153 |
|
141 | 154 |
// 회원정보 등록 |
142 | 155 |
HashMap<String, Object> result = new HashMap<>(); |
... | ... | @@ -145,7 +158,7 @@ |
145 | 158 |
throw new CustomInsertFailException("회원 정보 등록에 실패했습니다."); |
146 | 159 |
} |
147 | 160 |
|
148 |
- result.put("mbrId", mbrId); |
|
161 |
+ result.put("mbrId", joinDTO.getMbrId()); |
|
149 | 162 |
|
150 | 163 |
// 권한 등록 |
151 | 164 |
int authorResult = 0; |
... | ... | @@ -327,45 +340,23 @@ |
327 | 340 |
* @throws DataAccessException - db 관련 예외 발생 시 |
328 | 341 |
* @throws Exception - 그 외 예외 발생 시 |
329 | 342 |
* |
330 |
- * OAuth2 사용자 저장 |
|
343 |
+ * OAuth2 사용자 저장 - JoinDTO를 활용하여 기존 로직 재사용 |
|
331 | 344 |
*/ |
332 | 345 |
@Override |
333 | 346 |
@Transactional(rollbackFor = Exception.class) |
334 | 347 |
public MberVO saveOAuthUser(MberVO user) { |
335 | 348 |
try { |
336 |
- // 회원 아이디 생성 |
|
337 |
- String mbrId = mberIdgn.getNextStringId(); |
|
338 |
- user.setMbrId(mbrId); |
|
349 |
+ // OAuth2 사용자를 JoinDTO로 변환하여 기존 검증된 로직 활용 |
|
350 |
+ JoinDTO oauthJoinDTO = createOAuthJoinDTO(user); |
|
339 | 351 |
|
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"); |
|
352 |
+ // 기존 userJoin 메서드 활용 (검증된 로직) |
|
353 |
+ HashMap<String, Object> result = userJoin(null, oauthJoinDTO); |
|
349 | 354 |
|
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 |
- } |
|
355 |
+ // 생성된 회원 ID 설정 |
|
356 |
+ user.setMbrId(result.get("mbrId").toString()); |
|
367 | 357 |
|
368 | 358 |
return user; |
359 |
+ |
|
369 | 360 |
} catch (DataAccessException dae) { |
370 | 361 |
throw dae; |
371 | 362 |
} catch (Exception e) { |
... | ... | @@ -374,6 +365,46 @@ |
374 | 365 |
} |
375 | 366 |
|
376 | 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 |
+ /** |
|
377 | 408 |
* @param user - OAuth2 사용자 정보 |
378 | 409 |
* @return MberVO - 업데이트된 사용자 정보 |
379 | 410 |
* @throws CustomUpdateFailException - 사용자 업데이트 실패 예외 발생 시 |
--- src/main/java/com/takensoft/common/config/SecurityConfig.java
+++ src/main/java/com/takensoft/common/config/SecurityConfig.java
... | ... | @@ -170,9 +170,24 @@ |
170 | 170 |
); |
171 | 171 |
|
172 | 172 |
http.authorizeHttpRequests((auth) -> auth |
173 |
- .requestMatchers("/", "/mbr/**", "/refresh/**", "/sys/**", "/editFileUpload/**", "/fileUpload/**", "/oauth2/**", "/login/oauth2/**").permitAll() // 회원, 토큰, 시스템 제공, 파일, OAuth2 접근 모두 허용 |
|
173 |
+ .requestMatchers("/", "/mbr/**", "/refresh/**", "/sys/**", "/editFileUpload/**", "/fileUpload/**", "/oauth2/**", "/login/oauth2/**", "/.well-known/**").permitAll() // 회원, 토큰, 시스템 제공, 파일, OAuth2 접근 모두 허용 |
|
174 | 174 |
.requestMatchers("/admin/**").hasRole("ADMIN") // 관리자 페이지는 ADMIN 권한을 가진 사용자만 접근 가능 |
175 | 175 |
.anyRequest().authenticated() // 그 외에는 로그인한 사용자만 접근 가능 |
176 |
+ ); |
|
177 |
+ |
|
178 |
+ // OAuth2 로그인 설정 |
|
179 |
+ http.oauth2Login(oauth2 -> oauth2 |
|
180 |
+ .authorizationEndpoint(authorization -> authorization |
|
181 |
+ .baseUri("/oauth2/authorization") |
|
182 |
+ ) |
|
183 |
+ .redirectionEndpoint(redirection -> redirection |
|
184 |
+ .baseUri("/login/oauth2/code/*") |
|
185 |
+ ) |
|
186 |
+ .userInfoEndpoint(userInfo -> userInfo |
|
187 |
+ .userService(customOAuth2UserServiceImpl) |
|
188 |
+ ) |
|
189 |
+ .successHandler(oAuth2AuthenticationSuccessHandler) |
|
190 |
+ .failureHandler(oAuth2AuthenticationFailureHandler) |
|
176 | 191 |
); |
177 | 192 |
|
178 | 193 |
// Context Path 검증 필터 |
... | ... | @@ -188,14 +203,6 @@ |
188 | 203 |
http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshTokenService, lgnHstryService, httpRequestUtil, |
189 | 204 |
loginModeService, loginPolicyService, sessionUtil, JWT_ACCESSTIME, JWT_REFRESHTIME, COOKIE_TIME, redisTemplate), UsernamePasswordAuthenticationFilter.class); |
190 | 205 |
|
191 |
- // OAuth2 로그인 설정 |
|
192 |
- http.oauth2Login(oauth2 -> oauth2 |
|
193 |
- .userInfoEndpoint(userInfo -> userInfo |
|
194 |
- .userService(customOAuth2UserServiceImpl) |
|
195 |
- ) |
|
196 |
- .successHandler(oAuth2AuthenticationSuccessHandler) |
|
197 |
- .failureHandler(oAuth2AuthenticationFailureHandler) |
|
198 |
- ); |
|
199 | 206 |
|
200 | 207 |
return http.build(); |
201 | 208 |
} |
--- 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/OAuth2AuthenticationSuccessHandler.java
+++ src/main/java/com/takensoft/common/oauth/handler/OAuth2AuthenticationSuccessHandler.java
... | ... | @@ -54,7 +54,8 @@ |
54 | 54 |
private String FRONT_URL; |
55 | 55 |
|
56 | 56 |
@Override |
57 |
- public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{ |
|
57 |
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, |
|
58 |
+ Authentication authentication) throws IOException, ServletException { |
|
58 | 59 |
|
59 | 60 |
CustomOAuth2UserVO oAuth2User = (CustomOAuth2UserVO) authentication.getPrincipal(); |
60 | 61 |
|
... | ... | @@ -73,13 +74,19 @@ |
73 | 74 |
String tempUserId = createTempUserId(oAuth2User); |
74 | 75 |
processLoginByMode(request, response, oAuth2User, tempUserId, loginMode); |
75 | 76 |
|
76 |
- // 4. 프론트엔드 OAuth 콜백 페이지로 리다이렉트 |
|
77 |
- String redirectUrl = FRONT_URL + "/oauth/callback"; |
|
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; |
|
78 | 85 |
getRedirectStrategy().sendRedirect(request, response, redirectUrl); |
79 | 86 |
|
80 | 87 |
} catch (Exception e) { |
81 | 88 |
log.error("OAuth2 로그인 처리 중 오류 발생", e); |
82 |
- String errorUrl = FRONT_URL + "/login?error=oauth2_processing_failed"; |
|
89 |
+ String errorUrl = FRONT_URL + "/login.page?error=oauth2_processing_failed"; |
|
83 | 90 |
getRedirectStrategy().sendRedirect(request, response, errorUrl); |
84 | 91 |
} |
85 | 92 |
} |
... | ... | @@ -113,7 +120,9 @@ |
113 | 120 |
/** |
114 | 121 |
* 로그인 모드에 따른 처리 |
115 | 122 |
*/ |
116 |
- private void processLoginByMode(HttpServletRequest request, HttpServletResponse response, CustomOAuth2UserVO oAuth2User, String tempUserId, String loginMode){ |
|
123 |
+ private void processLoginByMode(HttpServletRequest request, HttpServletResponse response, |
|
124 |
+ CustomOAuth2UserVO oAuth2User, String tempUserId, String loginMode) |
|
125 |
+ throws IOException { |
|
117 | 126 |
|
118 | 127 |
// JWT 토큰 생성 |
119 | 128 |
String accessToken = jwtUtil.createJwt( |
... | ... | @@ -139,7 +148,8 @@ |
139 | 148 |
/** |
140 | 149 |
* 세션 모드 처리 |
141 | 150 |
*/ |
142 |
- private void processSessionMode(HttpServletRequest request, String tempUserId,String accessToken, CustomOAuth2UserVO oAuth2User) { |
|
151 |
+ private void processSessionMode(HttpServletRequest request, String tempUserId, |
|
152 |
+ String accessToken, CustomOAuth2UserVO oAuth2User) { |
|
143 | 153 |
HttpSession session = request.getSession(true); |
144 | 154 |
session.setAttribute("JWT_TOKEN", accessToken); |
145 | 155 |
session.setAttribute("oauth2User", oAuth2User); |
--- src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
+++ src/main/java/com/takensoft/common/oauth/web/OAuth2Controller.java
... | ... | @@ -4,17 +4,24 @@ |
4 | 4 |
import com.takensoft.cms.mber.vo.MberVO; |
5 | 5 |
import com.takensoft.common.message.MessageCode; |
6 | 6 |
import com.takensoft.common.service.VerificationService; |
7 |
+import com.takensoft.common.util.HttpRequestUtil; |
|
7 | 8 |
import com.takensoft.common.util.ResponseUtil; |
8 | 9 |
import jakarta.servlet.http.HttpServletRequest; |
10 |
+import jakarta.servlet.http.HttpServletResponse; |
|
9 | 11 |
import jakarta.servlet.http.HttpSession; |
10 | 12 |
import lombok.RequiredArgsConstructor; |
11 | 13 |
import lombok.extern.slf4j.Slf4j; |
14 |
+import org.springframework.beans.factory.annotation.Value; |
|
12 | 15 |
import org.springframework.http.ResponseEntity; |
13 | 16 |
import org.springframework.web.bind.annotation.GetMapping; |
14 | 17 |
import org.springframework.web.bind.annotation.RequestMapping; |
18 |
+import org.springframework.web.bind.annotation.RequestParam; |
|
15 | 19 |
import org.springframework.web.bind.annotation.RestController; |
16 | 20 |
|
21 |
+import java.io.IOException; |
|
22 |
+import java.util.Arrays; |
|
17 | 23 |
import java.util.HashMap; |
24 |
+import java.util.List; |
|
18 | 25 |
import java.util.Map; |
19 | 26 |
|
20 | 27 |
/** |
... | ... | @@ -23,8 +30,9 @@ |
23 | 30 |
* @modification |
24 | 31 |
* since | author | description |
25 | 32 |
* 2025.05.22 | takensoft | 최초 등록 |
33 |
+ * 2025.05.26 | takensoft | OAuth 리다이렉트 기능 추가 |
|
26 | 34 |
* |
27 |
- * OAuth2 관련 컨트롤러 |
|
35 |
+ * OAuth2 관련 통합 컨트롤러 |
|
28 | 36 |
*/ |
29 | 37 |
@RestController |
30 | 38 |
@RequiredArgsConstructor |
... | ... | @@ -35,6 +43,63 @@ |
35 | 43 |
private final MberService mberService; |
36 | 44 |
private final VerificationService verificationService; |
37 | 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 |
+ } |
|
38 | 103 |
|
39 | 104 |
/** |
40 | 105 |
* OAuth2 로그인 후 사용자 정보 조회 |
... | ... | @@ -65,7 +130,7 @@ |
65 | 130 |
} |
66 | 131 |
|
67 | 132 |
// 응답 데이터 구성 |
68 |
- Map<String, Object> response = createUserResponse(userInfo); |
|
133 |
+ HashMap<String, Object> response = createUserResponse(userInfo); |
|
69 | 134 |
return resUtil.successRes(response, MessageCode.COMMON_SUCCESS); |
70 | 135 |
|
71 | 136 |
} catch (Exception e) { |
... | ... | @@ -75,13 +140,66 @@ |
75 | 140 |
} |
76 | 141 |
|
77 | 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 |
+ /** |
|
78 | 196 |
* 세션의 OAuth2 사용자 정보 처리 |
79 | 197 |
*/ |
80 | 198 |
private ResponseEntity<?> handleSessionOAuth2User(HttpSession session) { |
81 | 199 |
try { |
82 | 200 |
// 세션에서 OAuth2 사용자 정보 추출 |
83 | 201 |
// 이는 DB 저장이 완료되기 전의 임시 상태 |
84 |
- Map<String, Object> tempResponse = new HashMap<>(); |
|
202 |
+ HashMap<String, Object> tempResponse = new HashMap<>(); |
|
85 | 203 |
tempResponse.put("mbrId", "TEMP_OAUTH2"); |
86 | 204 |
tempResponse.put("mbrNm", "OAuth2 User"); |
87 | 205 |
tempResponse.put("roles", new String[]{"ROLE_USER"}); |
... | ... | @@ -97,8 +215,8 @@ |
97 | 215 |
/** |
98 | 216 |
* 사용자 응답 데이터 생성 |
99 | 217 |
*/ |
100 |
- private Map<String, Object> createUserResponse(MberVO userInfo) { |
|
101 |
- Map<String, Object> response = new HashMap<>(); |
|
218 |
+ private HashMap<String, Object> createUserResponse(MberVO userInfo) { |
|
219 |
+ HashMap<String, Object> response = new HashMap<>(); |
|
102 | 220 |
response.put("mbrId", userInfo.getMbrId()); |
103 | 221 |
response.put("mbrNm", userInfo.getMbrNm()); |
104 | 222 |
response.put("eml", userInfo.getEml()); |
--- src/main/resources/mybatis/mapper/mber/mber-SQL.xml
+++ src/main/resources/mybatis/mapper/mber/mber-SQL.xml
... | ... | @@ -218,41 +218,6 @@ |
218 | 218 |
|
219 | 219 |
<!-- 이메일로만 사용자 조회 --> |
220 | 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 | 221 |
SELECT |
257 | 222 |
m.mbr_id as mbrId, |
258 | 223 |
m.lgn_id as lgnId, |
... | ... | @@ -281,14 +246,32 @@ |
281 | 246 |
m.mdfr, |
282 | 247 |
m.mdfcn_dt as mdfcnDt |
283 | 248 |
FROM mbr_info m |
284 |
- WHERE m.eml = #{0} |
|
285 |
- AND m.mbr_type = #{1} |
|
249 |
+ WHERE m.eml = #{email} |
|
286 | 250 |
AND m.use_yn = 'Y' |
287 | 251 |
AND m.mbr_stts = '1' |
288 | 252 |
</select> |
289 | 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 |
+ |
|
290 | 273 |
<!-- OAuth2 사용자 저장 --> |
291 |
- <insert id="saveOAuthUser" parameterType="MberVO"> |
|
274 |
+ <insert id="saveOAuthUser" parameterType="MberVO"> |
|
292 | 275 |
INSERT INTO mbr_info ( |
293 | 276 |
mbr_id, |
294 | 277 |
lgn_id, |
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?