diff --git a/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java b/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java index 163ae182..c4f687ba 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java +++ b/ontime-back/src/main/java/devkor/ontime_back/controller/SocialAuthController.java @@ -6,6 +6,7 @@ import devkor.ontime_back.dto.OAuthKakaoUserDto; import devkor.ontime_back.global.oauth.apple.AppleLoginService; import devkor.ontime_back.global.oauth.google.GoogleLoginService; +import devkor.ontime_back.response.ApiResponseForm; import devkor.ontime_back.service.UserAuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -16,6 +17,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @Slf4j @@ -113,24 +115,32 @@ public String appleRegisterOrLogin(@RequestBody OAuthAppleRequestDto appleLoginR summary = "애플 소셜 로그인 회원탈퇴" ) @DeleteMapping("/apple/me") - public String appleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) throws Exception { + public ResponseEntity> appleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) { Long userId = userAuthService.getUserIdFromToken(request); log.info("userId: {}", userId); - appleLoginService.revokeToken(userId); + try { + appleLoginService.revokeToken(userId); + } catch (Exception e) { + log.warn("Apple 토큰 철회에 실패했지만 계정 삭제를 계속 진행합니다. userId={}, reason={}", userId, e.getMessage()); + } userAuthService.deleteUser(userId, feedbackAddDto); - return "애플 로그인 회원탈퇴 성공"; + return ResponseEntity.ok(ApiResponseForm.success(null, "애플 로그인 회원탈퇴 성공")); } @Operation( summary = "구글 소셜 로그인 회원탈퇴" ) @DeleteMapping("/google/me") - public String googleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) throws Exception { + public ResponseEntity> googleDeleteUser(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) FeedbackAddDto feedbackAddDto) { Long userId = userAuthService.getUserIdFromToken(request); log.info("userId: {}", userId); - googleLoginService.revokeToken(userId); + try { + googleLoginService.revokeToken(userId); + } catch (Exception e) { + log.warn("Google 토큰 철회에 실패했지만 계정 삭제를 계속 진행합니다. userId={}, reason={}", userId, e.getMessage()); + } userAuthService.deleteUser(userId, feedbackAddDto); - return "구글 로그인 회원탈퇴 성공"; + return ResponseEntity.ok(ApiResponseForm.success(null, "구글 로그인 회원탈퇴 성공")); } diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java index 928a90b7..0c237eac 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginFilter.java @@ -12,14 +12,15 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import java.io.IOException; +import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @Slf4j @@ -48,13 +49,26 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ Object loginLock = LOGIN_LOCKS.computeIfAbsent(googleUserId, key -> new Object()); synchronized (loginLock) { - Optional existingUser = userRepository.findBySocialTypeAndSocialId(SocialType.GOOGLE, googleUserId); - - if (existingUser.isPresent()) { - return googleLoginService.handleLogin(oAuthGoogleRequestDto, existingUser.get(), response); + List existingUsers = userRepository.findAllBySocialTypeAndSocialIdOrderByIdDesc(SocialType.GOOGLE, googleUserId); + + if (!existingUsers.isEmpty()) { + if (existingUsers.size() > 1) { + log.warn("동일한 Google socialId를 가진 유저가 {}명 존재합니다. 최신 userId={} 계정으로 로그인합니다.", + existingUsers.size(), existingUsers.get(0).getId()); + } + return googleLoginService.handleLogin(oAuthGoogleRequestDto, existingUsers.get(0), response); } else { OAuthGoogleUserDto oAuthGoogleUserDto = new OAuthGoogleUserDto(googleUserId, (String) googlePayload.get("name"), (String) googlePayload.get("picture"), googlePayload.getEmail()); - return googleLoginService.handleRegister(oAuthGoogleRequestDto, oAuthGoogleUserDto, response); + try { + return googleLoginService.handleRegister(oAuthGoogleRequestDto, oAuthGoogleUserDto, response); + } catch (DataIntegrityViolationException e) { + log.warn("Google 회원가입 중 중복 socialId가 감지되어 기존 계정으로 로그인합니다. socialId={}", googleUserId); + User user = userRepository.findAllBySocialTypeAndSocialIdOrderByIdDesc(SocialType.GOOGLE, googleUserId) + .stream() + .findFirst() + .orElseThrow(() -> e); + return googleLoginService.handleLogin(oAuthGoogleRequestDto, user, response); + } } } diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java index 2be184bc..a2379e32 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java @@ -22,6 +22,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -175,7 +176,10 @@ public boolean revokeToken(Long userId) { String googleRefreshToken = user.getSocialLoginToken(); - RestTemplate restTemplate = new RestTemplate(); + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(3000); + requestFactory.setReadTimeout(3000); + RestTemplate restTemplate = new RestTemplate(requestFactory); String revokeUrl = GOOGLE_REVOKE_URL + googleRefreshToken; HttpHeaders headers = new HttpHeaders(); diff --git a/ontime-back/src/main/java/devkor/ontime_back/repository/UserRepository.java b/ontime-back/src/main/java/devkor/ontime_back/repository/UserRepository.java index d00c55d7..dc745b79 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/repository/UserRepository.java +++ b/ontime-back/src/main/java/devkor/ontime_back/repository/UserRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -21,8 +22,10 @@ public interface UserRepository extends JpaRepository { // 추가정보 입력받을때 사용 Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); + List findAllBySocialTypeAndSocialIdOrderByIdDesc(SocialType socialType, String socialId); + @Query("SELECT u.spareTime FROM User u WHERE u.id = :id") Integer findSpareTimeById(Long id); Optional findByAccessToken(String token); -} \ No newline at end of file +} diff --git a/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql b/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql new file mode 100644 index 00000000..c29388c5 --- /dev/null +++ b/ontime-back/src/main/resources/db/migration/V10__deduplicate_social_users_add_unique_constraint.sql @@ -0,0 +1,33 @@ +DELETE u +FROM user u +JOIN ( + SELECT * FROM ( + SELECT social_type, social_id, MAX(user_id) AS keep_user_id + FROM user + WHERE social_type IS NOT NULL + AND social_id IS NOT NULL + GROUP BY social_type, social_id + HAVING COUNT(*) > 1 + ) duplicate_groups +) d + ON u.social_type = d.social_type + AND u.social_id = d.social_id +WHERE u.user_id <> d.keep_user_id; + +SET @constraint_exists = ( + SELECT COUNT(*) + FROM information_schema.table_constraints + WHERE constraint_schema = DATABASE() + AND table_name = 'user' + AND constraint_name = 'uk_user_social_type_social_id' +); + +SET @add_constraint_sql = IF( + @constraint_exists = 0, + 'ALTER TABLE user ADD CONSTRAINT uk_user_social_type_social_id UNIQUE (social_type, social_id)', + 'SELECT 1' +); + +PREPARE add_constraint_statement FROM @add_constraint_sql; +EXECUTE add_constraint_statement; +DEALLOCATE PREPARE add_constraint_statement;