From 05f7ca0fd50916d531cf9488bfd468c03d8886c6 Mon Sep 17 00:00:00 2001 From: bricksky Date: Fri, 27 Jun 2025 02:57:32 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[FEATURE]=20=EC=95=A0=ED=94=8C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84:=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=A0=84=EB=8B=AC=20=EB=B6=80=EB=B6=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/calendar/dto/PostCalendarDto.java | 2 +- .../global/exception/BaseException.java | 6 ++ .../jugger/global/exception/ErrorCode.java | 7 ++ .../global/security/AppleJwtGenerator.java | 74 ++++++++++++++++ .../security/ApplePublicKeyProvider.java | 44 ++++++++++ .../jugger/global/security/JwtValidator.java | 66 ++++++++++++++ .../token/controller/AuthController.java | 30 ++++--- .../token/domain/AppleLoginRequest.java | 7 ++ .../token/domain/AppleProperties.java | 65 ++++++++++++++ .../token/domain/AppleSignUpRequest.java | 27 ++++++ .../token/domain/AppleTokenInfoResponse.java | 21 +++++ .../token/domain/AppleUserResponse.java | 4 + .../token/domain/RefreshTokenRequest.java | 2 +- .../token/service/AppleOAuthService.java | 86 +++++++++++++++++++ .../token/service/AppleTokenService.java | 71 +++++++++++++++ 15 files changed, 498 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java create mode 100644 src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java create mode 100644 src/main/java/com/usememo/jugger/global/security/JwtValidator.java create mode 100644 src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java create mode 100644 src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java create mode 100644 src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java create mode 100644 src/main/java/com/usememo/jugger/global/security/token/domain/AppleTokenInfoResponse.java create mode 100644 src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java create mode 100644 src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java create mode 100644 src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java diff --git a/src/main/java/com/usememo/jugger/domain/calendar/dto/PostCalendarDto.java b/src/main/java/com/usememo/jugger/domain/calendar/dto/PostCalendarDto.java index 638ad2a..1231e0b 100644 --- a/src/main/java/com/usememo/jugger/domain/calendar/dto/PostCalendarDto.java +++ b/src/main/java/com/usememo/jugger/domain/calendar/dto/PostCalendarDto.java @@ -15,4 +15,4 @@ public class PostCalendarDto { private String place; private Instant alarm; private String description; -} +} \ No newline at end of file diff --git a/src/main/java/com/usememo/jugger/global/exception/BaseException.java b/src/main/java/com/usememo/jugger/global/exception/BaseException.java index 3ab5122..6e733d5 100644 --- a/src/main/java/com/usememo/jugger/global/exception/BaseException.java +++ b/src/main/java/com/usememo/jugger/global/exception/BaseException.java @@ -12,4 +12,10 @@ public BaseException(ErrorCode errorCode) { this.errorCode = errorCode; this.message = errorCode.getMessage(); } + + public BaseException(ErrorCode errorCode, Throwable cause) { + super(errorCode.getMessage(), cause); // RuntimeException에 메시지 + 원인 등록 + this.errorCode = errorCode; + this.message = errorCode.getMessage(); + } } diff --git a/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java b/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java index 5e0a15f..9da0dd7 100644 --- a/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java +++ b/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java @@ -42,6 +42,13 @@ public enum ErrorCode { GOOGLE_USER_NOT_FOUND(BAD_REQUEST,404,"구글 유저가 존재하지 않습니다."), GOOGLE_NO_NAME(BAD_REQUEST,404,"이름이 존재하지 않습니다."), + APPLE_USER_NOT_FOUND(BAD_REQUEST, 430, "존재하지 않는 Apple 회원입니다."), + APPLE_TOKEN_REQUEST_FAILED(BAD_REQUEST, 431, "Apple 인가 코드로 토큰을 요청하는 데 실패했습니다."), + APPLE_CLIENT_SECRET_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 432, "Apple client secret 생성에 실패했습니다."), + APPLE_USERINFO_MISSING(BAD_REQUEST, 433, "Apple 사용자 정보가 불완전합니다."), + APPLE_TOKEN_PARSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 434, "Apple id_token 파싱에 실패했습니다."), + + JWT_KEY_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 422, "JWT 키 생성에 실패했습니다."), JWT_ACCESS_TOKEN_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 423, "액세스 토큰 생성 실패"), diff --git a/src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java b/src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java new file mode 100644 index 0000000..5763ce8 --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java @@ -0,0 +1,74 @@ +package com.usememo.jugger.global.security; + +import com.usememo.jugger.global.security.token.domain.AppleProperties; +import io.jsonwebtoken.Jwts; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class AppleJwtGenerator { + private final AppleProperties appleProperties; + private final ResourceLoader resourceLoader; + private static final String APPLE_AUDIENCE = "https://appleid.apple.com"; + + @Autowired + public AppleJwtGenerator(AppleProperties appleProperties, ResourceLoader resourceLoader) { + this.appleProperties = appleProperties; + this.resourceLoader = resourceLoader; + } + + public String createClientSecret() + throws java.io.IOException, + NoSuchAlgorithmException, + InvalidKeySpecException, + InvalidKeyException { + Date now = new Date(); + Date expiration = new Date(now.getTime() + 3600_000); // 유효시간: 1시간 + + return Jwts.builder() + .header() + .keyId(appleProperties.getKeyId()) + .and() + .subject(appleProperties.getClientId()) + .issuer(appleProperties.getTeamId()) + .audience() + .add(APPLE_AUDIENCE) + .and() + .expiration(expiration) + .signWith(getPrivateKey(), Jwts.SIG.ES256) + .compact(); + } + + private PrivateKey getPrivateKey() + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + Resource resource = resourceLoader.getResource(appleProperties.getPrivateKeyLocation()); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(resource.getInputStream()))) { + String keyContent = reader.lines().collect(Collectors.joining("\n")); + String key = + keyContent + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + byte[] encoded = Base64.getDecoder().decode(key); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(keySpec); + } + } +} diff --git a/src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java b/src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java new file mode 100644 index 0000000..f7a118e --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java @@ -0,0 +1,44 @@ +package com.usememo.jugger.global.security; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.net.URL; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class ApplePublicKeyProvider { + + private static final String APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys"; + + private final Map keyCache = new ConcurrentHashMap<>(); + + public JWK getKeyById(String keyId) { + try { + // 이미 캐시에 있다면 재사용 + if (keyCache.containsKey(keyId)) { + return keyCache.get(keyId); + } + + // Apple 서버에서 공개키 가져오기 + JWKSet jwkSet = JWKSet.load(new URL(APPLE_KEYS_URL)); + for (JWK key : jwkSet.getKeys()) { + keyCache.put(key.getKeyID(), key); + } + + JWK foundKey = keyCache.get(keyId); + if (foundKey == null) { + throw new IllegalArgumentException("Apple 공개키에서 keyId를 찾을 수 없습니다: " + keyId); + } + + return foundKey; + } catch (Exception e) { + log.error("Apple 공개키 로딩 실패", e); + throw new IllegalArgumentException("Apple 공개키 로딩 실패", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/usememo/jugger/global/security/JwtValidator.java b/src/main/java/com/usememo/jugger/global/security/JwtValidator.java new file mode 100644 index 0000000..1013f16 --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/JwtValidator.java @@ -0,0 +1,66 @@ +package com.usememo.jugger.global.security; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jwt.SignedJWT; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.text.ParseException; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtValidator { + + private final ApplePublicKeyProvider keyProvider; + + @Value("${apple.client-id}") + private String clientId; + + public SignedJWT validate(String idToken) { + try { + SignedJWT jwt = SignedJWT.parse(idToken); + JWSHeader header = jwt.getHeader(); + + // 알고리즘 확인 + if (!JWSAlgorithm.RS256.equals(header.getAlgorithm())) { + throw new IllegalArgumentException("Unexpected JWS algorithm: " + header.getAlgorithm()); + } + + // 공개키로 서명 검증 + var jwk = keyProvider.getKeyById(header.getKeyID()); + JWSVerifier verifier = new RSASSAVerifier(jwk.toRSAKey()); + + if (!jwt.verify(verifier)) { + throw new IllegalArgumentException("Apple ID Token 서명 검증 실패"); + } + + // 클레임 검증 + var claims = jwt.getJWTClaimsSet(); + Date now = new Date(); + + if (claims.getExpirationTime() == null || now.after(claims.getExpirationTime())) { + throw new IllegalArgumentException("Apple ID Token 만료됨"); + } + + if (!"https://appleid.apple.com".equals(claims.getIssuer())) { + throw new IllegalArgumentException("잘못된 iss: " + claims.getIssuer()); + } + + if (!claims.getAudience().contains(clientId)) { + throw new IllegalArgumentException("잘못된 aud: " + claims.getAudience()); + } + + return jwt; + + } catch (ParseException e) { + throw new IllegalArgumentException("Apple ID Token 파싱 실패", e); + } catch (Exception e) { + throw new IllegalArgumentException("Apple ID Token 검증 실패", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java b/src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java index d404f52..b09402b 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java +++ b/src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java @@ -6,6 +6,8 @@ import java.util.UUID; import java.util.logging.Logger; +import com.usememo.jugger.global.security.token.domain.*; +import com.usememo.jugger.global.security.token.service.AppleOAuthService; import org.apache.el.parser.Token; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -27,18 +29,7 @@ import com.usememo.jugger.global.exception.KakaoException; import com.usememo.jugger.global.security.CustomOAuth2User; import com.usememo.jugger.global.security.JwtTokenProvider; -import com.usememo.jugger.global.security.token.domain.GoogleLoginRequest; -import com.usememo.jugger.global.security.token.domain.GoogleSignupRequest; -import com.usememo.jugger.global.security.token.domain.KakaoLoginRequest; - -import com.usememo.jugger.global.security.token.domain.KakaoLogoutResponse; -import com.usememo.jugger.global.security.token.domain.KakaoSignUpRequest; - -import com.usememo.jugger.global.security.token.domain.LogOutRequest; -import com.usememo.jugger.global.security.token.domain.LogOutResponse; -import com.usememo.jugger.global.security.token.domain.NewTokenResponse; -import com.usememo.jugger.global.security.token.domain.RefreshTokenRequest; -import com.usememo.jugger.global.security.token.domain.TokenResponse; + import com.usememo.jugger.global.security.token.repository.RefreshTokenRepository; import com.usememo.jugger.global.security.token.service.GoogleOAuthService; import com.usememo.jugger.global.security.token.service.KakaoOAuthService; @@ -56,6 +47,7 @@ public class AuthController { private final KakaoOAuthService kakaoService; private final GoogleOAuthService googleOAuthService; + private final AppleOAuthService appleOAuthService; @Operation(summary = "[POST] refresh token으로 새로운 access token 발급") @@ -109,4 +101,18 @@ public Mono> signUpGoogle(@RequestBody GoogleSignu .map(token-> ResponseEntity.ok().body(token)); } + + @Operation(summary = "[POST] 애플 로그인") + @PostMapping("/apple") + public Mono> loginByApple(@RequestBody AppleLoginRequest appleLoginRequest){ + return appleOAuthService.loginWithApple(appleLoginRequest.code()) + .map(token -> ResponseEntity.ok().body(token)); + } + + @Operation(summary = "[POST] 애플 회원가입") + @PostMapping("/apple/signup") + public Mono> signUpApple(@RequestBody AppleSignUpRequest appleSignUpRequest){ + return appleOAuthService.signUpApple(appleSignUpRequest) + .map(token -> ResponseEntity.ok().body(token)); + } } diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java new file mode 100644 index 0000000..bfc0f85 --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java @@ -0,0 +1,7 @@ +package com.usememo.jugger.global.security.token.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record AppleLoginRequest(@Schema(description = "애플 인가 코드", example = "dY94g8JaZ1Ki1s...") + String code) { +} \ No newline at end of file diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java new file mode 100644 index 0000000..20fbe1b --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java @@ -0,0 +1,65 @@ +package com.usememo.jugger.global.security.token.domain; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "apple") +public class AppleProperties { + + private String clientId; + private String teamId; + private String keyId; + private String privateKeyLocation; + private String privateKey; + private String redirectUri; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getTeamId() { + return teamId; + } + + public void setTeamId(String teamId) { + this.teamId = teamId; + } + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public String getPrivateKeyLocation() { + return privateKeyLocation; + } + + public void setPrivateKeyLocation(String privateKeyLocation) { + this.privateKeyLocation = privateKeyLocation; + } + + public String getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } +} diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java new file mode 100644 index 0000000..74d46b0 --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java @@ -0,0 +1,27 @@ +package com.usememo.jugger.global.security.token.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +public record AppleSignUpRequest(@Schema(description = "사용자 이름", example = "홍길동", required = true) + String name, + + @Schema(description = "이메일 주소", example = "hong@example.com", required = true) + String email, + + @Schema(description = "약관 동의 정보", required = true) + KakaoSignUpRequest.Terms terms +) { + @Schema(name = "Terms", description = "약관 동의 항목들") + @Data + public static class Terms { + @Schema(description = "서비스 이용약관 동의 여부", example = "true", required = true) + private boolean termsOfService; + + @Schema(description = "개인정보 처리방침 동의 여부", example = "true", required = true) + private boolean privacyPolicy; + + @Schema(description = "마케팅 정보 수신 동의 여부", example = "false", required = true) + private boolean marketing; + } +} \ No newline at end of file diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleTokenInfoResponse.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleTokenInfoResponse.java new file mode 100644 index 0000000..41389d7 --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleTokenInfoResponse.java @@ -0,0 +1,21 @@ +//package com.usememo.jugger.global.security.token.domain; +// +//import lombok.AllArgsConstructor; +//import lombok.Builder; +//import lombok.Data; +//import lombok.NoArgsConstructor; +// +//@Data +//@Builder +//@NoArgsConstructor +//@AllArgsConstructor +//public class AppleTokenInfoResponse { +// private String sub; +// private String email; +// private Boolean emailVerified; +// private Boolean isPrivateEmail; +// +// public boolean isValid() { +// return sub != null && !sub.isEmpty(); +// } +//} diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java new file mode 100644 index 0000000..1854e5f --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java @@ -0,0 +1,4 @@ +package com.usememo.jugger.global.security.token.domain; + +public record AppleUserResponse(String sub, String email) { +} diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/RefreshTokenRequest.java b/src/main/java/com/usememo/jugger/global/security/token/domain/RefreshTokenRequest.java index 13d2dc1..a99e29a 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/domain/RefreshTokenRequest.java +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/RefreshTokenRequest.java @@ -1,4 +1,4 @@ package com.usememo.jugger.global.security.token.domain; public record RefreshTokenRequest(String refreshToken) { -} +} \ No newline at end of file diff --git a/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java b/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java new file mode 100644 index 0000000..ac08a95 --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java @@ -0,0 +1,86 @@ +package com.usememo.jugger.global.security.token.service; + +import com.usememo.jugger.domain.user.entity.User; +import com.usememo.jugger.domain.user.repository.UserRepository; +import com.usememo.jugger.global.exception.BaseException; +import com.usememo.jugger.global.exception.ErrorCode; +import com.usememo.jugger.global.security.JwtTokenProvider; +import com.usememo.jugger.global.security.token.domain.AppleSignUpRequest; +import com.usememo.jugger.global.security.token.domain.AppleUserResponse; +import com.usememo.jugger.global.security.token.domain.TokenResponse; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.ui.Model; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AppleOAuthService { + private final AppleTokenService appleTokenService; + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + + public Mono loginWithApple(String code) { + return appleTokenService.exchangeCodeForTokens(code) + .flatMap(appleTokenService::extractUserFromIdToken) + .flatMap(this::findUser) + .flatMap(user -> jwtTokenProvider.createTokenBundle(user.getUuid())); + } + + private Mono findUser(AppleUserResponse userInfo) { + return userRepository.findByEmailAndDomain(userInfo.email(), "apple") + .switchIfEmpty(Mono.error(new BaseException(ErrorCode.APPLE_USER_NOT_FOUND))); + } + + public Mono signUpApple(AppleUserResponse userInfo, String name, User.Terms terms) { + return userRepository.findByEmailAndDomain(userInfo.email(), "apple") + .flatMap(user -> Mono.error(new BaseException(ErrorCode.DUPLICATE_USER))) + .switchIfEmpty(Mono.defer(() -> { + String uuid = UUID.randomUUID().toString(); + User newUser = User.builder() + .uuid(uuid) + .email(userInfo.email()) + .name(name) + .domain("apple") + .terms(terms) + .build(); + + return userRepository.save(newUser) + .flatMap(saved -> jwtTokenProvider.createTokenBundle(saved.getUuid())); + })); + } + + public Mono signUpApple(AppleSignUpRequest request) { + String email = request.email(); + String name = request.name(); + + return userRepository.findByEmailAndDomainAndName(email, "apple", name) + .flatMap(existingUser -> Mono.error(new BaseException(ErrorCode.DUPLICATE_USER))) + .switchIfEmpty(Mono.defer(() -> { + String uuid = UUID.randomUUID().toString(); + + User.Terms terms = new User.Terms(); + terms.setMarketing(request.terms().isMarketing()); + terms.setPrivacyPolicy(request.terms().isPrivacyPolicy()); + terms.setTermsOfService(request.terms().isTermsOfService()); + + User user = User.builder() + .uuid(uuid) + .name(name) + .email(email) + .terms(terms) + .domain("apple") + .build(); + + return userRepository.save(user) + .flatMap(savedUser -> jwtTokenProvider.createTokenBundle(savedUser.getUuid())); + })); + } +} \ No newline at end of file diff --git a/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java b/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java new file mode 100644 index 0000000..e69ae71 --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java @@ -0,0 +1,71 @@ +package com.usememo.jugger.global.security.token.service; + +import com.nimbusds.jwt.SignedJWT; +import com.usememo.jugger.global.exception.BaseException; +import com.usememo.jugger.global.exception.ErrorCode; +import com.usememo.jugger.global.security.AppleJwtGenerator; +import com.usememo.jugger.global.security.token.domain.AppleProperties; +import com.usememo.jugger.global.security.token.domain.AppleUserResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; + +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.text.ParseException; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class AppleTokenService { + private final WebClient webClient = WebClient.create(); + private final AppleJwtGenerator appleJwtGenerator; + private final AppleProperties appleProperties; + + // 1. 인가 코드로 Apple 토큰 요청 + public Mono> exchangeCodeForTokens(String code) { + return Mono.fromCallable(() -> appleJwtGenerator.createClientSecret()) + .flatMap(clientSecret -> + webClient.post() + .uri("https://appleid.apple.com/auth/token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData("grant_type", "authorization_code") + .with("code", code) + .with("redirect_uri", appleProperties.getRedirectUri()) + .with("client_id", appleProperties.getClientId()) + .with("client_secret", clientSecret)) + .retrieve() + .onStatus(status -> !status.is2xxSuccessful(), + response -> Mono.error(new BaseException(ErrorCode.APPLE_TOKEN_REQUEST_FAILED))) + .bodyToMono(new ParameterizedTypeReference>() {}) + ) + .onErrorResume(e -> Mono.error(new BaseException(ErrorCode.APPLE_CLIENT_SECRET_FAILED, e))); + } + + + + // 2. id_token 검증 및 사용자 정보 추출 + public Mono extractUserFromIdToken(Map tokenMap) { + return Mono.fromCallable(() -> { + String idToken = (String) tokenMap.get("id_token"); + SignedJWT jwt = SignedJWT.parse(idToken); + var claims = jwt.getJWTClaimsSet(); + + String email = claims.getStringClaim("email"); + String sub = claims.getSubject(); + + if (sub == null || email == null) { + throw new BaseException(ErrorCode.APPLE_USERINFO_MISSING); + } + + return new AppleUserResponse(sub, email); + }).onErrorResume(e -> Mono.error(new BaseException(ErrorCode.APPLE_TOKEN_PARSE_ERROR, e))); + } +} From fcf661ff62178a4e9f749d3091c4212e0f9993fb Mon Sep 17 00:00:00 2001 From: bricksky Date: Fri, 27 Jun 2025 18:31:18 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[FEATURE]=20=EC=95=A0=ED=94=8C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84:=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=EB=B6=80=EB=B6=84=20=EB=B0=8F=20DTO=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../token/domain/AppleTokenInfoResponse.java | 21 ------------------- .../token/domain/AppleUserResponse.java | 2 +- .../token/service/AppleOAuthService.java | 18 ---------------- 3 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 src/main/java/com/usememo/jugger/global/security/token/domain/AppleTokenInfoResponse.java diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleTokenInfoResponse.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleTokenInfoResponse.java deleted file mode 100644 index 41389d7..0000000 --- a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleTokenInfoResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -//package com.usememo.jugger.global.security.token.domain; -// -//import lombok.AllArgsConstructor; -//import lombok.Builder; -//import lombok.Data; -//import lombok.NoArgsConstructor; -// -//@Data -//@Builder -//@NoArgsConstructor -//@AllArgsConstructor -//public class AppleTokenInfoResponse { -// private String sub; -// private String email; -// private Boolean emailVerified; -// private Boolean isPrivateEmail; -// -// public boolean isValid() { -// return sub != null && !sub.isEmpty(); -// } -//} diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java index 1854e5f..3588144 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java @@ -1,4 +1,4 @@ package com.usememo.jugger.global.security.token.domain; public record AppleUserResponse(String sub, String email) { -} +} \ No newline at end of file diff --git a/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java b/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java index ac08a95..b9f42f4 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java +++ b/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java @@ -39,24 +39,6 @@ private Mono findUser(AppleUserResponse userInfo) { .switchIfEmpty(Mono.error(new BaseException(ErrorCode.APPLE_USER_NOT_FOUND))); } - public Mono signUpApple(AppleUserResponse userInfo, String name, User.Terms terms) { - return userRepository.findByEmailAndDomain(userInfo.email(), "apple") - .flatMap(user -> Mono.error(new BaseException(ErrorCode.DUPLICATE_USER))) - .switchIfEmpty(Mono.defer(() -> { - String uuid = UUID.randomUUID().toString(); - User newUser = User.builder() - .uuid(uuid) - .email(userInfo.email()) - .name(name) - .domain("apple") - .terms(terms) - .build(); - - return userRepository.save(newUser) - .flatMap(saved -> jwtTokenProvider.createTokenBundle(saved.getUuid())); - })); - } - public Mono signUpApple(AppleSignUpRequest request) { String email = request.email(); String name = request.name(); From 1dbbe19c9d1df94fbcd92d42e91cf9aa4db3228f Mon Sep 17 00:00:00 2001 From: bricksky Date: Wed, 2 Jul 2025 17:50:48 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B0=98=EC=98=81(=EC=A3=BC=EC=84=9D=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jugger/global/config/WebClientConfig.java | 14 ++++++++++++++ .../security/token/controller/AuthController.java | 1 - .../security/token/domain/AppleProperties.java | 1 - .../security/token/service/AppleTokenService.java | 12 +++--------- 4 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/usememo/jugger/global/config/WebClientConfig.java diff --git a/src/main/java/com/usememo/jugger/global/config/WebClientConfig.java b/src/main/java/com/usememo/jugger/global/config/WebClientConfig.java new file mode 100644 index 0000000..c6815a9 --- /dev/null +++ b/src/main/java/com/usememo/jugger/global/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.usememo.jugger.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java b/src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java index b09402b..24b2cd8 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java +++ b/src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java @@ -8,7 +8,6 @@ import com.usememo.jugger.global.security.token.domain.*; import com.usememo.jugger.global.security.token.service.AppleOAuthService; -import org.apache.el.parser.Token; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java index 20fbe1b..90ba1e4 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java @@ -1,7 +1,6 @@ package com.usememo.jugger.global.security.token.domain; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; @Component diff --git a/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java b/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java index e69ae71..8076c32 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java +++ b/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java @@ -15,17 +15,12 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.text.ParseException; import java.util.Map; @Service @RequiredArgsConstructor public class AppleTokenService { - private final WebClient webClient = WebClient.create(); + private final WebClient webClient; private final AppleJwtGenerator appleJwtGenerator; private final AppleProperties appleProperties; @@ -44,13 +39,12 @@ public Mono> exchangeCodeForTokens(String code) { .retrieve() .onStatus(status -> !status.is2xxSuccessful(), response -> Mono.error(new BaseException(ErrorCode.APPLE_TOKEN_REQUEST_FAILED))) - .bodyToMono(new ParameterizedTypeReference>() {}) + .bodyToMono(new ParameterizedTypeReference>() { + }) ) .onErrorResume(e -> Mono.error(new BaseException(ErrorCode.APPLE_CLIENT_SECRET_FAILED, e))); } - - // 2. id_token 검증 및 사용자 정보 추출 public Mono extractUserFromIdToken(Map tokenMap) { return Mono.fromCallable(() -> { From a9a2b55c511521db3274f85d156fa4f610c4893c Mon Sep 17 00:00:00 2001 From: bricksky Date: Wed, 2 Jul 2025 18:23:43 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B0=98=EC=98=81(WebClient=20=EC=83=9D=EC=84=B1=EC=9E=90?= =?UTF-8?q?=20=EC=A3=BC=EC=9E=85,=20Lombok=20@Getter/@Setter=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/BaseException.java | 22 +++----- .../jugger/global/exception/ErrorCode.java | 1 + .../global/security/AppleJwtGenerator.java | 5 +- .../jugger/global/security/JwtValidator.java | 4 +- .../token/domain/AppleProperties.java | 52 ++----------------- .../token/service/AppleOAuthService.java | 3 ++ .../token/service/AppleTokenService.java | 4 +- 7 files changed, 23 insertions(+), 68 deletions(-) diff --git a/src/main/java/com/usememo/jugger/global/exception/BaseException.java b/src/main/java/com/usememo/jugger/global/exception/BaseException.java index 6e733d5..843018e 100644 --- a/src/main/java/com/usememo/jugger/global/exception/BaseException.java +++ b/src/main/java/com/usememo/jugger/global/exception/BaseException.java @@ -4,18 +4,12 @@ @Getter public class BaseException extends RuntimeException { - private final ErrorCode errorCode; - private final String message; - - public BaseException(ErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; - this.message = errorCode.getMessage(); - } - - public BaseException(ErrorCode errorCode, Throwable cause) { - super(errorCode.getMessage(), cause); // RuntimeException에 메시지 + 원인 등록 - this.errorCode = errorCode; - this.message = errorCode.getMessage(); - } + private final ErrorCode errorCode; + private final String message; + + public BaseException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.message = errorCode.getMessage(); + } } diff --git a/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java b/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java index 9da0dd7..6f39033 100644 --- a/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java +++ b/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java @@ -35,6 +35,7 @@ public enum ErrorCode { KAKAO_USER_NOT_FOUND(BAD_REQUEST,427,"존재하지 않는 회원입니다."), DUPLICATE_USER(BAD_REQUEST,428,"중복된 회원정보입니다."), + REQUIRED_TERMS_NOT_AGREED(BAD_REQUEST, 429, "필수 약관에 동의하지 않았습니다."), GOOGLE_LOGIN_FAIL(BAD_REQUEST,404,"로그인에 실패하였습니다."), diff --git a/src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java b/src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java index 5763ce8..2f3e223 100644 --- a/src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java +++ b/src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java @@ -35,10 +35,9 @@ public AppleJwtGenerator(AppleProperties appleProperties, ResourceLoader resourc public String createClientSecret() throws java.io.IOException, NoSuchAlgorithmException, - InvalidKeySpecException, - InvalidKeyException { + InvalidKeySpecException{ Date now = new Date(); - Date expiration = new Date(now.getTime() + 3600_000); // 유효시간: 1시간 + Date expiration = new Date(now.getTime() + 3600_000); return Jwts.builder() .header() diff --git a/src/main/java/com/usememo/jugger/global/security/JwtValidator.java b/src/main/java/com/usememo/jugger/global/security/JwtValidator.java index 1013f16..6e1d25a 100644 --- a/src/main/java/com/usememo/jugger/global/security/JwtValidator.java +++ b/src/main/java/com/usememo/jugger/global/security/JwtValidator.java @@ -16,6 +16,8 @@ @RequiredArgsConstructor public class JwtValidator { + private static final String APPLE_ISSUER = "https://appleid.apple.com"; + private final ApplePublicKeyProvider keyProvider; @Value("${apple.client-id}") @@ -47,7 +49,7 @@ public SignedJWT validate(String idToken) { throw new IllegalArgumentException("Apple ID Token 만료됨"); } - if (!"https://appleid.apple.com".equals(claims.getIssuer())) { + if (!APPLE_ISSUER.equals(claims.getIssuer())) { throw new IllegalArgumentException("잘못된 iss: " + claims.getIssuer()); } diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java index 90ba1e4..b07b1e4 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java @@ -1,8 +1,12 @@ package com.usememo.jugger.global.security.token.domain; +import lombok.Getter; +import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +@Getter +@Setter @Component @ConfigurationProperties(prefix = "apple") public class AppleProperties { @@ -13,52 +17,4 @@ public class AppleProperties { private String privateKeyLocation; private String privateKey; private String redirectUri; - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getTeamId() { - return teamId; - } - - public void setTeamId(String teamId) { - this.teamId = teamId; - } - - public String getKeyId() { - return keyId; - } - - public void setKeyId(String keyId) { - this.keyId = keyId; - } - - public String getPrivateKeyLocation() { - return privateKeyLocation; - } - - public void setPrivateKeyLocation(String privateKeyLocation) { - this.privateKeyLocation = privateKeyLocation; - } - - public String getPrivateKey() { - return privateKey; - } - - public void setPrivateKey(String privateKey) { - this.privateKey = privateKey; - } - - public String getRedirectUri() { - return redirectUri; - } - - public void setRedirectUri(String redirectUri) { - this.redirectUri = redirectUri; - } } diff --git a/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java b/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java index b9f42f4..2f71a4f 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java +++ b/src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java @@ -46,6 +46,9 @@ public Mono signUpApple(AppleSignUpRequest request) { return userRepository.findByEmailAndDomainAndName(email, "apple", name) .flatMap(existingUser -> Mono.error(new BaseException(ErrorCode.DUPLICATE_USER))) .switchIfEmpty(Mono.defer(() -> { + if (!request.terms().isTermsOfService() || !request.terms().isPrivacyPolicy()) { + return Mono.error(new BaseException(ErrorCode.REQUIRED_TERMS_NOT_AGREED)); + } String uuid = UUID.randomUUID().toString(); User.Terms terms = new User.Terms(); diff --git a/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java b/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java index 8076c32..d3ab9b1 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java +++ b/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java @@ -42,7 +42,7 @@ public Mono> exchangeCodeForTokens(String code) { .bodyToMono(new ParameterizedTypeReference>() { }) ) - .onErrorResume(e -> Mono.error(new BaseException(ErrorCode.APPLE_CLIENT_SECRET_FAILED, e))); + .onErrorResume(e -> Mono.error(new BaseException(ErrorCode.APPLE_CLIENT_SECRET_FAILED))); } // 2. id_token 검증 및 사용자 정보 추출 @@ -60,6 +60,6 @@ public Mono extractUserFromIdToken(Map tokenM } return new AppleUserResponse(sub, email); - }).onErrorResume(e -> Mono.error(new BaseException(ErrorCode.APPLE_TOKEN_PARSE_ERROR, e))); + }).onErrorResume(e -> Mono.error(new BaseException(ErrorCode.APPLE_TOKEN_PARSE_ERROR))); } } From 0c701d356d70c994289d7022ed2b5f6bdb20609c Mon Sep 17 00:00:00 2001 From: bricksky Date: Fri, 4 Jul 2025 20:48:30 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/ApplePublicKeyProvider.java | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java b/src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java index f7a118e..82e17f2 100644 --- a/src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java +++ b/src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java @@ -6,28 +6,52 @@ import org.springframework.stereotype.Component; import java.net.URL; +import java.time.Duration; +import java.time.Instant; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; @Slf4j @Component public class ApplePublicKeyProvider { private static final String APPLE_KEYS_URL = "https://appleid.apple.com/auth/keys"; + private static final Duration CACHE_DURATION = Duration.ofDays(15); private final Map keyCache = new ConcurrentHashMap<>(); + private volatile Instant lastCacheTime = Instant.MIN; + private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); public JWK getKeyById(String keyId) { + try { - // 이미 캐시에 있다면 재사용 - if (keyCache.containsKey(keyId)) { - return keyCache.get(keyId); + // 캐시 확인 및 만료 체크 + cacheLock.readLock().lock(); + try { + if (keyCache.containsKey(keyId) && !isCacheExpired()) { + return keyCache.get(keyId); + } + } finally { + cacheLock.readLock().unlock(); } - // Apple 서버에서 공개키 가져오기 - JWKSet jwkSet = JWKSet.load(new URL(APPLE_KEYS_URL)); - for (JWK key : jwkSet.getKeys()) { - keyCache.put(key.getKeyID(), key); + // 캐시 갱신 + cacheLock.writeLock().lock(); + try { + // 다른 스레드가 갱신했는지 재확인 + if (keyCache.containsKey(keyId) && !isCacheExpired()) { + return keyCache.get(keyId); + } + // Apple 서버에서 공개키 가져오기 + JWKSet jwkSet = JWKSet.load(new URL(APPLE_KEYS_URL)); + for (JWK key : jwkSet.getKeys()) { + keyCache.put(key.getKeyID(), key); + } + lastCacheTime = Instant.now(); + } finally { + cacheLock.writeLock().unlock(); } JWK foundKey = keyCache.get(keyId); @@ -41,4 +65,8 @@ public JWK getKeyById(String keyId) { throw new IllegalArgumentException("Apple 공개키 로딩 실패", e); } } + + private boolean isCacheExpired() { + return Duration.between(lastCacheTime, Instant.now()).compareTo(CACHE_DURATION) > 0; + } } \ No newline at end of file From 183620e986b821db0be38fbedd1795dcc5022721 Mon Sep 17 00:00:00 2001 From: bricksky Date: Fri, 4 Jul 2025 21:21:10 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B0=98=EC=98=81(JWT=20=EC=84=9C=EB=AA=85=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usememo/jugger/global/exception/ErrorCode.java | 1 + .../security/token/domain/AppleProperties.java | 2 +- .../security/token/service/AppleTokenService.java | 13 ++++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java b/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java index 6f39033..2c359b4 100644 --- a/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java +++ b/src/main/java/com/usememo/jugger/global/exception/ErrorCode.java @@ -48,6 +48,7 @@ public enum ErrorCode { APPLE_CLIENT_SECRET_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 432, "Apple client secret 생성에 실패했습니다."), APPLE_USERINFO_MISSING(BAD_REQUEST, 433, "Apple 사용자 정보가 불완전합니다."), APPLE_TOKEN_PARSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 434, "Apple id_token 파싱에 실패했습니다."), + APPLE_TOKEN_INVALID(BAD_REQUEST, 435, "Apple 토큰이 유효하지 않습니다."), diff --git a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java index b07b1e4..75a02c6 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java +++ b/src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java @@ -15,6 +15,6 @@ public class AppleProperties { private String teamId; private String keyId; private String privateKeyLocation; - private String privateKey; + //private String privateKey; private String redirectUri; } diff --git a/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java b/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java index d3ab9b1..06906ba 100644 --- a/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java +++ b/src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java @@ -4,6 +4,7 @@ import com.usememo.jugger.global.exception.BaseException; import com.usememo.jugger.global.exception.ErrorCode; import com.usememo.jugger.global.security.AppleJwtGenerator; +import com.usememo.jugger.global.security.JwtValidator; import com.usememo.jugger.global.security.token.domain.AppleProperties; import com.usememo.jugger.global.security.token.domain.AppleUserResponse; import lombok.RequiredArgsConstructor; @@ -23,6 +24,7 @@ public class AppleTokenService { private final WebClient webClient; private final AppleJwtGenerator appleJwtGenerator; private final AppleProperties appleProperties; + private final JwtValidator jwtValidator; // 1. 인가 코드로 Apple 토큰 요청 public Mono> exchangeCodeForTokens(String code) { @@ -49,7 +51,16 @@ public Mono> exchangeCodeForTokens(String code) { public Mono extractUserFromIdToken(Map tokenMap) { return Mono.fromCallable(() -> { String idToken = (String) tokenMap.get("id_token"); - SignedJWT jwt = SignedJWT.parse(idToken); + if(idToken == null){ + throw new BaseException(ErrorCode.APPLE_TOKEN_PARSE_ERROR); + } + SignedJWT jwt; + + try{ + jwt = jwtValidator.validate(idToken); + } catch (IllegalArgumentException e){ + throw new BaseException(ErrorCode.APPLE_TOKEN_INVALID); + } var claims = jwt.getJWTClaimsSet(); String email = claims.getStringClaim("email"); From 5849b722a767aca7897d4347170e81d0aae9030d Mon Sep 17 00:00:00 2001 From: bricksky Date: Wed, 9 Jul 2025 05:08:16 +0900 Subject: [PATCH 7/7] =?UTF-8?q?chore=20:=20CODEOWNERS=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=97=90=20=EA=B3=84=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5df43f1..8d1fe36 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @JjungminLee @minhyuk2 \ No newline at end of file +* @JjungminLee @minhyuk2 @bricksky