From cb17678e368947a3475cd8abf7a982127d3cdbfe Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 01:56:25 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20AppleAuthClient=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공개키 조회, 토큰 발급을 API 호출을 위한 클라이언트 --- .../feignClient/client/AppleAuthClient.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/com/onnoff/onnoff/auth/feignClient/client/AppleAuthClient.java diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/client/AppleAuthClient.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/client/AppleAuthClient.java new file mode 100644 index 0000000..58d30c3 --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/client/AppleAuthClient.java @@ -0,0 +1,21 @@ +package com.onnoff.onnoff.auth.feignClient.client; + + +import com.onnoff.onnoff.auth.feignClient.dto.JwkResponse; +import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient(name = "apple-auth-client",url = "https://appleid.apple.com/auth") +public interface AppleAuthClient{ + @GetMapping("/keys") + JwkResponse.JwkSet getKeys(); + + @GetMapping(value = "/token", consumes = "application/x-www-form-urlencoded") + TokenResponse getToken(MultiValueMap requestBody); + + //회원 탈퇴 메서드 +// @GetMapping("/revoke") +// KakaoOauth2DTO.TokenValidateResponseDTO getTokenValidate(@RequestHeader("Authorization") String accessToken); +} From 6352c086bfcf5fc31045cfe68b08d9bf50cbee98 Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 01:58:21 +0900 Subject: [PATCH 02/14] =?UTF-8?q?refactor:=20client=20dto=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20kakao,=20apple=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/feignClient/dto/KakaoOauth2DTO.java | 55 ------------------- .../dto/{ => kakao}/ErrorResponseDTO.java | 2 +- .../feignClient/dto/kakao/KakaoOauth2DTO.java | 26 +++++++++ 3 files changed, 27 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/com/onnoff/onnoff/auth/feignClient/dto/KakaoOauth2DTO.java rename src/main/java/com/onnoff/onnoff/auth/feignClient/dto/{ => kakao}/ErrorResponseDTO.java (67%) create mode 100644 src/main/java/com/onnoff/onnoff/auth/feignClient/dto/kakao/KakaoOauth2DTO.java diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/KakaoOauth2DTO.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/KakaoOauth2DTO.java deleted file mode 100644 index e9214a7..0000000 --- a/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/KakaoOauth2DTO.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.onnoff.onnoff.auth.feignClient.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.onnoff.onnoff.domain.user.enums.SocialType; -import lombok.Getter; -import lombok.ToString; - -import java.time.LocalDateTime; - -public class KakaoOauth2DTO { - - @Getter - public static class TokenResponseDTO{ - @JsonProperty("token_type") - private String tokenType; - @JsonProperty("access_token") - private String accessToken; - } - - @Getter - public static class TokenValidateResponseDTO{ - private Long id; - @JsonProperty("expires_in") - private Integer expiresIn; - @JsonProperty("app_id") - private Integer appId; - } - - @Getter - @ToString - public static class UserInfoResponseDTO{ - private Long id; - @JsonProperty("connected_at") - private LocalDateTime connectedAt; - @JsonProperty("kakao_account") - private KakaoAccountDTO kakaoAccount; - private SocialType socialType; - } - @Getter - @ToString - public static class KakaoAccountDTO{ - // 이메일 동의항목 -// private boolean has_email; -// private boolean email_needs_agreement; -// private boolean is_email_valid; -// private boolean is_email_verified; - private String email; - - // 이름 동의 항목 -// private boolean name_needs_agreement; - private String name; - - - } -} diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/ErrorResponseDTO.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/kakao/ErrorResponseDTO.java similarity index 67% rename from src/main/java/com/onnoff/onnoff/auth/feignClient/dto/ErrorResponseDTO.java rename to src/main/java/com/onnoff/onnoff/auth/feignClient/dto/kakao/ErrorResponseDTO.java index 562134d..7d8acea 100644 --- a/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/ErrorResponseDTO.java +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/kakao/ErrorResponseDTO.java @@ -1,4 +1,4 @@ -package com.onnoff.onnoff.auth.feignClient.dto; +package com.onnoff.onnoff.auth.feignClient.dto.kakao; import lombok.Getter; diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/kakao/KakaoOauth2DTO.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/kakao/KakaoOauth2DTO.java new file mode 100644 index 0000000..20186ce --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/kakao/KakaoOauth2DTO.java @@ -0,0 +1,26 @@ +package com.onnoff.onnoff.auth.feignClient.dto.kakao; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + + +public class KakaoOauth2DTO { + + @Getter + public static class TokenValidateResponseDTO{ + private Long id; + @JsonProperty("expires_in") + private Integer expiresIn; + @JsonProperty("app_id") + private Integer appId; + } + + @Getter + @ToString + public static class UserInfoResponseDTO{ + private String sub; + private String name; + private String email; + } +} From e77503b8dfc06015c8dc206e4eb33629f6b61215 Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 01:58:47 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20=EA=B3=B5=EA=B0=9C=ED=82=A4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20DTO=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/feignClient/dto/JwkResponse.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/com/onnoff/onnoff/auth/feignClient/dto/JwkResponse.java diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/JwkResponse.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/JwkResponse.java new file mode 100644 index 0000000..f06353c --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/JwkResponse.java @@ -0,0 +1,30 @@ +package com.onnoff.onnoff.auth.feignClient.dto; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +public class JwkResponse { + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class Jwk { + private String alg; + private String e; + private String kid; + private String kty; + private String n; + private String use; + } + @Getter + public static class JwkSet{ + @JsonProperty("keys") + private List jwkList; + } +} From 0f6e28c8c7a29431eb090fe05e6e30183b0dfe9f Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 02:03:55 +0900 Subject: [PATCH 04/14] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=95=88?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9D=91=EB=8B=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onnoff/onnoff/apiPayload/code/status/SuccessStatus.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/onnoff/onnoff/apiPayload/code/status/SuccessStatus.java b/src/main/java/com/onnoff/onnoff/apiPayload/code/status/SuccessStatus.java index 2e768ee..82afe51 100644 --- a/src/main/java/com/onnoff/onnoff/apiPayload/code/status/SuccessStatus.java +++ b/src/main/java/com/onnoff/onnoff/apiPayload/code/status/SuccessStatus.java @@ -10,10 +10,8 @@ @AllArgsConstructor public enum SuccessStatus implements BaseCode { // 일반적인 응답 - _OK(HttpStatus.OK, "COMMON200", "성공입니다."), + _OK(HttpStatus.OK, "COMMON200", "성공입니다."); - // 로그인 응답 - NEED_USER_DETAIL(HttpStatus.OK, "LOGIN200", "토큰이 유효하고 유저의 추가정보가 필요합니다."); private final HttpStatus httpStatus; private final String code; private final String message; From d6fbedb12793b6c19ae356ad69a664e30197e6e1 Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 02:09:23 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feignClient/client/KakaoApiClient.java | 9 +++--- .../auth/feignClient/dto/TokenResponse.java | 18 +++++++++++ .../feignClient/dto/apple/TokenRequest.java | 32 +++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/onnoff/onnoff/auth/feignClient/dto/TokenResponse.java create mode 100644 src/main/java/com/onnoff/onnoff/auth/feignClient/dto/apple/TokenRequest.java diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/client/KakaoApiClient.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/client/KakaoApiClient.java index f837d51..7a5a304 100644 --- a/src/main/java/com/onnoff/onnoff/auth/feignClient/client/KakaoApiClient.java +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/client/KakaoApiClient.java @@ -1,20 +1,19 @@ package com.onnoff.onnoff.auth.feignClient.client; import com.onnoff.onnoff.auth.feignClient.config.FeignConfig; -import com.onnoff.onnoff.auth.feignClient.dto.KakaoOauth2DTO; -import feign.Headers; +import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; /* - 토큰 유효성 검증하고 사용자 정보 가져오는 client + 토큰 유효성 검증 하고 사용자 정보 가져오는 client */ @FeignClient(name = "kakao-api-client", url = "https://kapi.kakao.com", configuration = FeignConfig.class) public interface KakaoApiClient { @GetMapping("v1/user/access_token_info") KakaoOauth2DTO.TokenValidateResponseDTO getTokenValidate(@RequestHeader("Authorization") String accessToken); - @GetMapping(value = "/v2/user/me") - KakaoOauth2DTO.UserInfoResponseDTO getUserInfo(@RequestHeader("Authorization") String accessToken, @RequestParam(name = "property_keys") String propertyKeys); + @GetMapping(value = "/v1/oidc/userinfo") + KakaoOauth2DTO.UserInfoResponseDTO getUserInfo(@RequestHeader("Authorization") String accessToken); } diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/TokenResponse.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/TokenResponse.java new file mode 100644 index 0000000..cdb336f --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/TokenResponse.java @@ -0,0 +1,18 @@ +package com.onnoff.onnoff.auth.feignClient.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public class TokenResponse { + @JsonProperty("access_token") + private String accessToken; + @JsonProperty("expires_in") + private Integer expiresIn; + @JsonProperty("id_token") + private String idToken; + @JsonProperty("refresh_token") + private String refreshToken; + @JsonProperty("token_type") + private String tokenType; +} diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/apple/TokenRequest.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/apple/TokenRequest.java new file mode 100644 index 0000000..3acbec7 --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/dto/apple/TokenRequest.java @@ -0,0 +1,32 @@ +package com.onnoff.onnoff.auth.feignClient.dto.apple; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@Builder +@AllArgsConstructor +public class TokenRequest { + @JsonProperty("client_id") + private String clientId; + @JsonProperty("client_secret") + private String clientSecret; + private String code; + @JsonProperty("grant_type") + private String grantType; + @JsonProperty("redirect_uri") + private String redirectUri; + @JsonProperty("refresh_token") + private String refreshToken; + public MultiValueMap toUrlEncoded(){ + LinkedMultiValueMap urlEncoded = new LinkedMultiValueMap<>(); + urlEncoded.add("client_id", clientId); + urlEncoded.add("client_secret", clientSecret); + urlEncoded.add("code", code); + urlEncoded.add("grant_type", grantType); + urlEncoded.add("redirect_uri", redirectUri); + return urlEncoded; + } +} From aba3bd02794ae736c33a5c9d163e47a10731f737 Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 02:11:48 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=ED=82=A4=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=B6=94=EA=B0=80,=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=ED=86=A0=ED=81=B0=20=EC=9D=91=EB=8B=B5=20DTO=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feignClient/client/KakaoOauth2Client.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/client/KakaoOauth2Client.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/client/KakaoOauth2Client.java index 6eed05e..577a305 100644 --- a/src/main/java/com/onnoff/onnoff/auth/feignClient/client/KakaoOauth2Client.java +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/client/KakaoOauth2Client.java @@ -2,7 +2,9 @@ import com.onnoff.onnoff.auth.feignClient.config.FeignConfig; -import com.onnoff.onnoff.auth.feignClient.dto.KakaoOauth2DTO; +import com.onnoff.onnoff.auth.feignClient.dto.JwkResponse; +import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse; +import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO; import feign.Headers; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; @@ -11,10 +13,11 @@ public interface KakaoOauth2Client { @Headers("Content-Type: application/x-www-form-urlencoded") @PostMapping("/oauth/token") - KakaoOauth2DTO.TokenResponseDTO getAccessToken(@RequestParam(name = "grant_type") String grantType, - @RequestParam(name = "client_id") String clientId, - @RequestParam(name = "redirect_uri") String redirectUri, - @RequestParam(name = "code") String code + TokenResponse getAccessToken(@RequestParam(name = "grant_type") String grantType, + @RequestParam(name = "client_id") String clientId, + @RequestParam(name = "redirect_uri") String redirectUri, + @RequestParam(name = "code") String code ); - + @GetMapping("/.well-known/jwks.json") + JwkResponse.JwkSet getKeys(); } From 15f45e7395b1da1b55d62e24f506b886cfd2f56e Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 10:27:08 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EB=B6=84=EB=A6=AC,=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20validator=EB=A1=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/KakaoLoginService.java | 49 ++++++++++++++ .../onnoff/auth/service/LoginService.java | 66 +++---------------- .../tokenValidator/SocialTokenValidator.java | 10 +++ 3 files changed, 67 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/onnoff/onnoff/auth/service/KakaoLoginService.java create mode 100644 src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidator.java diff --git a/src/main/java/com/onnoff/onnoff/auth/service/KakaoLoginService.java b/src/main/java/com/onnoff/onnoff/auth/service/KakaoLoginService.java new file mode 100644 index 0000000..c151531 --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/service/KakaoLoginService.java @@ -0,0 +1,49 @@ +package com.onnoff.onnoff.auth.service; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.onnoff.onnoff.auth.feignClient.client.KakaoApiClient; +import com.onnoff.onnoff.auth.feignClient.client.KakaoOauth2Client; +import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse; +import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO; +import com.onnoff.onnoff.auth.service.tokenValidator.SocialTokenValidator; +import com.onnoff.onnoff.domain.user.enums.SocialType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +@Slf4j +public class KakaoLoginService implements LoginService{ + private final KakaoOauth2Client kakaoOauth2Client; + private final KakaoApiClient kakaoApiClient; + private final SocialTokenValidator validator; + + /* + 테스트 용으로 만든거, 실제로는 프론트에서 처리해서 액세스 토큰만 가져다 줌 + */ + @Override + public TokenResponse getAccessTokenByCode(String code){ + return kakaoOauth2Client.getAccessToken("authorization_code", + "32c0787d1b1e9fcabcc24af247903ba8", + "http://localhost:8080/oauth2/login/kakao", + code); + } + // id 토큰 유효성 검증 + @Override + public void validate(String idToken){ + String cleanedAccessToken = cleanToken(idToken); + validator.validate(cleanedAccessToken, SocialType.KAKAO); + } + /* + 토큰으로 유저정보를 가져오는 메서드 + */ + public KakaoOauth2DTO.UserInfoResponseDTO getUserInfo(String accessToken) throws JsonProcessingException { + String cleanedAccessToken = cleanToken(accessToken); + accessToken = "bearer " + cleanedAccessToken; + KakaoOauth2DTO.UserInfoResponseDTO userInfo = kakaoApiClient.getUserInfo(accessToken); + return userInfo; + } +} diff --git a/src/main/java/com/onnoff/onnoff/auth/service/LoginService.java b/src/main/java/com/onnoff/onnoff/auth/service/LoginService.java index 6edfc1a..4a5691c 100644 --- a/src/main/java/com/onnoff/onnoff/auth/service/LoginService.java +++ b/src/main/java/com/onnoff/onnoff/auth/service/LoginService.java @@ -1,63 +1,13 @@ package com.onnoff.onnoff.auth.service; +import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.onnoff.onnoff.auth.feignClient.client.KakaoApiClient; -import com.onnoff.onnoff.auth.feignClient.client.KakaoOauth2Client; -import com.onnoff.onnoff.auth.feignClient.dto.KakaoOauth2DTO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Arrays; - -@Service -@RequiredArgsConstructor -@Slf4j -public class LoginService { - private final KakaoOauth2Client kakaoOauth2Client; - private final KakaoApiClient kakaoApiClient; - - /* - 테스트 용으로 만든거, 실제로는 프론트에서 처리해서 액세스 토큰만 가져다 줌 - */ - public String getAccessToken(String code){ - KakaoOauth2DTO.TokenResponseDTO tokenResponseDTO = kakaoOauth2Client.getAccessToken("authorization_code", - "32c0787d1b1e9fcabcc24af247903ba8", - "http://localhost:8080/oauth2/login/kakao", - code); - return tokenResponseDTO.getAccessToken(); - } - /* - 토큰 유효성 검증, 유효하지 않으면 예외를 발생시키도록 처리, 예외는 CustomErrorDecoder에서 처리 - */ - public void validate(String accessToken){ - String cleanedAccessToken = cleanAccessToken(accessToken); - kakaoApiClient.getTokenValidate(cleanedAccessToken); - } - - private String cleanAccessToken(String accessToken){ - accessToken = accessToken.replaceAll("[\u0000-\u001F\u007F-\u00FF:]", ""); // 헤더에 있으면 안되는 값 대체 - accessToken = accessToken.trim(); // 앞뒤 공백 제거 - accessToken = "Bearer " + accessToken; // 토큰 기반 인증 형식 - return accessToken; - } - /* - 토큰으로 유저정보를 가져오는 메서드 - */ - public KakaoOauth2DTO.UserInfoResponseDTO getUserInfo(String accessToken) throws JsonProcessingException { - String cleanedAccessToken = cleanAccessToken(accessToken); - String emailProperty = "kakao_account.email"; - String nameProperty = "kakao_account.name"; - List propertyKeysList = Arrays.asList(emailProperty, nameProperty); - - // List -> JSON 형식으로 바꾸어 전달 - ObjectMapper objectMapper = new ObjectMapper(); - String propertyKeys = objectMapper.writeValueAsString(propertyKeysList); - KakaoOauth2DTO.UserInfoResponseDTO userInfoResponseDTO = kakaoApiClient.getUserInfo(cleanedAccessToken, propertyKeys); - return userInfoResponseDTO; +public interface LoginService { + default String cleanToken(String token){ + token = token.replaceAll("[\u0000-\u001F\u007F-\u00FF:]", ""); // 헤더에 있으면 안되는 값 대체 + token = token.trim(); // 앞뒤 공백 제거 + return token; } + public TokenResponse getAccessTokenByCode(String code); + public void validate(String accessToken); } diff --git a/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidator.java b/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidator.java new file mode 100644 index 0000000..1aef7e8 --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidator.java @@ -0,0 +1,10 @@ +package com.onnoff.onnoff.auth.service.tokenValidator; + +import com.onnoff.onnoff.domain.user.enums.SocialType; + +/** + * 토큰 검증을 처리하는 역할 + */ +public interface SocialTokenValidator { + void validate(String token, SocialType socialType); +} From 4014f51954c6ced4f9d868acc34dc5b5c6e9feb5 Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 10:27:36 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AppleLoginService.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/main/java/com/onnoff/onnoff/auth/service/AppleLoginService.java diff --git a/src/main/java/com/onnoff/onnoff/auth/service/AppleLoginService.java b/src/main/java/com/onnoff/onnoff/auth/service/AppleLoginService.java new file mode 100644 index 0000000..e1af879 --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/service/AppleLoginService.java @@ -0,0 +1,119 @@ +package com.onnoff.onnoff.auth.service; + + +import com.onnoff.onnoff.auth.UserContext; +import com.onnoff.onnoff.auth.feignClient.client.AppleAuthClient; +import com.onnoff.onnoff.auth.feignClient.dto.apple.TokenRequest; +import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse; +import com.onnoff.onnoff.auth.service.tokenValidator.SocialTokenValidator; +import com.onnoff.onnoff.domain.user.User; +import com.onnoff.onnoff.domain.user.enums.SocialType; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.PrivateKey; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class AppleLoginService implements LoginService{ + private final AppleAuthClient appleAuthClient; + private final SocialTokenValidator validator; + @Value("${apple.key.id") + private static String kid; + @Value("${apple.key.path") + private static String keyPath; + @Value("${apple.aud") + private static String aud; + @Value("${apple.iss") + private static String iss; + @Value("${apple.team-id") + private static String teamId; + @Value("${apple.redirect-uri") + private static String redirectUri; + @Override + public TokenResponse getAccessTokenByCode(String code) { + // client secret 만들기 + String clientSecret = createClientSecret(); + // 요청 + MultiValueMap urlEncoded = TokenRequest.builder() + .clientId(aud) + .clientSecret(clientSecret) + .code("authorization_code_value") + .grantType("authorization_code") + .redirectUri(redirectUri) + .build().toUrlEncoded(); + return appleAuthClient.getToken(urlEncoded); + } + private String createClientSecret() { + Date expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant()); + Map jwtHeader = new HashMap<>(); + jwtHeader.put("kid", kid); + jwtHeader.put("alg", "ES256"); + + try { + return Jwts.builder() + .setHeaderParams(jwtHeader) + .setIssuer(teamId) // 토큰 발행자 = 우리 팀 + .setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간 - UNIX 시간 + .setExpiration(expirationDate) // 만료 시간 + .setAudience(iss) // 내가 토큰을 발행하니까 애플을 aud로 반대로 + .setSubject(aud) // 토큰의 주체 = 우리 앱 + .signWith(SignatureAlgorithm.ES256, getPrivateKey()) + .compact(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + public String getAccessTokenByRfToken(String code) { + // client secret 만들기 + String clientSecret = createClientSecret(); + // refreshToken 가져오기 + User user = UserContext.getUser(); + String appleRefreshToken = user.getAppleRefreshToken(); + // 요청 + MultiValueMap urlEncoded = TokenRequest.builder() + .clientId(aud) + .clientSecret(clientSecret) + .refreshToken(appleRefreshToken) + .grantType("refresh_token") + .redirectUri(redirectUri) + .build().toUrlEncoded(); + TokenResponse response = appleAuthClient.getToken(urlEncoded); + return null; + } + private PrivateKey getPrivateKey() throws IOException { + ClassPathResource resource = new ClassPathResource(keyPath); + String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI()))); + + Reader pemReader = new StringReader(privateKey); + PEMParser pemParser = new PEMParser(pemReader); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject(); + return converter.getPrivateKey(object); + } + + + @Override + public void validate(String identityToken){ + String cleanedIdentityToken = cleanToken(identityToken); + validator.validate(cleanedIdentityToken, SocialType.APPLE); + } +} From beaf36d4f47280f89baa1bba5d032ba9c767022f Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 10:31:11 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B3=B5=20=ED=86=A0=ED=81=B0=20=EA=B2=80=EC=A6=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버에서 검증 방법을 accessToken 유효성 검증을 요청하는 방식에서 공개키를 가져와 identityToken 직접 검증하는 방법으로 바꿈 --- .../SocialTokenValidatorImpl.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidatorImpl.java diff --git a/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidatorImpl.java b/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidatorImpl.java new file mode 100644 index 0000000..0b177fe --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidatorImpl.java @@ -0,0 +1,112 @@ +package com.onnoff.onnoff.auth.service.tokenValidator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.onnoff.onnoff.apiPayload.code.status.ErrorStatus; +import com.onnoff.onnoff.apiPayload.exception.GeneralException; +import com.onnoff.onnoff.auth.feignClient.client.AppleAuthClient; +import com.onnoff.onnoff.auth.feignClient.client.KakaoOauth2Client; +import com.onnoff.onnoff.auth.feignClient.dto.JwkResponse; +import com.onnoff.onnoff.domain.user.enums.SocialType; +import io.jsonwebtoken.*; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class SocialTokenValidatorImpl implements SocialTokenValidator{ + private final KakaoOauth2Client kakaoOauth2Client; + private final AppleAuthClient appleAuthClient; + @Value("${kakao.iss}") + private static String kakaoIss; + @Value("${kakao.aud}") + private static String kakaoAud; + @Value("${apple.iss}") + private static String appleIss; + @Value("${apple.aud}") + private static String appleAud; + @Override + public void validate(String token, SocialType socialType) { + JwkResponse.Jwk matchingJwk = getMatchingJwk(token, socialType); + PublicKey publicKey = jwkToPublickey(matchingJwk); + if( socialType.equals(SocialType.KAKAO)){ + verifyToken(token, kakaoIss, kakaoAud, publicKey); + } + else { + verifyToken(token, appleIss, appleAud, publicKey); + } + } + + // 발급자, 수신자, 만료 기한, 시그니처를 검증 + private Jwt verifyToken(String token, String iss, String aud, PublicKey publicKey) { + try { + return (Jwt) Jwts.parser() + .requireAudience(aud) + .requireIssuer(iss) + .verifyWith(publicKey) + .build() + .parse(token); + } + catch (MalformedJwtException | UnsupportedJwtException parseEx){ + throw new GeneralException(ErrorStatus.INVALID_ARGUMENT_ERROR); + } + catch (InvalidClaimException | ExpiredJwtException validateEx){ + throw new GeneralException(ErrorStatus.INVALID_TOKEN_ERROR); + } + } + private JwkResponse.Jwk getMatchingJwk(String token, SocialType socialType){ + // header 부분만 디코드 해서 kid 가져오기 + String keyId = getKeyId(token); + JwkResponse.JwkSet keys; + if( socialType.equals(SocialType.KAKAO)) { + keys = kakaoOauth2Client.getKeys(); + } + else{ + keys = appleAuthClient.getKeys(); + } + return keys.getJwkList().stream() + .filter(jwk -> jwk.getKid().equals(keyId)) + .findFirst() + .orElseThrow(); + + } + private String getKeyId(String token){ + Base64.Decoder decoder = Base64.getUrlDecoder(); + String[] splitToken = token.split("\\."); + String header = splitToken[0]; + String headerJson = new String(decoder.decode(header)); + + ObjectMapper mapper = new ObjectMapper(); + Map headerMap = null; + try { + headerMap = mapper.readValue(headerJson, Map.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return headerMap.get("kid"); + } + private PublicKey jwkToPublickey(JwkResponse.Jwk jwk){ + Base64.Decoder decoder = Base64.getUrlDecoder(); + byte[] decodeN = decoder.decode(jwk.getN()); + byte[] decodeE = decoder.decode(jwk.getE()); + BigInteger n = new BigInteger(1, decodeN); + BigInteger e = new BigInteger(1, decodeE); + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e); + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { + throw new RuntimeException(ex); + } + } +} From 70535ca325aa08164ac4f671219eb93725d8ec77 Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 10:34:03 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EA=B2=80=EC=A6=9D=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EA=B2=80=EC=A6=9D=20=EC=84=B1=EA=B3=B5?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/LoginController.java | 88 +++++++++++-------- .../onnoff/auth/dto/LoginRequestDTO.java | 26 ++++++ .../domain/user/converter/UserConverter.java | 19 ++-- 3 files changed, 89 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/onnoff/onnoff/auth/dto/LoginRequestDTO.java diff --git a/src/main/java/com/onnoff/onnoff/auth/controller/LoginController.java b/src/main/java/com/onnoff/onnoff/auth/controller/LoginController.java index 0d47f5a..897b47d 100644 --- a/src/main/java/com/onnoff/onnoff/auth/controller/LoginController.java +++ b/src/main/java/com/onnoff/onnoff/auth/controller/LoginController.java @@ -3,20 +3,19 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.onnoff.onnoff.apiPayload.ApiResponse; -import com.onnoff.onnoff.apiPayload.code.status.SuccessStatus; import com.onnoff.onnoff.auth.UserContext; -import com.onnoff.onnoff.auth.feignClient.dto.KakaoOauth2DTO; +import com.onnoff.onnoff.auth.dto.LoginRequestDTO; +import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse; +import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO; import com.onnoff.onnoff.auth.jwt.dto.JwtToken; import com.onnoff.onnoff.auth.jwt.service.JwtUtil; -import com.onnoff.onnoff.auth.service.LoginService; +import com.onnoff.onnoff.auth.service.AppleLoginService; +import com.onnoff.onnoff.auth.service.KakaoLoginService; import com.onnoff.onnoff.domain.user.User; import com.onnoff.onnoff.domain.user.converter.UserConverter; import com.onnoff.onnoff.domain.user.dto.UserResponseDTO; import com.onnoff.onnoff.domain.user.service.UserService; 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.ApiResponses; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,11 +28,13 @@ @Controller @RequiredArgsConstructor public class LoginController { - private final LoginService loginService; + private final KakaoLoginService kakaoLoginService; + private final AppleLoginService appleLoginService; private final UserService userService; private final JwtUtil jwtUtil; + /* 테스트용 API, CORS 때문에 직접 호출하지 않고 redirect */ @@ -51,60 +52,71 @@ public String login(){ */ @GetMapping("/oauth2/login/kakao") public ResponseEntity getAccessToken(@RequestParam(name = "code") String code){ - String accessToken = loginService.getAccessToken(code); - return ResponseEntity.ok("http://localhost:8080/oauth2/kakao/token/validate?accessToken="+accessToken); + TokenResponse tokenResponse = kakaoLoginService.getAccessTokenByCode(code); + return ResponseEntity.ok("http://localhost:8080/oauth2/kakao/token/validate?accessToken="+ tokenResponse.getAccessToken()); } /* - 1. 토큰 유효성 검증 + 1. ID 토큰 유효성 검증 2. 사용자 정보 얻어오기 3. DB 조회 및 추가 4. 응답 헤더에 Jwt 토큰 추가 */ @Operation(summary = "토큰 검증 API",description = "토큰을 검증 하고 이에 대한 결과를 응답합니다. 추가 정보 입력 여부도 같이 응답 합니다.") - @ApiResponses(value = { - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "202", description = "토큰 검증 성공," + " 추가 정보 기입이 필요합니다.", - content = @Content(schema = @Schema(implementation = UserResponseDTO.ApiResponseLoginDTO.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "토큰 검증 성공", - content = @Content(schema = @Schema(implementation = UserResponseDTO.ApiResponseUserDetailDTO.class))) - }) @ResponseBody @PostMapping("/oauth2/kakao/token/validate") - public ApiResponse validateToken(HttpServletResponse response, @RequestBody String accessToken) { - // 토큰 검증 - loginService.validate(accessToken); + public ApiResponse validateKakoToken(HttpServletResponse response, @RequestBody LoginRequestDTO.KakaoTokenValidateDTO requestDTO) { + // identity 토큰 검증 + kakaoLoginService.validate(requestDTO.getIdentityToken()); // ok -> 유저 정보 가져오기 - KakaoOauth2DTO.UserInfoResponseDTO userInfoResponseDTO = null; + KakaoOauth2DTO.UserInfoResponseDTO userInfo; try { - userInfoResponseDTO = loginService.getUserInfo(accessToken); + userInfo = kakaoLoginService.getUserInfo(requestDTO.getAccessToken()); } catch (JsonProcessingException e) { e.printStackTrace(); throw new RuntimeException(e); } // 유저 정보에 DB 조회하고 정보 있으면 응답만, 없으면 저장까지, 추가정보 입력 여부에 따라서 응답 다르게 - Long oauthId = userInfoResponseDTO.getId(); + String oauthId = userInfo.getSub(); + User user; if( userService.isExistByOauthId(oauthId)){ - User user = userService.getUserByOauthId(oauthId); - // 응답헤더에 토큰 추가 - JwtToken token = jwtUtil.generateToken(String.valueOf(user.getId())); - response.addHeader("Access-Token", token.getAccessToken()); - response.addHeader("Refresh-Token", token.getRefreshToken()); - if(user.isInfoSet()){ - return ApiResponse.onSuccess(UserConverter.toUserDetailDTO(user)); - } - return ApiResponse.of(SuccessStatus.NEED_USER_DETAIL, UserConverter.toLoginDTO(user)); + user = userService.getUserByOauthId(oauthId); } else{ - User user = UserConverter.toUser(userInfoResponseDTO); - Long id = userService.create(user); - // 응답헤더에 토큰 추가 - JwtToken token = jwtUtil.generateToken(String.valueOf(id)); - response.addHeader("Access-Token", token.getAccessToken()); - response.addHeader("Refresh-Token", token.getRefreshToken()); - return ApiResponse.of(SuccessStatus.NEED_USER_DETAIL, UserConverter.toLoginDTO(user)); + user = UserConverter.toUser(userInfo); + user = userService.create(user); } + // 응답헤더에 토큰 추가 + JwtToken token = jwtUtil.generateToken(String.valueOf(user.getId())); + response.addHeader("Access-Token", token.getAccessToken()); + response.addHeader("Refresh-Token", token.getRefreshToken()); + return ApiResponse.onSuccess(UserConverter.toLoginDTO(user)); } + @ResponseBody + @PostMapping("/oauth2/apple/token/validate") + public ApiResponse validateAppleToken(HttpServletResponse response, @RequestBody LoginRequestDTO.AppleTokenValidateDTO requestDTO) { + // 검증하기 + appleLoginService.validate(requestDTO.getIdentityToken()); + // 검증 성공 시 리프레시 토큰 발급받아 저장(기한 무제한, 회원탈퇴 시 필요) + TokenResponse tokenResponse = appleLoginService.getAccessTokenByCode(requestDTO.getAuthorizationCode()); + // 유저 정보 조회 및 저장 + String oauthId = requestDTO.getOauthId(); + User user; + if( userService.isExistByOauthId(oauthId)){ + user = userService.getUserByOauthId(oauthId); + } + else{ + user = UserConverter.toUser(requestDTO); + user.setAppleRefreshToken(tokenResponse.getRefreshToken()); + user = userService.create(user); + } + // 응답헤더에 토큰 추가 + JwtToken token = jwtUtil.generateToken(String.valueOf(user.getId())); + response.addHeader("Access-Token", token.getAccessToken()); + response.addHeader("Refresh-Token", token.getRefreshToken()); + return ApiResponse.onSuccess(UserConverter.toLoginDTO(user)); + } /* 테스트용 API */ diff --git a/src/main/java/com/onnoff/onnoff/auth/dto/LoginRequestDTO.java b/src/main/java/com/onnoff/onnoff/auth/dto/LoginRequestDTO.java new file mode 100644 index 0000000..347bdf3 --- /dev/null +++ b/src/main/java/com/onnoff/onnoff/auth/dto/LoginRequestDTO.java @@ -0,0 +1,26 @@ +package com.onnoff.onnoff.auth.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +public class LoginRequestDTO { + @Getter + public static class AppleTokenValidateDTO{ + @JsonProperty("user") + private String oauthId; + @JsonProperty("full_name") + private String fullName; + private String email; + @JsonProperty("identity_token") + private String identityToken; + @JsonProperty("authorization_code") + private String authorizationCode; + } + @Getter + public static class KakaoTokenValidateDTO{ + @JsonProperty("identity_token") + private String identityToken; + @JsonProperty("access_token") + private String accessToken; + } +} diff --git a/src/main/java/com/onnoff/onnoff/domain/user/converter/UserConverter.java b/src/main/java/com/onnoff/onnoff/domain/user/converter/UserConverter.java index 64b850d..327ebc2 100644 --- a/src/main/java/com/onnoff/onnoff/domain/user/converter/UserConverter.java +++ b/src/main/java/com/onnoff/onnoff/domain/user/converter/UserConverter.java @@ -1,21 +1,28 @@ package com.onnoff.onnoff.domain.user.converter; -import com.onnoff.onnoff.auth.feignClient.dto.KakaoOauth2DTO; +import com.onnoff.onnoff.auth.dto.LoginRequestDTO; +import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO; import com.onnoff.onnoff.domain.user.User; import com.onnoff.onnoff.domain.user.dto.UserResponseDTO; import com.onnoff.onnoff.domain.user.enums.SocialType; public class UserConverter { public static User toUser(KakaoOauth2DTO.UserInfoResponseDTO response){ - KakaoOauth2DTO.KakaoAccountDTO kakaoAccount = response.getKakaoAccount(); return User.builder() - .oauthId(response.getId()) - .email(kakaoAccount.getEmail()) - .name(kakaoAccount.getName()) + .oauthId(response.getSub()) + .email(response.getEmail()) + .name(response.getName()) .socialType(SocialType.KAKAO) .build(); } - + public static User toUser(LoginRequestDTO.AppleTokenValidateDTO request){ + return User.builder() + .oauthId(request.getOauthId()) + .email(request.getEmail()) + .name(request.getFullName()) + .socialType(SocialType.APPLE) + .build(); + } public static UserResponseDTO.LoginDTO toLoginDTO(User user){ return UserResponseDTO.LoginDTO.builder() .id(user.getId()) From bc6a012324fe33660b01fc62f4e6f5626c93bfaf Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 10:35:48 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - oauthId 멤버 타입 변경 - appleRefreshToken 멤버 추가 --- src/main/java/com/onnoff/onnoff/domain/user/User.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/onnoff/onnoff/domain/user/User.java b/src/main/java/com/onnoff/onnoff/domain/user/User.java index 7dc6203..3272116 100644 --- a/src/main/java/com/onnoff/onnoff/domain/user/User.java +++ b/src/main/java/com/onnoff/onnoff/domain/user/User.java @@ -31,7 +31,7 @@ public class User extends BaseEntity { private Long id; @Column(nullable = false) - private Long oauthId; //토큰으로 얻어온 정보로 유저를 조회할 때 사용 + private String oauthId; //토큰으로 얻어온 정보로 유저를 조회할 때 사용 @Column(columnDefinition = "TINYINT(1) DEFAULT 0") private boolean infoSet; //추가 정보 기입 여부 @@ -67,6 +67,8 @@ public class User extends BaseEntity { private String fcmToken; + private String appleRefreshToken; + @Enumerated(EnumType.STRING) private SocialType socialType; @@ -84,4 +86,8 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List worklogList = new ArrayList<>(); + + public void setAppleRefreshToken(String appleRefreshToken) { + this.appleRefreshToken = appleRefreshToken; + } } From 9a696302d67a826c00ca5ddca7192cfe4df05176 Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 10:37:24 +0900 Subject: [PATCH 12/14] =?UTF-8?q?fix:=20oauthId=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onnoff/domain/user/repository/UserRepository.java | 2 +- .../onnoff/onnoff/domain/user/service/UserService.java | 6 +++--- .../onnoff/domain/user/service/UserServiceImpl.java | 10 +++++----- .../domain/user/service/UserServiceImplTest.java | 9 ++------- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/onnoff/onnoff/domain/user/repository/UserRepository.java b/src/main/java/com/onnoff/onnoff/domain/user/repository/UserRepository.java index 09246a5..36eb01e 100644 --- a/src/main/java/com/onnoff/onnoff/domain/user/repository/UserRepository.java +++ b/src/main/java/com/onnoff/onnoff/domain/user/repository/UserRepository.java @@ -6,5 +6,5 @@ import java.util.Optional; public interface UserRepository extends JpaRepository { - Optional findByOauthId(Long oauthId); + Optional findByOauthId(String oauthId); } diff --git a/src/main/java/com/onnoff/onnoff/domain/user/service/UserService.java b/src/main/java/com/onnoff/onnoff/domain/user/service/UserService.java index d970991..b0cf32a 100644 --- a/src/main/java/com/onnoff/onnoff/domain/user/service/UserService.java +++ b/src/main/java/com/onnoff/onnoff/domain/user/service/UserService.java @@ -5,13 +5,13 @@ import java.util.List; public interface UserService { - public Long create(User user); + public User create(User user); public List getUserList(); public User getUser(Long id); - public boolean isExistByOauthId(Long oauthId); + public boolean isExistByOauthId(String oauthId); - public User getUserByOauthId(Long oauthId); + public User getUserByOauthId(String oauthId); } diff --git a/src/main/java/com/onnoff/onnoff/domain/user/service/UserServiceImpl.java b/src/main/java/com/onnoff/onnoff/domain/user/service/UserServiceImpl.java index 74be39a..76ab25f 100644 --- a/src/main/java/com/onnoff/onnoff/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/onnoff/onnoff/domain/user/service/UserServiceImpl.java @@ -16,8 +16,8 @@ public class UserServiceImpl implements UserService{ private final UserRepository userRepository; @Transactional @Override - public Long create(User user) { - return userRepository.save(user).getId(); + public User create(User user) { + return userRepository.save(user); } @Transactional(readOnly = true) @@ -36,13 +36,13 @@ public User getUser(Long id) { } @Transactional(readOnly = true) @Override - public boolean isExistByOauthId(Long oauthId) { - return userRepository.findById(oauthId).isPresent(); + public boolean isExistByOauthId(String oauthId) { + return userRepository.findByOauthId(oauthId).isPresent(); } @Transactional(readOnly = true) @Override - public User getUserByOauthId(Long oauthId) { + public User getUserByOauthId(String oauthId) { User user = userRepository.findByOauthId(oauthId).orElseThrow( () -> new GeneralException(ErrorStatus.USER_NOT_FOUND) ); diff --git a/src/test/java/com/onnoff/onnoff/domain/user/service/UserServiceImplTest.java b/src/test/java/com/onnoff/onnoff/domain/user/service/UserServiceImplTest.java index dadbfa8..76268a5 100644 --- a/src/test/java/com/onnoff/onnoff/domain/user/service/UserServiceImplTest.java +++ b/src/test/java/com/onnoff/onnoff/domain/user/service/UserServiceImplTest.java @@ -1,17 +1,12 @@ package com.onnoff.onnoff.domain.user.service; import com.onnoff.onnoff.domain.user.User; -import com.onnoff.onnoff.domain.user.repository.UserRepository; import jakarta.transaction.Transactional; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @@ -23,9 +18,9 @@ class UserServiceImplTest { void 생성_및_조회() { User user = User.builder().name("우성").nickname("우스").build(); - Long userId = userService.create(user); + User createdUser = userService.create(user); - User findUser = userService.getUser(userId); + User findUser = userService.getUser(createdUser.getId()); Assertions.assertThat(user.getName()).isEqualTo(findUser.getName()); } From a379ce020adc83f5a8b777aab6e877a6e15511dd Mon Sep 17 00:00:00 2001 From: wuseong Date: Sun, 28 Jan 2024 10:45:21 +0900 Subject: [PATCH 13/14] =?UTF-8?q?fix:=20import=EB=AC=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/onnoff/onnoff/auth/feignClient/CustomErrorDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/onnoff/onnoff/auth/feignClient/CustomErrorDecoder.java b/src/main/java/com/onnoff/onnoff/auth/feignClient/CustomErrorDecoder.java index 0c6f43e..dfefe80 100644 --- a/src/main/java/com/onnoff/onnoff/auth/feignClient/CustomErrorDecoder.java +++ b/src/main/java/com/onnoff/onnoff/auth/feignClient/CustomErrorDecoder.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.onnoff.onnoff.apiPayload.code.status.ErrorStatus; import com.onnoff.onnoff.apiPayload.exception.GeneralException; -import com.onnoff.onnoff.auth.feignClient.dto.ErrorResponseDTO; +import com.onnoff.onnoff.auth.feignClient.dto.kakao.ErrorResponseDTO; import feign.Response; import feign.codec.ErrorDecoder; import lombok.extern.slf4j.Slf4j; From 68d2d593604d90a1fd6d4bf10bc04b2fe18b154a Mon Sep 17 00:00:00 2001 From: wuseong Date: Mon, 29 Jan 2024 10:22:54 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20=EC=95=A0=ED=94=8C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EA=B0=92=20=EB=B3=80=EC=88=98=EB=93=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EA=B0=92=20=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AppleLoginService.java | 32 +++++++++---------- .../auth/service/KakaoLoginService.java | 10 ++++-- .../SocialTokenValidatorImpl.java | 24 ++++++++------ 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/onnoff/onnoff/auth/service/AppleLoginService.java b/src/main/java/com/onnoff/onnoff/auth/service/AppleLoginService.java index e1af879..045358c 100644 --- a/src/main/java/com/onnoff/onnoff/auth/service/AppleLoginService.java +++ b/src/main/java/com/onnoff/onnoff/auth/service/AppleLoginService.java @@ -36,25 +36,25 @@ public class AppleLoginService implements LoginService{ private final AppleAuthClient appleAuthClient; private final SocialTokenValidator validator; - @Value("${apple.key.id") - private static String kid; - @Value("${apple.key.path") - private static String keyPath; - @Value("${apple.aud") - private static String aud; - @Value("${apple.iss") - private static String iss; - @Value("${apple.team-id") - private static String teamId; - @Value("${apple.redirect-uri") - private static String redirectUri; + @Value("${apple.key.id}") + private String kid; + @Value("${apple.key.path}") + private String keyPath; + @Value("${apple.client-id}") + private String clientId; + @Value("${apple.iss}") + private String iss; + @Value("${apple.team-id}") + private String teamId; + @Value("${apple.redirect-uri}") + private String redirectUri; @Override public TokenResponse getAccessTokenByCode(String code) { // client secret 만들기 String clientSecret = createClientSecret(); // 요청 MultiValueMap urlEncoded = TokenRequest.builder() - .clientId(aud) + .clientId(clientId) .clientSecret(clientSecret) .code("authorization_code_value") .grantType("authorization_code") @@ -74,8 +74,8 @@ private String createClientSecret() { .setIssuer(teamId) // 토큰 발행자 = 우리 팀 .setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간 - UNIX 시간 .setExpiration(expirationDate) // 만료 시간 - .setAudience(iss) // 내가 토큰을 발행하니까 애플을 aud로 반대로 - .setSubject(aud) // 토큰의 주체 = 우리 앱 + .setAudience(iss) // 애플이 수신자 + .setSubject(clientId) // 토큰의 주체 = 우리 앱 .signWith(SignatureAlgorithm.ES256, getPrivateKey()) .compact(); } catch (IOException e) { @@ -90,7 +90,7 @@ public String getAccessTokenByRfToken(String code) { String appleRefreshToken = user.getAppleRefreshToken(); // 요청 MultiValueMap urlEncoded = TokenRequest.builder() - .clientId(aud) + .clientId(clientId) .clientSecret(clientSecret) .refreshToken(appleRefreshToken) .grantType("refresh_token") diff --git a/src/main/java/com/onnoff/onnoff/auth/service/KakaoLoginService.java b/src/main/java/com/onnoff/onnoff/auth/service/KakaoLoginService.java index c151531..7d3fa77 100644 --- a/src/main/java/com/onnoff/onnoff/auth/service/KakaoLoginService.java +++ b/src/main/java/com/onnoff/onnoff/auth/service/KakaoLoginService.java @@ -10,6 +10,7 @@ import com.onnoff.onnoff.domain.user.enums.SocialType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -20,15 +21,18 @@ public class KakaoLoginService implements LoginService{ private final KakaoOauth2Client kakaoOauth2Client; private final KakaoApiClient kakaoApiClient; private final SocialTokenValidator validator; - + @Value("${kakao.client-id}") + private String clientId; + @Value("${kakao.redirect-uri}") + private String redirectUri; /* 테스트 용으로 만든거, 실제로는 프론트에서 처리해서 액세스 토큰만 가져다 줌 */ @Override public TokenResponse getAccessTokenByCode(String code){ return kakaoOauth2Client.getAccessToken("authorization_code", - "32c0787d1b1e9fcabcc24af247903ba8", - "http://localhost:8080/oauth2/login/kakao", + clientId, + redirectUri, code); } // id 토큰 유효성 검증 diff --git a/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidatorImpl.java b/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidatorImpl.java index 0b177fe..08ae6da 100644 --- a/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidatorImpl.java +++ b/src/main/java/com/onnoff/onnoff/auth/service/tokenValidator/SocialTokenValidatorImpl.java @@ -28,13 +28,19 @@ public class SocialTokenValidatorImpl implements SocialTokenValidator{ private final KakaoOauth2Client kakaoOauth2Client; private final AppleAuthClient appleAuthClient; @Value("${kakao.iss}") - private static String kakaoIss; - @Value("${kakao.aud}") - private static String kakaoAud; + private String kakaoIss; + @Value("${kakao.client-id}") + private String kakaoAud; @Value("${apple.iss}") - private static String appleIss; - @Value("${apple.aud}") - private static String appleAud; + private String appleIss; + @Value("${apple.client-id}") + private String appleAud; + + /** + * 1. 공개키(JWK) 목록 조회하여 맞는 공개키 정보 획득 + * 2. 검증에 사용할 수 있는 공개키로 변환 + * 3. 공개키로 ID토큰 검증 + */ @Override public void validate(String token, SocialType socialType) { JwkResponse.Jwk matchingJwk = getMatchingJwk(token, socialType); @@ -51,9 +57,9 @@ public void validate(String token, SocialType socialType) { private Jwt verifyToken(String token, String iss, String aud, PublicKey publicKey) { try { return (Jwt) Jwts.parser() - .requireAudience(aud) - .requireIssuer(iss) - .verifyWith(publicKey) + .requireAudience(aud) //수신자 검증 + .requireIssuer(iss) // 발급자 검증 + .verifyWith(publicKey) // 시그니처 검증 .build() .parse(token); }