diff --git a/src/main/java/com/bookmile/backend/domain/record/controller/RecordController.java b/src/main/java/com/bookmile/backend/domain/record/controller/RecordController.java
index 47cca65..b477aa2 100644
--- a/src/main/java/com/bookmile/backend/domain/record/controller/RecordController.java
+++ b/src/main/java/com/bookmile/backend/domain/record/controller/RecordController.java
@@ -11,7 +11,6 @@
import com.bookmile.backend.domain.record.service.Impl.RecordServiceImpl;
import com.bookmile.backend.global.common.CommonResponse;
import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/com/bookmile/backend/domain/user/controller/OAuthController.java b/src/main/java/com/bookmile/backend/domain/user/controller/OAuthController.java
new file mode 100644
index 0000000..b0a35d7
--- /dev/null
+++ b/src/main/java/com/bookmile/backend/domain/user/controller/OAuthController.java
@@ -0,0 +1,57 @@
+package com.bookmile.backend.domain.user.controller;
+
+import com.bookmile.backend.domain.user.service.OAuthService;
+import com.bookmile.backend.global.common.CommonResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static com.bookmile.backend.global.common.StatusCode.SIGN_IN;
+import static com.bookmile.backend.global.common.StatusCode.USER_FOUND;
+
+@RestController
+@RequestMapping("api/v1/oauth2")
+@RequiredArgsConstructor
+public class OAuthController {
+ private final OAuthService oAuthService;
+
+ @Operation(summary = "[테스트] OAuth2 로그인 (소셜로그인)", description = "테스트용입니다.
" +
+ "토큰의 유효시간이 매우 짧습니다. accessToken : 30초, refreshToken : 1분
" +
+ "회원가입 따로 없이, test용 email을 입력하여 계정을 생성하고, redirectUrl이 올바르게 나오는지 확인합니다.
" +
+ "또한, 제공한 accessToken, refreshToken을 유효한지 확인합니다. ")
+ @PostMapping("/test")
+ public ResponseEntity>> testSocialLogin(
+ @RequestParam String email
+ ) {
+ return ResponseEntity.status(SIGN_IN.getStatus())
+ .body(CommonResponse.from(SIGN_IN.getMessage(), oAuthService.testSocialLogin(email)));
+ }
+
+ @Operation(summary = "연동된 소셜 로그인 조회", description = "현재 로그인한 사용자의 연동된 소셜 로그인 목록을 조회합니다.")
+ @GetMapping
+ public ResponseEntity>> getOAuthProviders(
+ @AuthenticationPrincipal UserDetails userDetails) {
+
+ return ResponseEntity.ok(CommonResponse.from(USER_FOUND.getMessage(), oAuthService.getOAuthProviders(userDetails.getUsername())));
+ }
+
+ @Operation(summary = "소셜로그인 연동 해제", description = "소셜 로그인 연동을 해제합니다.
" +
+ "provider에는 kakao, google, naver 중 1개 입력해주세요.
" +
+ "Header에 accessToken 필요합니다!")
+ @PostMapping( "unlink/{provider}")
+ public ResponseEntity> unlinkOAuthProvider(
+ HttpServletRequest request,
+ @PathVariable(value = "provider") String provider,
+ @AuthenticationPrincipal UserDetails userDetails) {
+ oAuthService.unlinkUserOAuth(request, provider, userDetails.getUsername());
+
+ return ResponseEntity.ok(CommonResponse.from("연동해제 성공"));
+ }
+}
diff --git a/src/main/java/com/bookmile/backend/domain/user/controller/UserController.java b/src/main/java/com/bookmile/backend/domain/user/controller/UserController.java
index 172f664..482fc2b 100644
--- a/src/main/java/com/bookmile/backend/domain/user/controller/UserController.java
+++ b/src/main/java/com/bookmile/backend/domain/user/controller/UserController.java
@@ -11,14 +11,12 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
-import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
-import java.util.HashMap;
import java.util.Map;
import static com.bookmile.backend.global.common.StatusCode.*;
@@ -135,18 +133,8 @@ public ResponseEntity> testSignIn(
.body(CommonResponse.from(SIGN_IN.getMessage(),userService.testSignIn(signInReqDto)));
}
- @Operation(summary = "[테스트] OAuth2 로그인 (소셜로그인)", description = "테스트용입니다.
" +
- "회원가입 따로 없이, test용 email을 입력하여 계정을 생성하고, rediectUrl이 올바르게 나오는지 확인합니다.
" +
- "또한, 제공한 accessToken, refreshToken을 유효한지 확인합니다. ")
- @PostMapping("/test/social-login")
- public ResponseEntity>> testSocialLogin(
- @RequestParam String email
- ) {
- return ResponseEntity.status(SIGN_IN.getStatus())
- .body(CommonResponse.from(SIGN_IN.getMessage(), userService.testSocialLogin(email)));
- }
- @Operation(summary = "[테스트] 리다이렉트",description = "리다이렉트 url에 토큰이 올바른지 검사합니다.
" +
+ @Operation(summary = "[테스트] 토큰 확인용",description = "리다이렉트 url에 토큰이 올바른지 검사합니다.
" +
"유저의 정보를 확인합니다.")
@PostMapping("/test/redirect")
public ResponseEntity>> testRedirect(
diff --git a/src/main/java/com/bookmile/backend/domain/user/entity/User.java b/src/main/java/com/bookmile/backend/domain/user/entity/User.java
index 085fc81..a01fc01 100644
--- a/src/main/java/com/bookmile/backend/domain/user/entity/User.java
+++ b/src/main/java/com/bookmile/backend/domain/user/entity/User.java
@@ -34,22 +34,14 @@ public class User extends BaseEntity {
@Enumerated(EnumType.STRING)
private UserRole role;
- // OAuth2.0 제공자
- private String provider;
-
- // OAuth 로그인 유저의 고유 ID
- private String providerId;
-
@Builder
- public User(String nickname, String email, String password, String image, Boolean isDeleted, UserRole role, String provider, String providerId) {
+ public User(String nickname, String email, String password, String image, Boolean isDeleted, UserRole role) {
this.nickname = nickname;
this.email = email;
this.password = password;
this.image = image;
this.isDeleted = isDeleted;
this.role = role;
- this.provider = provider;
- this.providerId = providerId;
}
public void updateNickname(String newNickname) {
diff --git a/src/main/java/com/bookmile/backend/domain/user/entity/UserOAuth.java b/src/main/java/com/bookmile/backend/domain/user/entity/UserOAuth.java
new file mode 100644
index 0000000..89d2451
--- /dev/null
+++ b/src/main/java/com/bookmile/backend/domain/user/entity/UserOAuth.java
@@ -0,0 +1,38 @@
+package com.bookmile.backend.domain.user.entity;
+
+import com.bookmile.backend.global.config.BaseEntity;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class UserOAuth extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "oauth_id")
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ private User user;
+
+ // OAuth2.0 제공자
+ @Column(nullable = false)
+ private String provider;
+
+ // OAuth 로그인 유저의 고유 ID
+ @Column(name= "provider_id", nullable = false, unique = true)
+ private String providerId;
+
+ @Builder
+ public UserOAuth(User user, String provider, String providerId) {
+ this.user = user;
+ this.provider = provider;
+ this.providerId = providerId;
+ }
+}
diff --git a/src/main/java/com/bookmile/backend/domain/user/repository/UserOAuthRepository.java b/src/main/java/com/bookmile/backend/domain/user/repository/UserOAuthRepository.java
new file mode 100644
index 0000000..ea0f9f3
--- /dev/null
+++ b/src/main/java/com/bookmile/backend/domain/user/repository/UserOAuthRepository.java
@@ -0,0 +1,16 @@
+package com.bookmile.backend.domain.user.repository;
+
+import com.bookmile.backend.domain.user.entity.User;
+import com.bookmile.backend.domain.user.entity.UserOAuth;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface UserOAuthRepository extends JpaRepository {
+ Optional findByProviderAndProviderId(String provider, String providerId);
+ List findByUserId(Long userId);
+ Optional findByUserIdAndProvider(Long userId, String provider);
+}
\ No newline at end of file
diff --git a/src/main/java/com/bookmile/backend/domain/user/repository/UserRepository.java b/src/main/java/com/bookmile/backend/domain/user/repository/UserRepository.java
index 17a4ffe..b0500ad 100644
--- a/src/main/java/com/bookmile/backend/domain/user/repository/UserRepository.java
+++ b/src/main/java/com/bookmile/backend/domain/user/repository/UserRepository.java
@@ -10,8 +10,6 @@
public interface UserRepository extends JpaRepository {
Optional findByEmail(String email);
- Boolean existsByEmail(String email);
-
Optional findById(Long userId);
Boolean existsByNickname(String nickname);
diff --git a/src/main/java/com/bookmile/backend/domain/user/service/OAuthService.java b/src/main/java/com/bookmile/backend/domain/user/service/OAuthService.java
new file mode 100644
index 0000000..e6fa4be
--- /dev/null
+++ b/src/main/java/com/bookmile/backend/domain/user/service/OAuthService.java
@@ -0,0 +1,13 @@
+package com.bookmile.backend.domain.user.service;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.util.List;
+import java.util.Map;
+
+public interface OAuthService {
+ List getOAuthProviders(String email);
+ void unlinkUserOAuth(HttpServletRequest request, String provider, String email);
+
+ Map testSocialLogin(String email);
+}
diff --git a/src/main/java/com/bookmile/backend/domain/user/service/UserService.java b/src/main/java/com/bookmile/backend/domain/user/service/UserService.java
index 52e8669..4bfb43f 100644
--- a/src/main/java/com/bookmile/backend/domain/user/service/UserService.java
+++ b/src/main/java/com/bookmile/backend/domain/user/service/UserService.java
@@ -6,7 +6,6 @@
import com.bookmile.backend.domain.user.dto.req.UserInfoReqDto;
import com.bookmile.backend.domain.user.dto.res.TokenResDto;
import com.bookmile.backend.domain.user.dto.res.UserDetailResDto;
-import com.bookmile.backend.domain.user.dto.res.UserInfoDto;
import com.bookmile.backend.domain.user.dto.res.UserResDto;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.multipart.MultipartFile;
@@ -17,7 +16,6 @@ public interface UserService {
UserResDto signUp(SignUpReqDto signUpReqDto);
TokenResDto signIn(SignInReqDto signInReqDto);
TokenResDto reIssue(HttpServletRequest request);
- UserInfoDto getUserInfo(Long userId);
UserDetailResDto getUser(String email);
Boolean checkNickname(String nickname);
@@ -30,6 +28,5 @@ public interface UserService {
// 테스트 로그인
TokenResDto testSignIn(SignInReqDto signInReqDto);
- Map testSocialLogin(String email);
Map testRedirect(String accessToken);
}
\ No newline at end of file
diff --git a/src/main/java/com/bookmile/backend/domain/user/service/impl/OAuthServiceImpl.java b/src/main/java/com/bookmile/backend/domain/user/service/impl/OAuthServiceImpl.java
new file mode 100644
index 0000000..dd8bbdd
--- /dev/null
+++ b/src/main/java/com/bookmile/backend/domain/user/service/impl/OAuthServiceImpl.java
@@ -0,0 +1,145 @@
+package com.bookmile.backend.domain.user.service.impl;
+
+import com.bookmile.backend.domain.user.entity.User;
+import com.bookmile.backend.domain.user.entity.UserOAuth;
+import com.bookmile.backend.domain.user.repository.UserOAuthRepository;
+import com.bookmile.backend.domain.user.repository.UserRepository;
+import com.bookmile.backend.domain.user.service.OAuthService;
+import com.bookmile.backend.global.common.StatusCode;
+import com.bookmile.backend.global.common.UserRole;
+import com.bookmile.backend.global.exception.CustomException;
+import com.bookmile.backend.global.jwt.JwtTokenProvider;
+import com.bookmile.backend.global.oauth.OAuth2UnlinkService;
+import com.bookmile.backend.global.common.RandomNickname;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.bookmile.backend.global.common.StatusCode.AUTHENTICATION_FAILED;
+import static com.bookmile.backend.global.common.StatusCode.INVALID_OAUTH_USER;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class OAuthServiceImpl implements OAuthService {
+ private final UserRepository userRepository;
+ private final UserOAuthRepository userOAuthRepository;
+ private final JwtTokenProvider jwtTokenProvider;
+ private final RandomNickname randomNickname;
+ private final OAuth2UnlinkService oAuth2UnlinkService;
+
+ @Value("${aws.main.profile}")
+ private String mainProfile;
+
+ @Value("${spring.oauth2.url.callback}")
+ private String callBackUrl;
+
+ // 회원의 소셜 연동 정보 조회
+ @Override
+ @Transactional
+ public List getOAuthProviders(String email) {
+ User user = findByEmail(email);
+
+ List providers = userOAuthRepository.findByUserId(user.getId()).stream()
+ .map(UserOAuth::getProvider)
+ .toList();
+
+ return providers;
+ }
+
+
+ @Override
+ @Transactional
+ public void unlinkUserOAuth(HttpServletRequest request, String provider, String email) {
+ String token = jwtTokenProvider.resolveToken(request);
+
+ User user = findByEmail(email);
+
+ UserOAuth userOAuth = userOAuthRepository.findByUserIdAndProvider(user.getId(), provider).orElseThrow(
+ () -> new CustomException(INVALID_OAUTH_USER));
+
+ // 연동 해제
+ if (provider.equals("kakao")){
+ oAuth2UnlinkService.unlinkKakao(userOAuth.getProviderId());
+ }else if(provider.equals("naver")){
+ oAuth2UnlinkService.unlinkNaver(token);
+ }else if(provider.equals("google")){
+ oAuth2UnlinkService.unlinkGoogle(token);
+ }else{
+ throw new CustomException(StatusCode.INPUT_VALUE_INVALID);
+ }
+
+ userOAuthRepository.delete(userOAuth);
+ }
+
+ // [테스트용] - OAuth 로그인
+ @Override
+ public Map testSocialLogin(String email) {
+
+ // test용 유저 생성
+ User testUser = userRepository.findByEmail(email).orElseGet(() -> {
+
+ // 유저 정보 저장
+ User newUser = userRepository.save( User.builder()
+ .email(email)
+ .nickname(randomNickname.generateNickname())
+ .image(mainProfile)
+ .role(UserRole.USER)
+ .isDeleted(false)
+ .build());
+
+ // OAuth2.0 정보 저장
+ userOAuthRepository.save( UserOAuth.builder()
+ .user(newUser)
+ .provider("test")
+ .providerId("test")
+ .build());
+ return newUser;
+ });
+
+ // OAuth2User 생성
+ Map attributes = new HashMap<>();
+ attributes.put("email", testUser.getEmail());
+ attributes.put("exist", true);
+ attributes.put("userId", testUser.getId());
+
+ OAuth2User oAuth2User = new DefaultOAuth2User(
+ Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
+ attributes, "email");
+
+ String accessToken = jwtTokenProvider.createTestAccessToken(testUser.getEmail(), testUser.getId(),
+ testUser.getRole().toString());
+ String refreshToken = jwtTokenProvider.createTestRefreshToken(testUser.getEmail(), testUser.getId());
+
+ String redirectUrl = UriComponentsBuilder.fromHttpUrl(callBackUrl)
+ .queryParam("testAccess", accessToken)
+ .queryParam("testRefresh", refreshToken)
+ .toUriString();
+
+ log.info("UserServiceImpl.testSocialLogin: redirectUrl - {},", redirectUrl);
+
+ Map response = new HashMap<>();
+ response.put("redirectUrl", redirectUrl);
+ response.put("accessToken", accessToken);
+ response.put("refreshToken", refreshToken);
+
+ return response;
+ }
+
+ private User findByEmail(String email) {
+ return userRepository.findByEmail(email).orElseThrow(() -> new CustomException(AUTHENTICATION_FAILED));
+ }
+
+}
diff --git a/src/main/java/com/bookmile/backend/domain/user/service/impl/UserServiceImpl.java b/src/main/java/com/bookmile/backend/domain/user/service/impl/UserServiceImpl.java
index 9c6f563..3f8f016 100644
--- a/src/main/java/com/bookmile/backend/domain/user/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/bookmile/backend/domain/user/service/impl/UserServiceImpl.java
@@ -1,42 +1,22 @@
package com.bookmile.backend.domain.user.service.impl;
-import static com.bookmile.backend.global.common.StatusCode.AUTHENTICATION_FAILED;
-import static com.bookmile.backend.global.common.StatusCode.EMAIL_CODE_NOT_MATCH;
-import static com.bookmile.backend.global.common.StatusCode.EMAIL_TOO_MANY_REQUESTS;
-import static com.bookmile.backend.global.common.StatusCode.INVALID_FILE_TYPE;
-import static com.bookmile.backend.global.common.StatusCode.INVALID_TOKEN;
-import static com.bookmile.backend.global.common.StatusCode.MAIL_SERVER_ERROR;
-import static com.bookmile.backend.global.common.StatusCode.PASSWORD_DUPLICATE;
-import static com.bookmile.backend.global.common.StatusCode.PASSWORD_NOT_MATCH;
-import static com.bookmile.backend.global.common.StatusCode.TOKEN_NOT_FOUND;
-import static com.bookmile.backend.global.common.StatusCode.USER_ALREADY_EXISTS;
-import static com.bookmile.backend.global.common.StatusCode.USER_NOT_FOUND;
+import static com.bookmile.backend.global.common.StatusCode.*;
import com.bookmile.backend.domain.image.service.ImageService;
-import com.bookmile.backend.domain.user.dto.req.PasswordReqDto;
-import com.bookmile.backend.domain.user.dto.req.SignInReqDto;
-import com.bookmile.backend.domain.user.dto.req.SignUpReqDto;
-import com.bookmile.backend.domain.user.dto.req.UserInfoReqDto;
-import com.bookmile.backend.domain.user.dto.res.TokenResDto;
-import com.bookmile.backend.domain.user.dto.res.UserDetailResDto;
-import com.bookmile.backend.domain.user.dto.res.UserInfoDto;
-import com.bookmile.backend.domain.user.dto.res.UserResDto;
+import com.bookmile.backend.domain.user.dto.req.*;
+import com.bookmile.backend.domain.user.dto.res.*;
import com.bookmile.backend.domain.user.entity.User;
-import com.bookmile.backend.domain.user.repository.UserRepository;
+import com.bookmile.backend.domain.user.repository.*;
import com.bookmile.backend.domain.user.service.UserService;
-import com.bookmile.backend.global.common.UserRole;
import com.bookmile.backend.global.exception.CustomException;
import com.bookmile.backend.global.jwt.JwtTokenProvider;
-import com.bookmile.backend.global.oauth.nickname.RandomNickname;
-import com.bookmile.backend.global.redis.RefreshToken;
-import com.bookmile.backend.global.redis.RefreshTokenRepository;
+import com.bookmile.backend.global.common.RandomNickname;
+import com.bookmile.backend.global.redis.*;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import jakarta.servlet.http.HttpServletRequest;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Random;
+
+import java.util.*;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -45,14 +25,10 @@
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Async;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
-import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.util.UriComponentsBuilder;
@Service
@RequiredArgsConstructor
@@ -76,9 +52,6 @@ public class UserServiceImpl implements UserService {
@Value("${aws.main.profile}")
private String mainProfile;
- @Value("${spring.oauth2.url.callback}")
- private String callBackUrl;
-
private static final String[] ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"};
@Override
@@ -157,15 +130,6 @@ public TokenResDto reIssue(HttpServletRequest request) {
return TokenResDto.toDto(newAccessToken, newRefreshToken);
}
- // 회원 정보 조회
- @Override
- public UserInfoDto getUserInfo(Long userId) {
- User user = userRepository.findById(userId)
- .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
-
- return UserInfoDto.toDto(user);
- }
-
// 회원 정보 조회 (토큰)
@Override
public UserDetailResDto getUser(String email) {
@@ -298,7 +262,7 @@ public void deleteUser(String email) {
user.updateIsDeleted();
}
- // 테스트용 - 로그인
+ // [테스트용] - 로그인
@Override
@Transactional
public TokenResDto testSignIn(SignInReqDto signInReqDto) {
@@ -315,54 +279,7 @@ public TokenResDto testSignIn(SignInReqDto signInReqDto) {
return TokenResDto.toDto(accessToken, refreshToken);
}
- // 테스트용 - OAuth 로그인
- @Override
- @Transactional
- public Map testSocialLogin(String email) {
-
- // test용 유저 생성
- User testUser = userRepository.findByEmail(email).orElseGet(() -> {
- User newUser = User.builder()
- .email(email)
- .nickname(randomNickname.generateNickname())
- .image(mainProfile)
- .provider("test")
- .providerId("test")
- .role(UserRole.USER)
- .isDeleted(false)
- .build();
- return userRepository.save(newUser);
- });
-
- // OAuth2User 생성
- Map attributes = new HashMap<>();
- attributes.put("email", testUser.getEmail());
- attributes.put("exist", true);
- attributes.put("userId", testUser.getId());
-
- OAuth2User oAuth2User = new DefaultOAuth2User(
- Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
- attributes, "email");
-
- String accessToken = jwtTokenProvider.createTestAccessToken(testUser.getEmail(), testUser.getId(),
- testUser.getRole().toString());
- String refreshToken = jwtTokenProvider.createTestRefreshToken(testUser.getEmail(), testUser.getId());
-
- String redirectUrl = UriComponentsBuilder.fromHttpUrl(callBackUrl)
- .queryParam("testAccess", accessToken)
- .queryParam("testRefresh", refreshToken)
- .toUriString();
-
- log.info("UserServiceImpl.testSocialLogin: redirectUrl - {},", redirectUrl);
-
- Map response = new HashMap<>();
- response.put("redirectUrl", redirectUrl);
- response.put("accessToken", accessToken);
- response.put("refreshToken", refreshToken);
-
- return response;
- }
-
+ // [테스트용] 리다이렉트 경로 확인
@Override
public Map testRedirect(String accessToken) {
log.info("UserServiceImpl.testRedirect: accessToken - {} ", accessToken);
@@ -378,7 +295,6 @@ public Map testRedirect(String accessToken) {
return response;
}
-
private boolean validateImageFile(MultipartFile file) {
String extension = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase();
@@ -390,6 +306,10 @@ private boolean validateImageFile(MultipartFile file) {
return false;
}
+ private User findByEmail(String email) {
+ return userRepository.findByEmail(email).orElseThrow(() -> new CustomException(AUTHENTICATION_FAILED));
+ }
+
private Map getUserIdByToken(HttpServletRequest request) {
Map map = new HashMap<>();
@@ -410,12 +330,8 @@ private void existsByEmail(String email) {
if (userRepository.existsByEmailAndIsDeletedFalse(email)) {
throw new CustomException(USER_ALREADY_EXISTS);
}
- ;
}
- private User findByEmail(String email) {
- return userRepository.findByEmail(email).orElseThrow(() -> new CustomException(AUTHENTICATION_FAILED));
- }
// 이메일 요청 카운트 증가
private void increaseEmailRequestCount(String email) {
diff --git a/src/main/java/com/bookmile/backend/global/oauth/nickname/RandomNickname.java b/src/main/java/com/bookmile/backend/global/common/RandomNickname.java
similarity index 98%
rename from src/main/java/com/bookmile/backend/global/oauth/nickname/RandomNickname.java
rename to src/main/java/com/bookmile/backend/global/common/RandomNickname.java
index 5a2969e..a831630 100644
--- a/src/main/java/com/bookmile/backend/global/oauth/nickname/RandomNickname.java
+++ b/src/main/java/com/bookmile/backend/global/common/RandomNickname.java
@@ -1,4 +1,4 @@
-package com.bookmile.backend.global.oauth.nickname;
+package com.bookmile.backend.global.common;
import com.bookmile.backend.domain.user.repository.UserRepository;
import com.bookmile.backend.global.exception.CustomException;
diff --git a/src/main/java/com/bookmile/backend/global/common/StatusCode.java b/src/main/java/com/bookmile/backend/global/common/StatusCode.java
index 424c73e..22f5ac4 100644
--- a/src/main/java/com/bookmile/backend/global/common/StatusCode.java
+++ b/src/main/java/com/bookmile/backend/global/common/StatusCode.java
@@ -65,7 +65,6 @@ public enum StatusCode {
FORBIDDEN_TOKEN(FORBIDDEN, "접근 권한이 없습니다."),
TOKEN_NOT_FOUND(NOT_FOUND, "존재하는 토큰이 없습니다."),
-
/* 400 BAD_REQUEST : 잘못된 요청 */
PASSWORD_NOT_MATCH(BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
PASSWORD_DUPLICATE(BAD_REQUEST, "이전 비밀번호와 동일합니다."),
@@ -76,6 +75,10 @@ public enum StatusCode {
CUSTOM_GOAL_REQUIRED(BAD_REQUEST, "GoalType이 CUSTOM일 경우 사용자 정의 목표(customGoal)는 필수입니다."),
MULTI_PART_FILE_INVALID(BAD_REQUEST,"유효하지 않은 MultiPartFile입니다."),
FILE_INVALID(BAD_REQUEST,"파일 저장 중 오류가 발생했습니다."),
+ INVALID_REQUEST(BAD_REQUEST,"유효하지 않은 요청입니다."),
+ INVALID_GROUP_STATUS_UPDATE(BAD_REQUEST,"잘못된 요청입니다."),
+ INVALID_GROUP(BAD_REQUEST,"유효하지 않은 그룹입니다." ),
+ INVALID_BOOK_ID(BAD_REQUEST,"존재하지 않는 책입니다." ),
/* 401 UNAUTHORIZED : 비인증 사용자 */
AUTHENTICATION_FAILED(UNAUTHORIZED, "회원의 정보가 일치하지 않습니다."),
@@ -98,6 +101,7 @@ public enum StatusCode {
INVALID_GOAL_TYPE(NOT_FOUND, "유효하지 않은 GoalType 값입니다"),
INVALID_TEMPLATE_ID(NOT_FOUND, "존재하지 않는 템플릿입니다."),
BOOK_INFO_NOT_FOUND(NOT_FOUND, "책 정보를 가져올 수 없습니다."),
+ INVALID_OAUTH_USER(NOT_FOUND, "해당 소셜 계정이 연동되지 않았습니다."),
/* 409 CONFLICT : 리소스 충돌 */
USER_ALREADY_EXISTS(CONFLICT, "이미 존재하는 회원입니다."),
@@ -107,15 +111,13 @@ public enum StatusCode {
NICKNAME_TOO_MANY_REQUESTS(TOO_MANY_REQUESTS, "더이상 생성할 닉네임이 없습니다."),
/* 500 INTERNAL_SERVER_ERROR Error */
+ SERVER_ERROR(INTERNAL_SERVER_ERROR, "프로그램 서버에 연결할 수 없습니다."),
REDIS_ERROR(INTERNAL_SERVER_ERROR, "Redis 서버에 연결할 수 없습니다. "),
MAIL_SERVER_ERROR(INTERNAL_SERVER_ERROR, "메일 서버에 오류가 생겼습니다."),
INVALID_TEMPLATE_USAGE(NOT_FOUND,"완독한 그룹의 템플릿만 사용할 수 있습니다."),
GOAL_CONTENT_REQUIRED(NOT_FOUND,"목표 상세 내용은 필수입니다." ),
NO_PERMISSION(UNAUTHORIZED,"그룹장만 그룹의 상태를 변경할 수 있습니다."),
NOT_MEMBER(UNAUTHORIZED,"그룹 구성원이 아닙니다." ),
- INVALID_GROUP_STATUS_UPDATE(BAD_REQUEST,"잘못된 요청입니다."),
- INVALID_GROUP(BAD_REQUEST,"유효하지 않은 그룹입니다." ),
- INVALID_BOOK_ID(BAD_REQUEST,"존재하지 않는 책입니다." ),
BESTSELLER_SEARCH(OK,"베스트 셀러 조회에 성공했습니다." ),
NEWBOOK_SEARCH(OK,"신간 도서 조회에 성공했습니다." ),
diff --git a/src/main/java/com/bookmile/backend/global/oauth/CustomOAuth2UserService.java b/src/main/java/com/bookmile/backend/global/oauth/CustomOAuth2UserService.java
index abb6c66..e2d12da 100644
--- a/src/main/java/com/bookmile/backend/global/oauth/CustomOAuth2UserService.java
+++ b/src/main/java/com/bookmile/backend/global/oauth/CustomOAuth2UserService.java
@@ -1,8 +1,10 @@
package com.bookmile.backend.global.oauth;
import com.bookmile.backend.domain.user.entity.User;
+import com.bookmile.backend.domain.user.entity.UserOAuth;
+import com.bookmile.backend.domain.user.repository.UserOAuthRepository;
import com.bookmile.backend.domain.user.repository.UserRepository;
-import com.bookmile.backend.global.oauth.nickname.RandomNickname;
+import com.bookmile.backend.global.common.RandomNickname;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -24,6 +26,7 @@
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
+ private final UserOAuthRepository userOAuthRepository;
private final RandomNickname randomNickname;
@Override
@@ -33,40 +36,53 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
OAuth2User oauth2User = super.loadUser(userRequest);
// 클라이언트 등록 ID(google, naver, kakao)와 사용자 이름 속성을 가져온다.
- String registrationId = userRequest.getClientRegistration().getRegistrationId();
-
- String userNameAttributeName = userRequest.getClientRegistration()
- .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
- log.info("CustomOAuth2UserService.loadUser: userNameAttributeName {} " , userNameAttributeName);
+ String provider= userRequest.getClientRegistration().getRegistrationId();
// OAuth2UserService를 사용하여 가져온 OAuth2User 정보로 OAuth2UserInfo 객체를 만든다.
- OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(registrationId, userNameAttributeName, oauth2User.getAttributes());
- log.info("CustomOAuth2UserService.loadUser: OAuth2UserInfo - {}", oAuth2UserInfo);
+ OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(provider, oauth2User.getAttributes());
+ //log.info("CustomOAuth2UserService.loadUser: OAuth2UserInfo - {}", oAuth2UserInfo);
Map userAttributes = oAuth2UserInfo.convertToMap();
+ log.info("CustomOAuth2UserService.loadUser: userAttributes {}", userAttributes);
String email = (String) userAttributes.get("email");
+ String providerId = (String) userAttributes.get("attributeKey");
+ log.info("CustomOAuth2UserService.loadUser: providerId {}", providerId);
// User 정보 반환
Optional findUser = userRepository.findByEmail(email);
+ boolean isFirstLogin = findUser.isEmpty();
- if(findUser.isEmpty()) {
-
+ User user = findUser.orElseGet(() -> {
+ // 새로운 유저 추가
User newUser = userRepository.save(oAuth2UserInfo.toEntity(randomNickname));
- // 존재하지 않을 경우
- userAttributes.put("exist", false);
- userAttributes.put("userId", newUser.getId());
- return new DefaultOAuth2User(
- Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
- userAttributes, "email");
+ // 소셜 정보 저장
+ userOAuthRepository.save(UserOAuth.builder()
+ .user(newUser)
+ .provider(provider)
+ .providerId(providerId)
+ .build());
+
+ return newUser;
+ });
+
+ Optional existingOAuth = userOAuthRepository.findByProviderAndProviderId(provider, providerId);
+
+ // 기존 유저 + 새로운 provider 추가
+ if (!isFirstLogin && existingOAuth.isEmpty()) {
+ userOAuthRepository.save(
+ UserOAuth.builder()
+ .user(user)
+ .provider(provider)
+ .providerId(providerId)
+ .build());
}
- // 존재하는 경우
- userAttributes.put("exist", true);
- userAttributes.put("userId", findUser.map(User::getId).orElse(null));
+ userAttributes.put("exist", !isFirstLogin); // 최초 로그인 여부에 따라 T/F
+ userAttributes.put("userId", user.getId());
return new DefaultOAuth2User(
- Collections.singleton(new SimpleGrantedAuthority(findUser.get().getRole().toString())),
+ Collections.singleton(new SimpleGrantedAuthority(user.getRole().toString())),
userAttributes, "email");
}
diff --git a/src/main/java/com/bookmile/backend/global/oauth/OAuth2SuccessHandler.java b/src/main/java/com/bookmile/backend/global/oauth/OAuth2SuccessHandler.java
index e4e3389..3f83aa5 100644
--- a/src/main/java/com/bookmile/backend/global/oauth/OAuth2SuccessHandler.java
+++ b/src/main/java/com/bookmile/backend/global/oauth/OAuth2SuccessHandler.java
@@ -26,7 +26,7 @@ public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
private String callbackUrl;
@Value("${spring.oauth2.url.sign-up}")
- private String signUpUrl;
+ private String mainUrl;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
@@ -58,8 +58,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
response.sendRedirect(redirectUrl);
}
else{
- // 회원 정보 새로 저장 -> 로그인 페이지로 이동
- String redirectUrl = UriComponentsBuilder.fromHttpUrl(signUpUrl)
+ // 회원 정보 새로 저장 -> 메인페이지로 이동
+ String redirectUrl = UriComponentsBuilder.fromHttpUrl(mainUrl)
.toUriString();
response.sendRedirect(redirectUrl);
}
diff --git a/src/main/java/com/bookmile/backend/global/oauth/OAuth2UnlinkService.java b/src/main/java/com/bookmile/backend/global/oauth/OAuth2UnlinkService.java
new file mode 100644
index 0000000..16fdc43
--- /dev/null
+++ b/src/main/java/com/bookmile/backend/global/oauth/OAuth2UnlinkService.java
@@ -0,0 +1,128 @@
+package com.bookmile.backend.global.oauth;
+
+import com.bookmile.backend.global.common.StatusCode;
+import com.bookmile.backend.global.exception.CustomException;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Map;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OAuth2UnlinkService {
+
+ private String GOOGLE_URL = "https://oauth2.googleapis.com/revoke";
+ private String KAKAO_URL = "https://kapi.kakao.com/v1/user/unlink";
+ private String NAVER_URL = "https://nid.naver.com/oauth2.0/token";
+
+
+ @Value("${spring.security.oauth2.client.registration.naver.client-id}")
+ private String NAVER_CLIENT_ID;
+
+ @Value("${spring.security.oauth2.client.registration.naver.client-secret}")
+ private String NAVER_CLIENT_SECRET;
+
+ @Value("${spring.security.oauth2.client.provider.kakao.admin-key}")
+ private String KAKAO_ADMIN_KEY;
+
+ private final RestTemplate restTemplate;
+
+ public void unlinkKakao(String providerId) {
+
+ // 헤더에 admin key 넣기
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Authorization", "KakaoAK " + KAKAO_ADMIN_KEY);
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+
+ MultiValueMap params = new LinkedMultiValueMap<>();
+ params.add("target_id_type", "user_id");
+ params.add("target_id", providerId);
+ log.info("unlinkKaKao : {}", params);
+
+ HttpEntity> requestEntity = new HttpEntity<>(params, headers);
+
+ try {
+ // 카카오 API 로 POST 호출
+ ResponseEntity