diff --git a/src/main/java/solitour_backend/solitour/auth/controller/OauthController.java b/src/main/java/solitour_backend/solitour/auth/controller/OauthController.java index 931f75f..8bec21b 100644 --- a/src/main/java/solitour_backend/solitour/auth/controller/OauthController.java +++ b/src/main/java/solitour_backend/solitour/auth/controller/OauthController.java @@ -99,8 +99,8 @@ public ResponseEntity reissueAccessToken(HttpServletResponse response, @Authenticated @DeleteMapping() - public ResponseEntity deleteUser(HttpServletResponse response, @AuthenticationPrincipal Long id, - @RequestParam String type) { + public ResponseEntity deleteUser(HttpServletResponse response, @AuthenticationPrincipal Long id, + @RequestParam String type) { Token token = tokenRepository.findByUserId(id) .orElseThrow(() -> new TokenNotExistsException("토큰이 존재하지 않습니다")); String oauthRefreshToken = getOauthAccessToken(type, token.getOauthToken()); @@ -110,11 +110,11 @@ public ResponseEntity deleteUser(HttpServletResponse response, @Authenti oauthService.logout(response, id); oauthService.deleteUser(id); - - return ResponseEntity.ok("User deleted successfully"); } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); + throw new UserRevokeErrorException("회원 탈퇴 중 오류가 발생했습니다"); } + + return ResponseEntity.noContent().build(); } private String setCookieHeader(Cookie cookie) { @@ -128,10 +128,13 @@ private String getOauthAccessToken(String type, String refreshToken) { case "kakao" -> { token = kakaoConnector.refreshToken(refreshToken); } - case "google" -> { - token = googleConnector.refreshToken(refreshToken); + case "naver" -> { + token = naverConnector.refreshToken(refreshToken); } - default -> throw new RuntimeException("Unsupported oauth type"); +// case "google" -> { +// token = googleConnector.refreshToken(refreshToken); +// } + default -> throw new UnsupportedLoginTypeException("지원하지 않는 로그인 타입입니다"); } return token; } diff --git a/src/main/java/solitour_backend/solitour/auth/exception/RevokeFailException.java b/src/main/java/solitour_backend/solitour/auth/exception/RevokeFailException.java new file mode 100644 index 0000000..6bd23ec --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/exception/RevokeFailException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.auth.exception; + +public class RevokeFailException extends RuntimeException { + public RevokeFailException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/exception/UnsupportedLoginTypeException.java b/src/main/java/solitour_backend/solitour/auth/exception/UnsupportedLoginTypeException.java new file mode 100644 index 0000000..4b10d35 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/exception/UnsupportedLoginTypeException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.auth.exception; + +public class UnsupportedLoginTypeException extends RuntimeException { + public UnsupportedLoginTypeException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/exception/UserRevokeErrorException.java b/src/main/java/solitour_backend/solitour/auth/exception/UserRevokeErrorException.java new file mode 100644 index 0000000..d69a887 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/exception/UserRevokeErrorException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.auth.exception; + +public class UserRevokeErrorException extends RuntimeException { + public UserRevokeErrorException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/service/OauthService.java b/src/main/java/solitour_backend/solitour/auth/service/OauthService.java index f7d90da..f507a2a 100644 --- a/src/main/java/solitour_backend/solitour/auth/service/OauthService.java +++ b/src/main/java/solitour_backend/solitour/auth/service/OauthService.java @@ -151,7 +151,23 @@ private User checkAndSaveUser(String type, String code, String redirectUrl) { checkUserStatus(user); Token token = tokenRepository.findByUserId(user.getId()) - .orElseGet(() -> tokenService.saveToken(tokenResponse, user)); + .orElseGet(() -> tokenService.saveToken(tokenResponse.getRefreshToken(), user)); + + return user; + } + if (Objects.equals(type, "naver")) { + NaverTokenAndUserResponse response = naverConnector.requestNaverUserInfo(code); + NaverTokenResponse tokenResponse = response.getNaverTokenResponse(); + NaverUserResponse naverUserResponse = response.getNaverUserResponse(); + + String id = naverUserResponse.getResponse().getId().toString(); + User user = userRepository.findByOauthId(id) + .orElseGet(() -> saveNaverUser(naverUserResponse)); + + checkUserStatus(user); + + Token token = tokenRepository.findByUserId(user.getId()) + .orElseGet(() -> tokenService.saveToken(tokenResponse.getRefreshToken(), user)); return user; } @@ -162,7 +178,38 @@ private User checkAndSaveUser(String type, String code, String redirectUrl) { return userRepository.findByOauthId(id) .orElseGet(() -> saveGoogleUser(response)); } else { - throw new RuntimeException("지원하지 않는 oauth 타입입니다."); + throw new UnsupportedLoginTypeException("지원하지 않는 oauth 로그인입니다."); + } + } + + private User saveNaverUser(NaverUserResponse naverUserResponse) { + String convertedSex = convertSex(naverUserResponse.getResponse().getGender()); + String imageUrl = getDefaultUserImage(convertedSex); + UserImage savedUserImage = userImageService.saveUserImage(imageUrl); + + User user = User.builder() + .userStatus(UserStatus.ACTIVATE) + .oauthId(String.valueOf(naverUserResponse.getResponse().getId())) + .provider("naver") + .isAdmin(false) + .userImage(savedUserImage) + .nickname(RandomNickName.generateRandomNickname()) + .email(naverUserResponse.getResponse().getEmail()) + .name(naverUserResponse.getResponse().getName()) + .age(Integer.parseInt(naverUserResponse.getResponse().getBirthyear())) + .sex(convertedSex) + .createdAt(LocalDateTime.now()) + .build(); + return userRepository.save(user); + } + + private String convertSex(String gender) { + if (gender.equals("M")) { + return "male"; + } else if (gender.equals("F")) { + return "female"; + } else { + return "none"; } } @@ -175,15 +222,6 @@ private void checkUserStatus(User user) { } } - private void saveToken(KakaoTokenResponse tokenResponse, User user) { - Token token = Token.builder() - .user(user) - .oauthToken(tokenResponse.getRefreshToken()) - .build(); - - tokenRepository.save(token); - } - private User saveGoogleUser(GoogleUserResponse response) { String imageUrl = getGoogleUserImage(response); UserImage savedUserImage = userImageService.saveUserImage(imageUrl); @@ -253,8 +291,8 @@ private User saveKakaoUser(KakaoUserResponse response) { return userRepository.save(user); } - private String getKakaoUserImage(KakaoUserResponse response) { - String gender = response.getKakaoAccount().getGender(); + + private String getDefaultUserImage(String gender) { if (Objects.equals(gender, "male")) { return USER_PROFILE_MALE; } @@ -268,7 +306,8 @@ private String getAuthLink(String type, String redirectUrl) { return switch (type) { case "kakao" -> kakaoProvider.generateAuthUrl(redirectUrl); case "google" -> googleProvider.generateAuthUrl(redirectUrl); - default -> throw new RuntimeException("지원하지 않는 oauth 타입입니다."); + case "naver" -> naverProvider.generateAuthUrl(redirectUrl); + default -> throw new UnsupportedLoginTypeException("지원하지 않는 oauth 로그인입니다."); }; } @@ -298,19 +337,18 @@ private void deleteCookie(String name, String value, HttpServletResponse respons response.addCookie(cookie); } + public void revokeToken(String type, String token) throws IOException { HttpStatusCode responseCode; switch (type) { case "kakao" -> responseCode = kakaoConnector.requestRevoke(token); case "google" -> responseCode = googleConnector.requestRevoke(token); - default -> throw new RuntimeException("Unsupported oauth type"); + case "naver" -> responseCode = naverConnector.requestRevoke(token); + default -> throw new UnsupportedLoginTypeException("지원하지 않는 oauth 로그인입니다."); } - if (responseCode.is2xxSuccessful()) { - System.out.println("Token successfully revoked"); - } else { - System.out.println("Failed to revoke token, response code: " + responseCode); - throw new RuntimeException("Failed to revoke token"); + if (!responseCode.is2xxSuccessful()) { + throw new RevokeFailException("회원탈퇴에 실패하였습니다."); } } diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/NaverConnector.java b/src/main/java/solitour_backend/solitour/auth/support/naver/NaverConnector.java new file mode 100644 index 0000000..5d47e7b --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/NaverConnector.java @@ -0,0 +1,118 @@ +package solitour_backend.solitour.auth.support.naver; + + +import java.io.IOException; +import java.util.Collections; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoUserResponse; +import solitour_backend.solitour.auth.support.naver.dto.NaverTokenAndUserResponse; +import solitour_backend.solitour.auth.support.naver.dto.NaverTokenResponse; +import solitour_backend.solitour.auth.support.naver.dto.NaverUserResponse; + +@Getter +@RequiredArgsConstructor +@Component +public class NaverConnector { + + private static final String BEARER_TYPE = "Bearer"; + private static final RestTemplate REST_TEMPLATE = new RestTemplate(); + + private final NaverProvider provider; + + public NaverTokenAndUserResponse requestNaverUserInfo(String code) { + NaverTokenResponse naverTokenResponse = requestAccessToken(code); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", String.join(" ", BEARER_TYPE, naverTokenResponse.getAccessToken())); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity responseEntity = REST_TEMPLATE.exchange(provider.getUserInfoUrl(), + HttpMethod.GET, entity, + NaverUserResponse.class); + + return new NaverTokenAndUserResponse(naverTokenResponse, responseEntity.getBody()); + + } + + public NaverTokenResponse requestAccessToken(String code) { + HttpEntity> entity = new HttpEntity<>( + createBody(code), createHeaders()); + + return REST_TEMPLATE.postForEntity( + provider.getAccessTokenUrl(), + entity, + NaverTokenResponse.class).getBody(); + } + + public String refreshToken(String refreshToken) { + HttpEntity> entity = new HttpEntity<>( + createRefreshBody(refreshToken), createHeaders()); + + return REST_TEMPLATE.postForEntity( + provider.getAccessTokenUrl(), + entity, NaverTokenResponse.class).getBody().getAccessToken(); + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + return headers; + } + + private MultiValueMap createBody(String code) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("code", code); + body.add("grant_type", provider.getGrantType()); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("state", UUID.randomUUID().toString()); + return body; + } + + private MultiValueMap createRefreshBody(String refreshToken) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", provider.getRefreshGrantType()); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("refresh_token", refreshToken); + return body; + } + + public HttpStatusCode requestRevoke(String token) throws IOException { + HttpEntity> entity = new HttpEntity<>(createRevokeBody(token),createRevokeHeaders(token)); + + ResponseEntity response = REST_TEMPLATE.postForEntity(provider.getAccessTokenUrl(), entity, String.class); + + return response.getStatusCode(); + } + + private MultiValueMap createRevokeBody(String accessToken) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("grant_type", provider.getRevokeGrantType()); + body.add("access_token", accessToken); + body.add("service_provider", provider.getServiceProvider()); + return body; + } + + private HttpHeaders createRevokeHeaders(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", String.join(" ", BEARER_TYPE, token)); + return headers; + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/NaverProvider.java b/src/main/java/solitour_backend/solitour/auth/support/naver/NaverProvider.java new file mode 100644 index 0000000..3b2e3f8 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/NaverProvider.java @@ -0,0 +1,79 @@ +package solitour_backend.solitour.auth.support.naver; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import solitour_backend.solitour.auth.support.naver.dto.NaverTokenResponse; + +@Getter +@Component +public class NaverProvider { + + private final String clientId; + private final String clientSecret; + private final String authUrl; + private final String accessTokenUrl; + private final String userInfoUrl; + private final String grantType; + private final String refreshGrantType; + private final String revokeGrantType; + private final String serviceProvider; + private final String state = UUID.randomUUID().toString(); + + + public NaverProvider(@Value("${oauth2.naver.client.id}") String clientId, + @Value("${oauth2.naver.client.secret}") String clientSecret, + @Value("${oauth2.naver.url.auth}") String authUrl, + @Value("${oauth2.naver.url.token}") String accessTokenUrl, + @Value("${oauth2.naver.url.userinfo}") String userInfoUrl, + @Value("${oauth2.naver.service-provider}") String serviceProvider, + @Value("${oauth2.naver.grant-type}") String grantType, + @Value("${oauth2.naver.refresh-grant-type}") String refreshGrantType, + @Value("${oauth2.naver.revoke-grant-type}") String revokeGrantType) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.authUrl = authUrl; + this.accessTokenUrl = accessTokenUrl; + this.userInfoUrl = userInfoUrl; + this.serviceProvider = serviceProvider; + this.grantType = grantType; + this.refreshGrantType = refreshGrantType; + this.revokeGrantType = revokeGrantType; + } + + public String generateTokenUrl(String grantType, String code) { + Map params = new HashMap<>(); + params.put("grant_type", grantType); + params.put("client_id", clientId); + params.put("client_secret", clientSecret); + params.put("code", code); + params.put("state", state); + return authUrl + "?" + concatParams(params); + } + + public String generateAuthUrl(String redirectUrl) { + Map params = new HashMap<>(); + params.put("response_type", "code"); + params.put("client_id", clientId); + params.put("redirect_uri", redirectUrl); + params.put("state", state); + return authUrl + "?" + concatParams(params); + } + + private String concatParams(Map params) { + return params.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } + + public String generateAccessTokenUrl(String code) { + return generateTokenUrl("authorization_code", code); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenAndUserResponse.java b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenAndUserResponse.java new file mode 100644 index 0000000..fcdee93 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenAndUserResponse.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.auth.support.naver.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NaverTokenAndUserResponse { + + private NaverTokenResponse naverTokenResponse; + private NaverUserResponse naverUserResponse; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenResponse.java b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenResponse.java new file mode 100644 index 0000000..ccb38fb --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenResponse.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.auth.support.naver.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NaverTokenResponse { + + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverUserResponse.java b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverUserResponse.java new file mode 100644 index 0000000..6b4329d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverUserResponse.java @@ -0,0 +1,36 @@ +package solitour_backend.solitour.auth.support.naver.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; +import java.util.HashMap; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoUserResponse.Partner; + +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class NaverUserResponse { + private String resultcode; + private String message; + private Response response; + + @Getter + @NoArgsConstructor + public static class Response { + private String id; + private String email; + private String name; + private String nickname; + private String gender; + private String age; + private String birthday; + private String birthyear; + private String mobile; + private String profileImage; + } + +} diff --git a/src/main/java/solitour_backend/solitour/error/GlobalControllerAdvice.java b/src/main/java/solitour_backend/solitour/error/GlobalControllerAdvice.java index d9f9bdf..a877ef3 100644 --- a/src/main/java/solitour_backend/solitour/error/GlobalControllerAdvice.java +++ b/src/main/java/solitour_backend/solitour/error/GlobalControllerAdvice.java @@ -5,6 +5,8 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import solitour_backend.solitour.auth.exception.TokenNotExistsException; +import solitour_backend.solitour.auth.exception.UnsupportedLoginTypeException; +import solitour_backend.solitour.auth.exception.UserRevokeErrorException; import solitour_backend.solitour.book_mark_gathering.exception.GatheringBookMarkNotExistsException; import solitour_backend.solitour.book_mark_information.exception.InformationBookMarkNotExistsException; import solitour_backend.solitour.category.exception.CategoryNotExistsException; @@ -40,7 +42,8 @@ public class GlobalControllerAdvice { RequestValidationFailedException.class, ImageRequestValidationFailedException.class, GatheringApplicantsManagerException.class, - InformationNotManageException.class + InformationNotManageException.class, + UnsupportedLoginTypeException.class }) public ResponseEntity validationException(Exception exception) { return ResponseEntity @@ -111,5 +114,13 @@ public ResponseEntity unauthorizedException(Exception exception) { .status(HttpStatus.UNAUTHORIZED) .body(exception.getMessage()); } + @ExceptionHandler({ + UserRevokeErrorException.class + }) + public ResponseEntity serverErrorException(Exception exception) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(exception.getMessage()); + } }