From e0df414def5c183852f0d5792d8db6840c17c6de Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:22:41 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=ED=95=84=EC=9A=94=20=EB=B0=8F=20oauth=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=ED=95=84=EC=9A=94=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/exception/NotOAuthAuthorizedException.java | 10 ++++++++++ .../exception/RequiredTermsAgreementException.java | 10 ++++++++++ src/main/resources/errors.properties | 4 +++- src/main/resources/errors_en_US.properties | 2 ++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/user/exception/NotOAuthAuthorizedException.java create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/user/exception/RequiredTermsAgreementException.java diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/user/exception/NotOAuthAuthorizedException.java b/src/main/java/com/wl2c/elswhereuserservice/domain/user/exception/NotOAuthAuthorizedException.java new file mode 100644 index 0000000..5df22d6 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/user/exception/NotOAuthAuthorizedException.java @@ -0,0 +1,10 @@ +package com.wl2c.elswhereuserservice.domain.user.exception; + +import com.wl2c.elswhereuserservice.global.error.exception.LocalizedMessageException; +import org.springframework.http.HttpStatus; + +public class NotOAuthAuthorizedException extends LocalizedMessageException { + public NotOAuthAuthorizedException() { + super(HttpStatus.BAD_REQUEST, "required.oauth-authorization"); + } +} diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/user/exception/RequiredTermsAgreementException.java b/src/main/java/com/wl2c/elswhereuserservice/domain/user/exception/RequiredTermsAgreementException.java new file mode 100644 index 0000000..d44b63c --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/user/exception/RequiredTermsAgreementException.java @@ -0,0 +1,10 @@ +package com.wl2c.elswhereuserservice.domain.user.exception; + +import com.wl2c.elswhereuserservice.global.error.exception.LocalizedMessageException; +import org.springframework.http.HttpStatus; + +public class RequiredTermsAgreementException extends LocalizedMessageException { + public RequiredTermsAgreementException() { + super(HttpStatus.FORBIDDEN, "required.terms-agreement"); + } +} diff --git a/src/main/resources/errors.properties b/src/main/resources/errors.properties index 94caa82..c50754b 100644 --- a/src/main/resources/errors.properties +++ b/src/main/resources/errors.properties @@ -12,13 +12,15 @@ notfound.product-like=\uD574\uB2F9 \uC0C1\uD488\uC5D0 \uB300\uD55C \uC88B\uC544\ unexpected=\uC608\uC0C1\uCE58 \uBABB\uD55C \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. +required.oauth-authorization=OAuth \uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. +required.terms-agreement=\uC11C\uBE44\uC2A4 \uC774\uC6A9 \uC57D\uAD00 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. required.access-token=\uC561\uC138\uC2A4 \uD1A0\uD070\uC774 \uD544\uC694\uD55C \uC694\uCCAD\uC785\uB2C8\uB2E4. required.parameter=\uBC18\uB4DC\uC2DC \uC785\uB825\uD574\uC57C\uD558\uB294 Query Parameter\uC785\uB2C8\uB2E4. required.granted=\uD574\uB2F9 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. failed.oauth-callback-processing=OAuth \uCF5C\uBC31 \uC791\uC5C5\uC744 \uD558\uB294 \uB3C4\uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. failed.get-google-oauth-token=\uAD6C\uAE00\uB85C\uBD80\uD130 \uD1A0\uD070\uC744 \uBC1B\uB294\uB370 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. -failed.get-apple-oauth-token=\uc560\ud50c\uB85C\uBD80\uD130 \uD1A0\uD070\uC744 \uBC1B\uB294\uB370 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. +failed.get-apple-oauth-token=\uC560\uD50C\uB85C\uBD80\uD130 \uD1A0\uD070\uC744 \uBC1B\uB294\uB370 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. already.nickname=\uC774\uBBF8 \uC0AC\uC6A9\uC911\uC778 \uB2C9\uB124\uC784\uC785\uB2C8\uB2E4. already.interest=\uC774\uBBF8 \uB4F1\uB85D\uB41C \uAD00\uC2EC \uC0C1\uD488\uC785\uB2C8\uB2E4. diff --git a/src/main/resources/errors_en_US.properties b/src/main/resources/errors_en_US.properties index a20d174..ab0174a 100644 --- a/src/main/resources/errors_en_US.properties +++ b/src/main/resources/errors_en_US.properties @@ -12,6 +12,8 @@ notfound.product-like=No record was found of clicking Like for that product. unexpected=An unexpected error occurred. +required.oauth-authorization=OAuth authentication is required. +required.terms-agreement=Terms and conditions of service need to be agreed. required.access-token=This request requires access-token. required.parameter=Query parameter is required. required.granted=Access denied. From 636486d48378c240d10d363c0b1622c1a198260e Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:45:26 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=EA=B3=BC=EC=A0=95=20=EB=8F=99=EC=95=88=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=A0=95=EB=B3=B4=20=EC=9E=84=EC=8B=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/repository/SignupAuthRepository.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/user/repository/SignupAuthRepository.java diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/user/repository/SignupAuthRepository.java b/src/main/java/com/wl2c/elswhereuserservice/domain/user/repository/SignupAuthRepository.java new file mode 100644 index 0000000..9ed940d --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/user/repository/SignupAuthRepository.java @@ -0,0 +1,35 @@ +package com.wl2c.elswhereuserservice.domain.user.repository; + +import java.time.Instant; +import java.util.Optional; + +public interface SignupAuthRepository { + + /** + * 회원가입 토큰을 키로, 인증 정보를 저장합니다. + * + * @param signupToken 회원가입 토큰 + * @param authName 인증 정보 이름 (구분자) + * @param data 인증 정보 데이터 + */ + void setAuthPayload(String signupToken, String authName, Object data, Instant now); + + /** + * 회원가입 토큰을 통해 저장된 인증 정보를 가져옵니다. + * + * @param signupToken 회원가입 토큰 + * @param authName 인증 정보 이름 (구분자) + * @param payloadClass 인증 정보 클래스 타입 + * @param now 현재 시각 + */ + Optional getAuthPayload(String signupToken, String authName, Class payloadClass, Instant now); + + /** + * 회원가입 토큰을 통해 저장된 인증 정보를 삭제합니다. + * + * @param signupToken 회원가입 토큰 + * @param authName 인증 정보 이름 (구분자) + * @return 삭제된 경우 true, 아니면 false반환 + */ + boolean deleteAuthPayload(String signupToken, String authName); +} From 39f2aa1ae2a54e56bcc4e49e672b537c9ca70e19 Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:48:53 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=EA=B3=BC=EC=A0=95=20=EB=8F=99=EC=95=88=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=A0=95=EB=B3=B4=20=EC=9E=84=EC=8B=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EA=B5=AC=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 --- .../impl/SignupAuthRedisRepository.java | 45 +++++++++++++++++++ .../global/config/redis/RedisKeys.java | 1 + 2 files changed, 46 insertions(+) create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/user/repository/impl/SignupAuthRedisRepository.java diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/user/repository/impl/SignupAuthRedisRepository.java b/src/main/java/com/wl2c/elswhereuserservice/domain/user/repository/impl/SignupAuthRedisRepository.java new file mode 100644 index 0000000..8931683 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/user/repository/impl/SignupAuthRedisRepository.java @@ -0,0 +1,45 @@ +package com.wl2c.elswhereuserservice.domain.user.repository.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wl2c.elswhereuserservice.domain.user.repository.SignupAuthRepository; +import com.wl2c.elswhereuserservice.global.base.AbstractKeyValueCacheRepository; +import com.wl2c.elswhereuserservice.global.config.redis.RedisKeys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; + +@Repository +public class SignupAuthRedisRepository extends AbstractKeyValueCacheRepository implements SignupAuthRepository { + + private final Duration cacheDuration; + + protected SignupAuthRedisRepository(StringRedisTemplate redisTemplate, + ObjectMapper objectMapper, + @Value("${app.auth.signup-expires}") Duration cacheDuration) { + super(redisTemplate, objectMapper, RedisKeys.SIGNUP_AUTH_KEY); + this.cacheDuration = cacheDuration; + } + + public void setAuthPayload(String signupToken, String authName, Object data, Instant now) { + String key = makeEntryKey(signupToken, authName); + set(key, data, now, cacheDuration); + } + + public Optional getAuthPayload(String signupToken, String authName, Class clazz, Instant now) { + String key = makeEntryKey(signupToken, authName); + return get(key, clazz, now); + } + + public boolean deleteAuthPayload(String signupToken, String authName) { + String key = makeEntryKey(signupToken, authName); + return remove(key); + } + + public String makeEntryKey(String signupToken, String authName) { + return signupToken + RedisKeys.KEY_DELIMITER + authName; + } +} diff --git a/src/main/java/com/wl2c/elswhereuserservice/global/config/redis/RedisKeys.java b/src/main/java/com/wl2c/elswhereuserservice/global/config/redis/RedisKeys.java index 2021796..067d3a6 100644 --- a/src/main/java/com/wl2c/elswhereuserservice/global/config/redis/RedisKeys.java +++ b/src/main/java/com/wl2c/elswhereuserservice/global/config/redis/RedisKeys.java @@ -4,6 +4,7 @@ public class RedisKeys { public static final String KEY_DELIMITER = ":"; public static final String USER_LOGOUT_KEY = "logout"; public static final String USER_INFO_CACHE_KEY = "userInfo"; + public static final String SIGNUP_AUTH_KEY = "signupAuth"; public static String combine(Object key1, Object key2) { return key1 + KEY_DELIMITER + key2; From a98a9f12ab468d0af7e229e75a0f000db4739e45 Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:53:09 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=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 --- .../request/RequestIsAgreedToTermsDto.java | 19 ++++++ .../domain/user/service/SignupService.java | 65 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/user/model/dto/request/RequestIsAgreedToTermsDto.java create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/user/service/SignupService.java diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/user/model/dto/request/RequestIsAgreedToTermsDto.java b/src/main/java/com/wl2c/elswhereuserservice/domain/user/model/dto/request/RequestIsAgreedToTermsDto.java new file mode 100644 index 0000000..c1a5ae5 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/user/model/dto/request/RequestIsAgreedToTermsDto.java @@ -0,0 +1,19 @@ +package com.wl2c.elswhereuserservice.domain.user.model.dto.request; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +public class RequestIsAgreedToTermsDto { + + @Schema(description = "회원가입을 진행하는 사용자의 서비스 이용 약관 동의 여부", example = "true") + private final boolean agreed; + + @JsonCreator + public RequestIsAgreedToTermsDto(@JsonProperty("agreed") boolean agreed) { + this.agreed = agreed; + } + +} diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/user/service/SignupService.java b/src/main/java/com/wl2c/elswhereuserservice/domain/user/service/SignupService.java new file mode 100644 index 0000000..12beca6 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/user/service/SignupService.java @@ -0,0 +1,65 @@ +package com.wl2c.elswhereuserservice.domain.user.service; + +import com.wl2c.elswhereuserservice.domain.user.exception.NotOAuthAuthorizedException; +import com.wl2c.elswhereuserservice.domain.user.exception.RequiredTermsAgreementException; +import com.wl2c.elswhereuserservice.domain.user.model.dto.request.RequestIsAgreedToTermsDto; +import com.wl2c.elswhereuserservice.domain.user.model.dto.response.ResponseLoginDto; +import com.wl2c.elswhereuserservice.domain.user.model.entity.User; +import com.wl2c.elswhereuserservice.domain.user.repository.SignupAuthRepository; +import com.wl2c.elswhereuserservice.domain.user.repository.UserRepository; +import com.wl2c.elswhereuserservice.global.auth.jwt.AuthenticationToken; +import com.wl2c.elswhereuserservice.global.auth.jwt.JwtProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Clock; +import java.time.Instant; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SignupService { + + public static final String OAUTH_AUTH_NAME = "oauth"; + + private final Clock clock; + private final SignupAuthRepository signupAuthRepository; + private final UserRepository userRepository; + private final JwtProvider jwtProvider; + + private final UserInfoService userInfoService; + + @Transactional + public ResponseLoginDto signup(String signupToken, RequestIsAgreedToTermsDto requestIsAgreedToTermsDto) { + User user = getUserInfo(signupToken); + + boolean deleted = deleteUserInfo(signupToken); + if (!deleted) + log.error("Can't delete user signup authentication: oauth user info"); + + if (!requestIsAgreedToTermsDto.isAgreed()) { + throw new RequiredTermsAgreementException(); + } + + userRepository.save(user); + + AuthenticationToken token = jwtProvider.issue(user); + userInfoService.cacheUserInfo(user.getId(), user); + + return new ResponseLoginDto(token); + } + + private User getUserInfo(String signupToken) { + Instant now = Instant.now(clock); + return signupAuthRepository.getAuthPayload(signupToken, OAUTH_AUTH_NAME, User.class, now) + .orElseThrow(NotOAuthAuthorizedException::new); + } + + private boolean deleteUserInfo(String signupToken) { + return signupAuthRepository.deleteAuthPayload(signupToken, OAUTH_AUTH_NAME); + } + +} From d3bf2db4b1eecec46675e5040b64b80735af1b17 Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:53:25 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=EB=8F=99?= =?UTF-8?q?=EC=9D=98=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/user/controller/UserController.java b/src/main/java/com/wl2c/elswhereuserservice/domain/user/controller/UserController.java index 1dd32d3..9703ef4 100644 --- a/src/main/java/com/wl2c/elswhereuserservice/domain/user/controller/UserController.java +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/user/controller/UserController.java @@ -2,12 +2,20 @@ import com.wl2c.elswhereuserservice.domain.user.model.dto.request.RequestNickNameChangeDto; import com.wl2c.elswhereuserservice.domain.user.model.dto.request.RequestRefreshTokenDto; +import com.wl2c.elswhereuserservice.domain.user.model.dto.request.RequestIsAgreedToTermsDto; +import com.wl2c.elswhereuserservice.domain.user.model.dto.response.ResponseLoginDto; import com.wl2c.elswhereuserservice.domain.user.model.dto.response.ResponseRefreshTokenDto; import com.wl2c.elswhereuserservice.domain.user.model.dto.response.ResponseUserInfoDto; import com.wl2c.elswhereuserservice.domain.user.model.dto.response.ResponseUserNicknameDto; +import com.wl2c.elswhereuserservice.domain.user.service.SignupService; import com.wl2c.elswhereuserservice.domain.user.service.UserInfoService; import com.wl2c.elswhereuserservice.domain.user.service.UserService; import com.wl2c.elswhereuserservice.domain.user.service.UserWithdrawService; +import com.wl2c.elswhereuserservice.global.model.dto.ErrorResponseDto; +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.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; @@ -25,6 +33,7 @@ public class UserController { private final UserService userService; private final UserInfoService userInfoService; private final UserWithdrawService userWithdrawService; + private final SignupService signupService; /** * 내 정보 조회 @@ -89,6 +98,34 @@ public ResponseRefreshTokenDto refreshToken(HttpServletRequest request, return userService.refreshToken(request, dto.getRefreshToken()); } + /** + * 회원가입 + *

+ * 각 Oauth를 통해 가입을 진행할 때, + * 서비스 이용 약관 페이지로 리다이렉션을 하면서 서버에서 건내준 signup_token을 해당 api에서 이용합니다. + *

+ * + * @param dto 서비스 이용 약관 동의 여부 및 약관 버전에 대한 dto + * @param signupToken 회원가입 토큰 + * @return 액세스 토큰 및 리프레쉬 토큰에 대한 dto + */ + @PostMapping("/signup/{signup-token}") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "액세스 토큰 및 리프레쉬 토큰에 대한 dto", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ResponseLoginDto.class))), + @ApiResponse(responseCode = "400", description = "OAuth 인증이 필요합니다.", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDto.class))), + @ApiResponse(responseCode = "403", description = "서비스 이용 약관 동의가 필요합니다.", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = ErrorResponseDto.class))) + }) + public ResponseLoginDto signup(@Valid @RequestBody RequestIsAgreedToTermsDto dto, + @PathVariable("signup-token") String signupToken) { + return signupService.signup(signupToken, dto); + } + /** * 로그아웃 */ From 0ebefc9e784e968e6755e0cdcfa2b6118cab1a9a Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:55:08 +0900 Subject: [PATCH 06/12] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20google?= =?UTF-8?q?=20oauth=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20v1=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ervice.java => GoogleOAuth2V1Service.java} | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) rename src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/{GoogleOAuth2Service.java => GoogleOAuth2V1Service.java} (90%) diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2Service.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2V1Service.java similarity index 90% rename from src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2Service.java rename to src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2V1Service.java index eb177f7..ba11bb4 100644 --- a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2Service.java +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2V1Service.java @@ -18,6 +18,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,7 +34,7 @@ @Service @RequiredArgsConstructor @Slf4j -public class GoogleOAuth2Service { +public class GoogleOAuth2V1Service { @Value("${oauth2.google.client-id}") private String googleClientId; @@ -41,8 +42,8 @@ public class GoogleOAuth2Service { @Value("${oauth2.google.client-secret}") private String googleClientSecret; - @Value("${oauth2.google.redirect-uri}") - private String googleRedirectUri; + @Value("${oauth2.google.redirect-uri-v1}") + private String googleRedirectUriV1; @Value("${oauth2.google.token-uri}") private String googleTokenUri; @@ -61,14 +62,20 @@ public class GoogleOAuth2Service { private final RestTemplate restTemplate = new RestTemplate(); @Transactional - public ResponseEntity processCallback(String code, HttpServletResponse response) { + public ResponseEntity handleGoogleOAuthCallback(Map params, HttpServletResponse response) { + + String code = params.get("code"); + if (params.containsKey("error")) { + return ResponseEntity.status(HttpStatus.SEE_OTHER).header("Location", "elswhere://?error=access_denied").build(); + } + try { // 구글로부터 토큰(엑세스, 리프레쉬) 요청 Map tokenRequest = new HashMap<>(); tokenRequest.put("code", code); tokenRequest.put("client_id", googleClientId); tokenRequest.put("client_secret", googleClientSecret); - tokenRequest.put("redirect_uri", googleRedirectUri); + tokenRequest.put("redirect_uri", googleRedirectUriV1); tokenRequest.put("grant_type", "authorization_code"); ResponseEntity tokenResponse = restTemplate.postForEntity(googleTokenUri, tokenRequest, JsonNode.class); @@ -131,10 +138,10 @@ public ResponseEntity processCallback(String code, HttpServletResponse r public String getAuthorizationUri() { return googleAuthorizationUri + "?client_id=" + googleClientId - + "&redirect_uri=" + googleRedirectUri + + "&redirect_uri=" + googleRedirectUriV1 + "&response_type=code" + "&scope=profile email" + "&access_type=offline" + "&prompt=consent"; } -} +} \ No newline at end of file From ca21615cd35cb8644323f5ae609e84b0f18c1197 Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:55:27 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20google?= =?UTF-8?q?=20oauth=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EB=A5=BC=20v1?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GoogleOAuth2Controller.java | 43 ------------------- .../controller/GoogleOAuth2V1Controller.java | 42 ++++++++++++++++++ 2 files changed, 42 insertions(+), 43 deletions(-) delete mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2Controller.java create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2V1Controller.java diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2Controller.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2Controller.java deleted file mode 100644 index 9efdb2c..0000000 --- a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2Controller.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.wl2c.elswhereuserservice.domain.oauth.google.controller; - -import com.wl2c.elswhereuserservice.domain.oauth.google.service.GoogleOAuth2Service; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.io.IOException; -import java.util.Map; - -@Tag(name = "구글 OAuth", description = "구글 OAuth 관련 api") -@RestController -@RequiredArgsConstructor -@RequestMapping("/v1/oauth2/google") -public class GoogleOAuth2Controller { - - private final GoogleOAuth2Service googleOAuth2Service; - - /** - * Google OAuth 인증 페이지로 리디렉션하는 엔드포인트 - */ - @GetMapping("/login") - public void login(HttpServletResponse response) throws IOException { - String authorizationUri = googleOAuth2Service.getAuthorizationUri(); - response.sendRedirect(authorizationUri); - } - - /** - * Google OAuth에서 인증 후 리디렉션될 콜백 엔드포인트 - */ - @GetMapping("/callback") - public ResponseEntity callback(@RequestParam Map param, HttpServletResponse response) throws IOException { - String code = param.get("code"); - if (param.containsKey("error")) { - return ResponseEntity.status(HttpStatus.SEE_OTHER).header("Location", "elswhere://?error=access_denied").build(); - } else { - return googleOAuth2Service.processCallback(code, response); - } - } -} diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2V1Controller.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2V1Controller.java new file mode 100644 index 0000000..306ac36 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2V1Controller.java @@ -0,0 +1,42 @@ +package com.wl2c.elswhereuserservice.domain.oauth.google.controller; + +import com.wl2c.elswhereuserservice.domain.oauth.google.service.GoogleOAuth2V1Service; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.Map; + +@Tag(name = "구글 OAuth", description = "구글 OAuth 관련 api") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/oauth2/google/") +public class GoogleOAuth2V1Controller { + + private final GoogleOAuth2V1Service googleOAuth2V1Service; + + /** + * Google OAuth 인증 페이지로 리다이렉션하는 엔드포인트 v1 + */ + @GetMapping("/login") + public void loginV1(HttpServletResponse response) throws IOException { + String authorizationUri = googleOAuth2V1Service.getAuthorizationUri(); + response.sendRedirect(authorizationUri); + } + + + /** + * Google OAuth에서 인증 후 리다이렉션될 콜백 엔드포인트 v1 + */ + @GetMapping("/callback") + public ResponseEntity callbackV1(@RequestParam Map params, HttpServletResponse response) throws IOException { + return googleOAuth2V1Service.handleGoogleOAuthCallback(params, response); + } + +} From 96a2df8efa197285772864d16035473b972e03aa Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:57:02 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20=EC=9D=B4=EC=9A=A9=20=EC=95=BD?= =?UTF-8?q?=EA=B4=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20google=20oauth=20v2=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../google/service/GoogleOAuth2V2Service.java | 163 ++++++++++++++++++ .../domain/user/util/CodeGenerator.java | 12 ++ 2 files changed, 175 insertions(+) create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2V2Service.java create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/user/util/CodeGenerator.java diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2V2Service.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2V2Service.java new file mode 100644 index 0000000..97c6f77 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/service/GoogleOAuth2V2Service.java @@ -0,0 +1,163 @@ +package com.wl2c.elswhereuserservice.domain.oauth.google.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.wl2c.elswhereuserservice.domain.oauth.google.exception.FailedToReceiveGoogleOAuth2TokenException; +import com.wl2c.elswhereuserservice.domain.user.model.SocialType; +import com.wl2c.elswhereuserservice.domain.user.model.UserStatus; +import com.wl2c.elswhereuserservice.domain.user.model.entity.User; +import com.wl2c.elswhereuserservice.domain.user.repository.SignupAuthRepository; +import com.wl2c.elswhereuserservice.domain.user.repository.UserRepository; +import com.wl2c.elswhereuserservice.domain.user.service.UserInfoService; +import com.wl2c.elswhereuserservice.domain.user.service.UserService; +import com.wl2c.elswhereuserservice.domain.user.util.CodeGenerator; +import com.wl2c.elswhereuserservice.global.auth.jwt.AuthenticationToken; +import com.wl2c.elswhereuserservice.global.auth.jwt.JwtProvider; +import com.wl2c.elswhereuserservice.global.auth.role.UserRole; +import com.wl2c.elswhereuserservice.global.error.exception.FailedOAuthCallbackProcessingException; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class GoogleOAuth2V2Service { + + @Value("${oauth2.google.client-id}") + private String googleClientId; + + @Value("${oauth2.google.client-secret}") + private String googleClientSecret; + + @Value("${oauth2.google.redirect-uri-v2}") + private String googleRedirectUriV2; + + @Value("${oauth2.google.token-uri}") + private String googleTokenUri; + + @Value("${oauth2.google.user-info-uri}") + private String googleUserInfoUri; + + @Value("${oauth2.google.authorization-uri}") + private String googleAuthorizationUri; + + public static final String OAUTH_AUTH_NAME = "oauth"; + + private final Clock clock; + + private final UserRepository userRepository; + private final SignupAuthRepository signupAuthRepository; + private final JwtProvider jwtProvider; + + private final UserService userService; + private final UserInfoService userInfoService; + private final RestTemplate restTemplate = new RestTemplate(); + + @Transactional(readOnly = true) + public ResponseEntity handleGoogleOAuthCallback(Map params, HttpServletResponse response) { + + String code = params.get("code"); + if (params.containsKey("error")) { + return ResponseEntity.status(HttpStatus.SEE_OTHER).header("Location", "elswhere://?error=access_denied").build(); + } + + try { + // 구글로부터 토큰(엑세스, 리프레쉬) 요청 + Map tokenRequest = new HashMap<>(); + tokenRequest.put("code", code); + tokenRequest.put("client_id", googleClientId); + tokenRequest.put("client_secret", googleClientSecret); + tokenRequest.put("redirect_uri", googleRedirectUriV2); + tokenRequest.put("grant_type", "authorization_code"); + + ResponseEntity tokenResponse = restTemplate.postForEntity(googleTokenUri, tokenRequest, JsonNode.class); + + if (tokenResponse.getStatusCode().is2xxSuccessful()) { + String accessToken = Objects.requireNonNull(tokenResponse.getBody()).get("access_token").asText(); + String refreshToken = tokenResponse.getBody().get("refresh_token").asText(); + + Map userInfo = null; + try { + ResponseEntity> userInfoResponse = restTemplate.exchange( + googleUserInfoUri + "?access_token=" + accessToken, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {} + ); + userInfo = userInfoResponse.getBody(); + + } catch (RestClientException e) { + e.printStackTrace(); + } + + Optional optionalUser = userRepository.findBySocialId(userInfo.get("sub").toString()); + User user; + if (optionalUser.isEmpty()) { + user = User.builder() + .socialId(userInfo.get("sub").toString()) + .socialType(SocialType.GOOGLE) + .email(userInfo.get("email").toString()) + .name(userInfo.get("name").toString()) + .nickname(userService.createRandomNickname()) + .userStatus(UserStatus.ACTIVE) + .userRole(UserRole.USER) + .build(); + + String signupToken = CodeGenerator.generateUUIDCode(); + signupAuthRepository.setAuthPayload(signupToken, OAUTH_AUTH_NAME, user, Instant.now(clock)); + + // 서비스 이용 약관 페이지로 리다이렉션 + String redirectUrl = "elswhere://terms?signup_token=" + signupToken; + return ResponseEntity.status(302).header("Location", redirectUrl).build(); + } else { + user = optionalUser.get(); + + // 백엔드 서버에서 서비스를 위한 자체 토큰 발급 + AuthenticationToken token = jwtProvider.issue(user); + userInfoService.cacheUserInfo(user.getId(), user); + + String redirectUrl = "elswhere://callback?access_token=" + token.getAccessToken() + "&refresh_token=" + token.getRefreshToken(); + return ResponseEntity.status(302).header("Location", redirectUrl).build(); + } + + } else { + response.sendRedirect("elswhere://?error=invalid_token"); + throw new FailedToReceiveGoogleOAuth2TokenException(); + } + } catch (Exception e) { + try { + response.sendRedirect("elswhere://?error=invalid_token"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + throw new FailedOAuthCallbackProcessingException(e); + } + } + + public String getAuthorizationUri() { + return googleAuthorizationUri + + "?client_id=" + googleClientId + + "&redirect_uri=" + googleRedirectUriV2 + + "&response_type=code" + + "&scope=profile email" + + "&access_type=offline" + + "&prompt=consent"; + } +} diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/user/util/CodeGenerator.java b/src/main/java/com/wl2c/elswhereuserservice/domain/user/util/CodeGenerator.java new file mode 100644 index 0000000..51e1574 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/user/util/CodeGenerator.java @@ -0,0 +1,12 @@ +package com.wl2c.elswhereuserservice.domain.user.util; + +import java.util.Random; +import java.util.UUID; + +public class CodeGenerator { + + public static String generateUUIDCode() { + return UUID.randomUUID().toString(); + } + +} From 8f3c1789579db8e85590c69e7ea2a1140fb62d58 Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 00:57:20 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20google=20oauth=20v2=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GoogleOAuth2V2Controller.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2V2Controller.java diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2V2Controller.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2V2Controller.java new file mode 100644 index 0000000..9f34db1 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/google/controller/GoogleOAuth2V2Controller.java @@ -0,0 +1,41 @@ +package com.wl2c.elswhereuserservice.domain.oauth.google.controller; + +import com.wl2c.elswhereuserservice.domain.oauth.google.service.GoogleOAuth2V2Service; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.Map; + +@Tag(name = "구글 OAuth", description = "구글 OAuth 관련 api") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v2/oauth2/google/") +public class GoogleOAuth2V2Controller { + + private final GoogleOAuth2V2Service googleOAuth2V2Service; + + /** + * Google OAuth 인증 페이지로 리다이렉션하는 엔드포인트 v2 + */ + @GetMapping("/login") + public void loginV2(HttpServletResponse response) throws IOException { + String authorizationUri = googleOAuth2V2Service.getAuthorizationUri(); + response.sendRedirect(authorizationUri); + } + + /** + * Google OAuth에서 인증 후 리다이렉션될 콜백 엔드포인트 v2 + */ + @GetMapping("/callback") + public ResponseEntity callbackV2(@RequestParam Map params, HttpServletResponse response) throws IOException { + return googleOAuth2V2Service.handleGoogleOAuthCallback(params, response); + } + +} From f8afa5c43eaf97b686eea88615e1cfa58ac85cdd Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 01:00:10 +0900 Subject: [PATCH 10/12] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20apple=20?= =?UTF-8?q?oauth=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20v1=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Service.java => AppleOAuth2V1Service.java} | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) rename src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/{AppleOAuth2Service.java => AppleOAuth2V1Service.java} (95%) diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2Service.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2V1Service.java similarity index 95% rename from src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2Service.java rename to src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2V1Service.java index 236d5b6..47ce098 100644 --- a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2Service.java +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2V1Service.java @@ -13,12 +13,9 @@ import com.wl2c.elswhereuserservice.global.auth.jwt.JwtDecoder; import com.wl2c.elswhereuserservice.global.auth.jwt.JwtProvider; import com.wl2c.elswhereuserservice.global.auth.role.UserRole; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; @@ -35,7 +32,7 @@ @Service @RequiredArgsConstructor @Slf4j -public class AppleOAuth2Service { +public class AppleOAuth2V1Service { @Value("${oauth2.apple.client-id}") private String appleClientId; @@ -46,8 +43,8 @@ public class AppleOAuth2Service { @Value("${oauth2.apple.team-id}") private String appleTeamId; - @Value("${oauth2.apple.redirect-uri}") - private String appleRedirectUri; + @Value("${oauth2.apple.redirect-uri-v1}") + private String appleRedirectUriV1; @Value("${oauth2.apple.key-path}") private String appleKeyPath; @@ -92,17 +89,17 @@ public ResponseEntity handleAppleOAuthCallback(Map params, Ht String accessToken = tokenResponse.get("access_token"); String refreshToken = tokenResponse.get("refresh_token"); - // 클라이언트 앱의 콜백 URL 스킴을 사용하여 리디렉션 + // 클라이언트 앱의 콜백 URL 스킴을 사용하여 리다이렉션 String callbackUrl = builder .queryParam("access_token", accessToken) .queryParam("refresh_token", refreshToken) .build().toUriString(); - // 리디렉션을 위한 헤더 설정 + // 리다이렉션을 위한 헤더 설정 HttpHeaders headers = new HttpHeaders(); headers.add("Location", callbackUrl); - // 302 리디렉션 응답 + // 302 리다이렉션 응답 return new ResponseEntity<>(headers, HttpStatus.FOUND); } @@ -136,7 +133,7 @@ public Map getTokens(Map params, HttpServletResp String keyId = appleKeyId; String privateKeyPath = appleKeyPath; String tokenUri = appleTokenUri; - String redirectUri = appleRedirectUri; + String redirectUri = appleRedirectUriV1; // JWT 생성 String clientSecret = tokenProvider.createJwtToken(clientId, teamId, keyId, privateKeyPath); @@ -206,7 +203,7 @@ public Map getTokens(Map params, HttpServletResp public String getAuthorizationUri() { return appleAuthorizationUri + "?client_id=" + appleClientId - + "&redirect_uri=" + appleRedirectUri + + "&redirect_uri=" + appleRedirectUriV1 + "&response_type=code" + "&response_mode=form_post" + "&scope=name email"; From 79acb7927dd041fa552d52cfdd38d35d7bc61af4 Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 01:00:44 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20apple=20?= =?UTF-8?q?oauth=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EB=A5=BC=20v1?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ntroller.java => AppleOAuth2V1Controller.java} | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) rename src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/{AppleOAuth2Controller.java => AppleOAuth2V1Controller.java} (70%) diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2Controller.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2V1Controller.java similarity index 70% rename from src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2Controller.java rename to src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2V1Controller.java index 5e8ae5b..5b91e21 100644 --- a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2Controller.java +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2V1Controller.java @@ -1,16 +1,11 @@ package com.wl2c.elswhereuserservice.domain.oauth.apple.controller; -import com.wl2c.elswhereuserservice.domain.oauth.apple.service.AppleOAuth2Service; +import com.wl2c.elswhereuserservice.domain.oauth.apple.service.AppleOAuth2V1Service; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; import java.util.Map; @@ -19,16 +14,16 @@ @RestController @RequiredArgsConstructor @RequestMapping("/v1/oauth2/apple") -public class AppleOAuth2Controller { +public class AppleOAuth2V1Controller { - private final AppleOAuth2Service appleOAuth2Service; + private final AppleOAuth2V1Service appleOAuth2V1Service; /** * Apple OAuth 인증 페이지로 리디렉션하는 엔드포인트 */ @GetMapping("/login") public void redirectToAppleOAuth(HttpServletResponse response) throws IOException { - String authorizationUri = appleOAuth2Service.getAuthorizationUri(); + String authorizationUri = appleOAuth2V1Service.getAuthorizationUri(); response.sendRedirect(authorizationUri); } @@ -37,6 +32,6 @@ public void redirectToAppleOAuth(HttpServletResponse response) throws IOExceptio */ @PostMapping("/callback") public ResponseEntity handleAppleOAuthCallback(@RequestParam Map params, HttpServletResponse response) throws Exception { - return appleOAuth2Service.handleAppleOAuthCallback(params, response); + return appleOAuth2V1Service.handleAppleOAuthCallback(params, response); } } From 4075fe788dae532c052cd7ba8bfaf00737c52088 Mon Sep 17 00:00:00 2001 From: kjungw1025 Date: Sat, 19 Oct 2024 01:01:19 +0900 Subject: [PATCH 12/12] =?UTF-8?q?feat:=20=EC=9D=B4=EC=9A=A9=20=EC=95=BD?= =?UTF-8?q?=EA=B4=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20apple=20oauth=20v2=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AppleOAuth2V2Controller.java | 37 ++++ .../apple/service/AppleOAuth2V2Service.java | 188 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2V2Controller.java create mode 100644 src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2V2Service.java diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2V2Controller.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2V2Controller.java new file mode 100644 index 0000000..d3c291e --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/controller/AppleOAuth2V2Controller.java @@ -0,0 +1,37 @@ +package com.wl2c.elswhereuserservice.domain.oauth.apple.controller; + +import com.wl2c.elswhereuserservice.domain.oauth.apple.service.AppleOAuth2V2Service; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.Map; + +@Tag(name = "애플 OAuth", description = "애플 OAuth 관련 api") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v2/oauth2/apple") +public class AppleOAuth2V2Controller { + + private final AppleOAuth2V2Service appleOAuth2V2Service; + + /** + * Apple OAuth 인증 페이지로 리디렉션하는 엔드포인트 + */ + @GetMapping("/login") + public void redirectToAppleOAuth(HttpServletResponse response) throws IOException { + String authorizationUri = appleOAuth2V2Service.getAuthorizationUri(); + response.sendRedirect(authorizationUri); + } + + /** + * Apple OAuth에서 인증 후 리디렉션될 콜백 엔드포인트 + */ + @PostMapping("/callback") + public ResponseEntity handleAppleOAuthCallback(@RequestParam Map params, HttpServletResponse response) throws Exception { + return appleOAuth2V2Service.handleAppleOAuthCallback(params, response); + } +} diff --git a/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2V2Service.java b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2V2Service.java new file mode 100644 index 0000000..9306ce8 --- /dev/null +++ b/src/main/java/com/wl2c/elswhereuserservice/domain/oauth/apple/service/AppleOAuth2V2Service.java @@ -0,0 +1,188 @@ +package com.wl2c.elswhereuserservice.domain.oauth.apple.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wl2c.elswhereuserservice.domain.oauth.apple.jwt.AppleJwtTokenProvider; +import com.wl2c.elswhereuserservice.domain.oauth.google.exception.FailedToReceiveGoogleOAuth2TokenException; +import com.wl2c.elswhereuserservice.domain.user.model.SocialType; +import com.wl2c.elswhereuserservice.domain.user.model.UserStatus; +import com.wl2c.elswhereuserservice.domain.user.model.entity.User; +import com.wl2c.elswhereuserservice.domain.user.repository.SignupAuthRepository; +import com.wl2c.elswhereuserservice.domain.user.repository.UserRepository; +import com.wl2c.elswhereuserservice.domain.user.service.UserInfoService; +import com.wl2c.elswhereuserservice.domain.user.service.UserService; +import com.wl2c.elswhereuserservice.domain.user.util.CodeGenerator; +import com.wl2c.elswhereuserservice.global.auth.jwt.AuthenticationToken; +import com.wl2c.elswhereuserservice.global.auth.jwt.JwtDecoder; +import com.wl2c.elswhereuserservice.global.auth.jwt.JwtProvider; +import com.wl2c.elswhereuserservice.global.auth.role.UserRole; +import com.wl2c.elswhereuserservice.global.error.exception.FailedOAuthCallbackProcessingException; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AppleOAuth2V2Service { + + @Value("${oauth2.apple.client-id}") + private String appleClientId; + + @Value("${oauth2.apple.key-id}") + private String appleKeyId; + + @Value("${oauth2.apple.team-id}") + private String appleTeamId; + + @Value("${oauth2.apple.redirect-uri-v2}") + private String appleRedirectUriV2; + + @Value("${oauth2.apple.key-path}") + private String appleKeyPath; + + @Value("${oauth2.apple.authorization-uri}") + private String appleAuthorizationUri; + + @Value("${oauth2.apple.token-uri}") + private String appleTokenUri; + + @Value("${oauth2.apple.revoke-token-uri}") + private String appleRevokeTokenUri; + + private String fullName; + + public static final String OAUTH_AUTH_NAME = "oauth"; + + private final Clock clock; + + private final UserRepository userRepository; + private final SignupAuthRepository signupAuthRepository; + private final JwtProvider jwtProvider; + + private final UserService userService; + private final UserInfoService userInfoService; + private final AppleJwtTokenProvider appleJwtTokenProvider; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final RestTemplate restTemplate = new RestTemplate(); + + @Transactional(readOnly = true) + public ResponseEntity handleAppleOAuthCallback(Map params, HttpServletResponse response) throws Exception { + + if (params.containsKey("error")) { + return ResponseEntity.status(HttpStatus.SEE_OTHER).header("Location", "elswhere://?error=access_denied").build(); + } + + try { + HttpEntity> request = createAppleAuthTokenRequest(params); + ResponseEntity tokenResponse = restTemplate.exchange(appleTokenUri, HttpMethod.POST, request, Map.class); + + if (tokenResponse.getStatusCode().is2xxSuccessful()) { + + Map userInfo = JwtDecoder.decode((String) tokenResponse.getBody().get("id_token")); + + Optional optionalUser = userRepository.findBySocialId(userInfo.get("sub").toString()); + User user; + if (optionalUser.isEmpty()) { + user = User.builder() + .socialId(userInfo.get("sub").toString()) + .socialType(SocialType.APPLE) + .email(userInfo.get("email").toString()) + .name(fullName) + .nickname(userService.createRandomNickname()) + .userStatus(UserStatus.ACTIVE) + .userRole(UserRole.USER) + .build(); + + String signupToken = CodeGenerator.generateUUIDCode(); + signupAuthRepository.setAuthPayload(signupToken, OAUTH_AUTH_NAME, user, Instant.now(clock)); + + // 서비스 이용 약관 페이지로 리다이렉션 + String redirectUrl = "elswhere://terms?signup_token=" + signupToken; + return ResponseEntity.status(302).header("Location", redirectUrl).build(); + } else { + user = optionalUser.get(); + + // 백엔드 서버에서 서비스를 위한 자체 토큰 발급 + AuthenticationToken token = jwtProvider.issue(user); + userInfoService.cacheUserInfo(user.getId(), user); + + String redirectUrl = "elswhere://callback?access_token=" + token.getAccessToken() + "&refresh_token=" + token.getRefreshToken(); + return ResponseEntity.status(302).header("Location", redirectUrl).build(); + } + } else { + response.sendRedirect("elswhere://?error=invalid_token"); + throw new FailedToReceiveGoogleOAuth2TokenException(); + } + } catch (Exception e) { + try { + response.sendRedirect("elswhere://?error=invalid_token"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + throw new FailedOAuthCallbackProcessingException(e); + } + } + + private HttpEntity> createAppleAuthTokenRequest(Map params) throws Exception { + + // 최초 인증 시에만 이름이 옴 + String authorizationCode = params.get("code").toString(); + String firstName; + String lastName; + + // 'userJson'을 JSON으로 변환 + Map userMap; + if (Objects.nonNull(params.get("user"))) { + userMap = objectMapper.readValue(params.get("user").toString(), Map.class); + + // 'userMap'에서 사용자의 이름과 이메일 추출 + if (!userMap.isEmpty()) { + Map nameMap = (Map) userMap.get("name"); + if (Objects.nonNull(nameMap)) { + firstName = (String) nameMap.get("firstName"); + lastName = (String) nameMap.get("lastName"); + fullName = (lastName + " " + firstName).trim(); + } + } + } + + // JWT 생성 + String clientSecret = appleJwtTokenProvider.createJwtToken(appleClientId, appleTeamId, appleKeyId, appleKeyPath); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + // Apple에 토큰 요청 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("client_id", appleClientId); + body.add("client_secret", clientSecret); + body.add("code", authorizationCode); // 클라이언트에서 받은 authorization_code + body.add("grant_type", "authorization_code"); + body.add("redirect_uri", appleRedirectUriV2); // 콜백 URL + + return new HttpEntity<>(body, headers); + } + + public String getAuthorizationUri() { + return appleAuthorizationUri + + "?client_id=" + appleClientId + + "&redirect_uri=" + appleRedirectUriV2 + + "&response_type=code" + + "&response_mode=form_post" + + "&scope=name email"; + } +} \ No newline at end of file