Skip to content

Commit

Permalink
Feat : 네이버 oauth (#211)
Browse files Browse the repository at this point in the history
회원가입, 로그인, 로그아웃, 회원탈퇴
  • Loading branch information
Astin01 authored Sep 25, 2024
1 parent 65bcb22 commit 1a5dfb8
Show file tree
Hide file tree
Showing 11 changed files with 369 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ public ResponseEntity<Void> reissueAccessToken(HttpServletResponse response,

@Authenticated
@DeleteMapping()
public ResponseEntity<String> deleteUser(HttpServletResponse response, @AuthenticationPrincipal Long id,
@RequestParam String type) {
public ResponseEntity<Void> deleteUser(HttpServletResponse response, @AuthenticationPrincipal Long id,
@RequestParam String type) {
Token token = tokenRepository.findByUserId(id)
.orElseThrow(() -> new TokenNotExistsException("토큰이 존재하지 않습니다"));
String oauthRefreshToken = getOauthAccessToken(type, token.getOauthToken());
Expand All @@ -110,11 +110,11 @@ public ResponseEntity<String> 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) {
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package solitour_backend.solitour.auth.exception;

public class RevokeFailException extends RuntimeException {
public RevokeFailException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package solitour_backend.solitour.auth.exception;

public class UnsupportedLoginTypeException extends RuntimeException {
public UnsupportedLoginTypeException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package solitour_backend.solitour.auth.exception;

public class UserRevokeErrorException extends RuntimeException {
public UserRevokeErrorException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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";
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 로그인입니다.");
};
}

Expand Down Expand Up @@ -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("회원탈퇴에 실패하였습니다.");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Void> entity = new HttpEntity<>(headers);

ResponseEntity<NaverUserResponse> responseEntity = REST_TEMPLATE.exchange(provider.getUserInfoUrl(),
HttpMethod.GET, entity,
NaverUserResponse.class);

return new NaverTokenAndUserResponse(naverTokenResponse, responseEntity.getBody());

}

public NaverTokenResponse requestAccessToken(String code) {
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(
createBody(code), createHeaders());

return REST_TEMPLATE.postForEntity(
provider.getAccessTokenUrl(),
entity,
NaverTokenResponse.class).getBody();
}

public String refreshToken(String refreshToken) {
HttpEntity<MultiValueMap<String, String>> 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<String, String> createBody(String code) {
MultiValueMap<String, String> 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<String, String> createRefreshBody(String refreshToken) {
MultiValueMap<String, String> 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<MultiValueMap<String, String>> entity = new HttpEntity<>(createRevokeBody(token),createRevokeHeaders(token));

ResponseEntity<String> response = REST_TEMPLATE.postForEntity(provider.getAccessTokenUrl(), entity, String.class);

return response.getStatusCode();
}

private MultiValueMap<String, String> createRevokeBody(String accessToken) {
MultiValueMap<String, String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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<String, String> params) {
return params.entrySet()
.stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
}

public String generateAccessTokenUrl(String code) {
return generateTokenUrl("authorization_code", code);
}
}
Loading

0 comments on commit 1a5dfb8

Please sign in to comment.