From b51c5b123fa6682199c3b902d52d5b57c4e6ad90 Mon Sep 17 00:00:00 2001 From: YoungJun Park Date: Sun, 14 Nov 2021 23:51:48 +0900 Subject: [PATCH] =?UTF-8?q?[#4]=20=E2=9C=8D=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lazy/api/controller/AuthController.java | 51 ++++++------ .../domain/oauth/kakao/KakaoProfileDto.java | 22 ++++- .../lazy/api/external/kakao/KakaoClient.java | 43 ++++++++++ .../lazy/api/service/AuthService.java | 83 +++++++++++++++++++ 4 files changed, 169 insertions(+), 30 deletions(-) create mode 100644 api/src/main/java/com/teamnexters/lazy/api/external/kakao/KakaoClient.java create mode 100644 api/src/main/java/com/teamnexters/lazy/api/service/AuthService.java diff --git a/api/src/main/java/com/teamnexters/lazy/api/controller/AuthController.java b/api/src/main/java/com/teamnexters/lazy/api/controller/AuthController.java index 9e78d12..6690106 100644 --- a/api/src/main/java/com/teamnexters/lazy/api/controller/AuthController.java +++ b/api/src/main/java/com/teamnexters/lazy/api/controller/AuthController.java @@ -1,29 +1,27 @@ package com.teamnexters.lazy.api.controller; +import com.teamnexters.lazy.api.config.auth.jwt.Token; +import com.teamnexters.lazy.api.service.AuthService; +import com.teamnexters.lazy.common.error.ErrorResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; @Tag(name = "Auth Controller", description = "소셜 로그인 관련 컨트롤러") @Slf4j +@RequiredArgsConstructor @Controller public class AuthController { - @Value("${spring.security.oauth2.client.registration.kakao.auth-url}") - private String kakaoAuthUrl; // Auth URL - - @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") - private String kakaoRedirectUri; // Redirect URI - - @Value("${spring.security.oauth2.client.registration.kakao.client-id}") - private String kakaoRestApiKey; // REST API Key - - @Value("${spring.security.oauth2.client.registration.kakao.scope}") - private String kakaoScope; // 추가 동의 항목 + private final AuthService authService; @Operation(summary = "✅ 카카오 OAuth 랜딩 API", description = "카카오 OAuth 페이지를 랜딩해요.", @@ -32,21 +30,22 @@ public class AuthController { responseCode = "200", description = "[Ok] Kakao Login Page Landing")}) @GetMapping("api/v1/oauth/kakao") public String kakaoLoginLanding() { - StringBuffer url = new StringBuffer(); - url.append(kakaoAuthUrl) - .append("/oauth/authorize?") - .append("client_id=") - .append(kakaoRestApiKey) - .append("&redirect_uri=") - .append(kakaoRedirectUri) - .append("&scope=") - .append(kakaoScope) - .append("&response_type=") - .append("code"); // code 고정 - - log.info("Kakao Login Landing URL : {}", url); + String kakaoLandingUrl = authService.makeKakaoLandingUrl(); + return "redirect:" + kakaoLandingUrl; + } - return "redirect:" + url; + @Operation(summary = "✅ 카카오 액세스 토큰으로 App JWT 생성", + description = "카카오 액세스 토큰으로 App Server JWT 얻어와요.", + responses = { + @ApiResponse( + responseCode = "200", description = "[Ok] JWT Create By Kakao Token", + content = @Content(schema = @Schema(implementation = Token.class))), + @ApiResponse( + responseCode = "408", description = "[Error] Token is not valid", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)))}) + @GetMapping(value = "api/v1/token/{kakao-token}") + public ResponseEntity kakaoAuthRequest(@PathVariable(name="kakao-token") String kakaoToken) { + return ResponseEntity.ok().body(authService.getTokenAndSaveMemberAfterKakaoUserApi(kakaoToken)); } } diff --git a/api/src/main/java/com/teamnexters/lazy/api/domain/oauth/kakao/KakaoProfileDto.java b/api/src/main/java/com/teamnexters/lazy/api/domain/oauth/kakao/KakaoProfileDto.java index ed13451..397a10d 100644 --- a/api/src/main/java/com/teamnexters/lazy/api/domain/oauth/kakao/KakaoProfileDto.java +++ b/api/src/main/java/com/teamnexters/lazy/api/domain/oauth/kakao/KakaoProfileDto.java @@ -13,15 +13,29 @@ public class KakaoProfileDto { private Long id; private Properties properties; + @JsonProperty("kakao_account") + private Account account; + @Getter @Setter @ToString - private static class Properties { + public static class Properties { @JsonProperty("nickname") - private String nickname; + public String nickname; @JsonProperty("thumbnail_image") - private String thumbnailImage; + public String thumbnailImage; @JsonProperty("profile_image") - private String profileImage; + public String profileImage; + } + + /** + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + */ + @Getter + @Setter + @ToString + public static class Account { + @JsonProperty("email") + public String email; } } \ No newline at end of file diff --git a/api/src/main/java/com/teamnexters/lazy/api/external/kakao/KakaoClient.java b/api/src/main/java/com/teamnexters/lazy/api/external/kakao/KakaoClient.java new file mode 100644 index 0000000..4d2fbf5 --- /dev/null +++ b/api/src/main/java/com/teamnexters/lazy/api/external/kakao/KakaoClient.java @@ -0,0 +1,43 @@ +package com.teamnexters.lazy.api.external.kakao; + +import com.teamnexters.lazy.api.domain.oauth.kakao.KakaoProfileDto; +import com.teamnexters.lazy.api.exception.ExternalApiException; +import com.teamnexters.lazy.api.exception.TokenValidException; +import com.teamnexters.lazy.common.domain.member.Member; +import com.teamnexters.lazy.common.domain.member.Provider; +import com.teamnexters.lazy.common.domain.member.Role; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Component +@RequiredArgsConstructor +public class KakaoClient { + + private final WebClient webClient; + + // Kakao User 정보 받아오는 URI + private static final String KAKAO_USER_INFO_URI = "https://kapi.kakao.com/v2/user/me"; + + public Member getUserInfoByKakaoApi(String accessToken) { + KakaoProfileDto kakaoUserResponse = webClient.get() + .uri(KAKAO_USER_INFO_URI) + .headers(h -> h.setBearerAuth(accessToken)) // Bearer 설정 + .retrieve() + .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new TokenValidException())) + .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new ExternalApiException("GET : " + KAKAO_USER_INFO_URI))) + .bodyToMono(KakaoProfileDto.class) + .block(); + + return Member.builder() + .oauthId(String.valueOf(kakaoUserResponse.getId())) + .name(kakaoUserResponse.getProperties().getNickname()) + .email(kakaoUserResponse.getAccount().getEmail()) + .provider(Provider.KAKAO) + .role(Role.USER) + .picture(kakaoUserResponse.getProperties().getProfileImage()) + .build(); + } +} diff --git a/api/src/main/java/com/teamnexters/lazy/api/service/AuthService.java b/api/src/main/java/com/teamnexters/lazy/api/service/AuthService.java new file mode 100644 index 0000000..6f70c63 --- /dev/null +++ b/api/src/main/java/com/teamnexters/lazy/api/service/AuthService.java @@ -0,0 +1,83 @@ +package com.teamnexters.lazy.api.service; + +import com.teamnexters.lazy.api.config.auth.jwt.JwtTokenProvider; +import com.teamnexters.lazy.api.config.auth.jwt.Token; +import com.teamnexters.lazy.api.external.kakao.KakaoClient; +import com.teamnexters.lazy.common.domain.member.Member; +import com.teamnexters.lazy.common.domain.member.MemberRepository; +import com.teamnexters.lazy.common.domain.member.Provider; +import com.teamnexters.lazy.common.domain.member.Role; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@PropertySource("classpath:application-oauth.yml") +public class AuthService { + + @Value("${spring.security.oauth2.client.registration.kakao.auth-url}") + private String kakaoAuthUrl; // Auth URL + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String kakaoRedirectUri; // Redirect URI + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String kakaoRestApiKey; // REST API Key + + @Value("${spring.security.oauth2.client.registration.kakao.scope}") + private String kakaoScope; // 추가 동의 항목 + + private final KakaoClient kakaoClient; + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + /** + * Kakao landing URL 생성 + * @return Kakao landing URL + */ + public String makeKakaoLandingUrl() { + StringBuffer url = new StringBuffer(); + url.append(kakaoAuthUrl) + .append("/oauth/authorize?") + .append("client_id=") + .append(kakaoRestApiKey) + .append("&redirect_uri=") + .append(kakaoRedirectUri) + .append("&scope=") + .append(kakaoScope) + .append("&response_type=") + .append("code"); // code 고정 + + log.info("Kakao Login Landing URL : {}", url); + + return url.toString(); + } + + /** + * 카카오 API 통해 회원 정보 가져와서 생성 후 Token 발행 + * + * @param kakaoToken Kakao Token + * @return App Server Token + */ + @Transactional + public Token getTokenAndSaveMemberAfterKakaoUserApi(String kakaoToken) { + // Kakao External Client API 호출 + Member kakaoUserInfo = kakaoClient.getUserInfoByKakaoApi(kakaoToken); + String oauthId = kakaoUserInfo.getOauthId(); + // Member + Member member = memberRepository.findByOauthIdAndProvider(oauthId, Provider.KAKAO).orElseGet( + () -> memberRepository.save(kakaoUserInfo) + ); + + // Token 생성 + Token token = jwtTokenProvider.createToken(member.getEmail(), Role.USER); + log.info("### Token : [ {} ]", token); + return token; + } + +}