-
Notifications
You must be signed in to change notification settings - Fork 0
feat : apple 로그인 기능 구현 #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
## Walkthrough
애플 OAuth 인증 및 회원가입 기능이 새롭게 도입되었습니다. 이를 위해 여러 보안 컴포넌트, 서비스, DTO, 예외 코드가 추가되었으며, 컨트롤러에 애플 로그인/회원가입 엔드포인트가 구현되었습니다. 일부 파일에서는 불필요한 개행이 제거되었습니다.
## Changes
| 파일(들) | 변경 요약 |
|---|---|
| .../calendar/dto/PostCalendarDto.java<br>.../token/domain/RefreshTokenRequest.java | 파일 끝의 불필요한 개행 제거 (기능 변화 없음) |
| .../global/exception/BaseException.java | 들여쓰기 및 포맷 정리 (기능 변화 없음) |
| .../global/exception/ErrorCode.java | 애플 인증 관련 6개 에러코드 추가 (필수 약관 미동의 포함) |
| .../global/security/AppleJwtGenerator.java | 애플용 JWT client secret 생성 컴포넌트 신규 추가 |
| .../global/security/ApplePublicKeyProvider.java | 애플 공개키 조회 및 캐싱 컴포넌트 신규 추가 |
| .../global/security/JwtValidator.java | 애플 ID 토큰(JWT) 검증 컴포넌트 신규 추가 |
| .../global/security/token/controller/AuthController.java | 애플 로그인/회원가입용 POST 엔드포인트 2개 추가, AppleOAuthService 의존성 주입 |
| .../global/security/token/domain/AppleLoginRequest.java | 애플 로그인 요청용 record(DTO) 추가 |
| .../global/security/token/domain/AppleProperties.java | 애플 OAuth 관련 프로퍼티 바인딩용 컴포넌트 추가 |
| .../global/security/token/domain/AppleSignUpRequest.java | 애플 회원가입 요청용 record(DTO) 및 약관 동의 내포 클래스 추가 |
| .../global/security/token/domain/AppleUserResponse.java | 애플 사용자 정보 응답 record(DTO) 추가 |
| .../global/security/token/service/AppleOAuthService.java | 애플 OAuth 로그인/회원가입 서비스 신규 추가 (Mono 기반 비동기 처리) |
| .../global/security/token/service/AppleTokenService.java | 애플 OAuth 토큰 교환 및 사용자 추출 서비스 신규 추가 (Mono 기반) |
| .../global/config/WebClientConfig.java | WebClient 빈 등록을 위한 설정 클래스 신규 추가 |
## Sequence Diagram(s)
```mermaid
sequenceDiagram
participant Client
participant AuthController
participant AppleOAuthService
participant AppleTokenService
participant AppleJwtGenerator
participant JwtValidator
participant UserRepository
participant JwtTokenProvider
Client->>AuthController: POST /auth/apple (AppleLoginRequest)
AuthController->>AppleOAuthService: loginWithApple(code)
AppleOAuthService->>AppleTokenService: exchangeCodeForTokens(code)
AppleTokenService->>AppleJwtGenerator: createClientSecret()
AppleTokenService->>Apple: POST /auth/token (애플 서버)
AppleTokenService->>JwtValidator: validate(id_token)
AppleTokenService->>AppleOAuthService: AppleUserResponse 반환
AppleOAuthService->>UserRepository: findByEmailAndDomain
alt 유저 있음
AppleOAuthService->>JwtTokenProvider: generateTokenBundle(user)
AppleOAuthService->>AuthController: TokenResponse 반환
AuthController->>Client: 200 OK + TokenResponse
else 유저 없음
AppleOAuthService->>AuthController: APPLE_USER_NOT_FOUND 예외
AuthController->>Client: 에러 응답
end sequenceDiagram
participant Client
participant AuthController
participant AppleOAuthService
participant UserRepository
participant JwtTokenProvider
Client->>AuthController: POST /auth/apple/signup (AppleSignUpRequest)
AuthController->>AppleOAuthService: signUpApple(request)
AppleOAuthService->>UserRepository: findByEmailAndDomainAndName
alt 이미 존재
AppleOAuthService->>AuthController: DUPLICATE_USER 예외
AuthController->>Client: 에러 응답
else 신규 회원
AppleOAuthService->>UserRepository: save(new User)
AppleOAuthService->>JwtTokenProvider: generateTokenBundle(newUser)
AppleOAuthService->>AuthController: TokenResponse 반환
AuthController->>Client: 200 OK + TokenResponse
end
Suggested reviewers
Poem
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 11
🧹 Nitpick comments (10)
src/main/java/com/usememo/jugger/domain/calendar/dto/PostCalendarDto.java (1)
18-18
: 파일 끝 개행(EOF newline) 유지 권장마지막
}
뒤 개행이 사라졌습니다. POSIX 텍스트 파일 규약에 따르면 파일은 개행 문자로 끝나는 것이 바람직하며, 일부 Git diff·lint 도구가 경고를 표시할 수 있습니다. 무시해도 동작에는 영향이 없지만, 일관성을 위해 개행을 복구하는 편이 좋습니다.@@ private String description; -} +} +src/main/java/com/usememo/jugger/global/exception/ErrorCode.java (1)
50-51
: 불필요한 빈 줄을 제거하는 것을 권장합니다.코드 가독성을 위해 불필요한 연속된 빈 줄을 제거하는 것이 좋겠습니다.
APPLE_TOKEN_PARSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 434, "Apple id_token 파싱에 실패했습니다."), - - + JWT_KEY_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 422, "JWT 키 생성에 실패했습니다."),src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java (1)
7-65
: Lombok을 활용한 보일러플레이트 코드 제거getter/setter 메서드가 많아 코드가 장황합니다. Lombok을 사용하여 간결하게 만들 수 있습니다.
+import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter @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; - } - // ... 나머지 getter/setter 메서드 제거 }src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java (3)
29-33
: 불필요한 @Autowired 어노테이션 제거Spring 4.3 이후 생성자가 하나인 경우
@Autowired
는 필요하지 않습니다.- @Autowired public AppleJwtGenerator(AppleProperties appleProperties, ResourceLoader resourceLoader) {
57-73
: 프라이빗 키 캐싱으로 성능 개선매번 파일에서 프라이빗 키를 읽는 것은 비효율적입니다. 키를 캐싱하여 성능을 개선할 수 있습니다.
public class AppleJwtGenerator { private final AppleProperties appleProperties; private final ResourceLoader resourceLoader; private static final String APPLE_AUDIENCE = "https://appleid.apple.com"; + private volatile PrivateKey cachedPrivateKey; // ... 기존 코드 ... private PrivateKey getPrivateKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + if (cachedPrivateKey != null) { + return cachedPrivateKey; + } + + synchronized (this) { + if (cachedPrivateKey != null) { + return cachedPrivateKey; + } + Resource resource = resourceLoader.getResource(appleProperties.getPrivateKeyLocation()); try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { String keyContent = reader.lines().collect(Collectors.joining("\n")); + + // PEM 형식 검증 + if (!keyContent.contains("-----BEGIN PRIVATE KEY-----") || + !keyContent.contains("-----END PRIVATE KEY-----")) { + throw new IllegalArgumentException("유효하지 않은 PEM 형식의 프라이빗 키입니다"); + } + 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); + cachedPrivateKey = keyFactory.generatePrivate(keySpec); + return cachedPrivateKey; } + } } }
41-41
: 토큰 유효 시간을 설정 가능하도록 개선현재 1시간으로 하드코딩되어 있는 토큰 유효 시간을 설정 가능하도록 만드는 것이 좋습니다.
+ @Value("${apple.client-secret-expiration-millis:3600000}") + private long clientSecretExpirationMillis; + public String createClientSecret() throws java.io.IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { Date now = new Date(); - Date expiration = new Date(now.getTime() + 3600_000); // 유효시간: 1시간 + Date expiration = new Date(now.getTime() + clientSecretExpirationMillis);src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java (1)
4-4
: 불필요한 import 제거record에서는
lombok.Data
가 필요하지 않습니다.-import lombok.Data;
src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java (1)
52-53
: 불필요한 빈 줄 제거- -src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java (2)
12-12
: 사용하지 않는 import 제거-import lombok.Value;
15-16
: 사용하지 않는 import 제거-import org.springframework.ui.Model; -import org.springframework.web.reactive.function.client.WebClient;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
src/main/java/com/usememo/jugger/domain/calendar/dto/PostCalendarDto.java
(1 hunks)src/main/java/com/usememo/jugger/global/exception/BaseException.java
(1 hunks)src/main/java/com/usememo/jugger/global/exception/ErrorCode.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/JwtValidator.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java
(4 hunks)src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/token/domain/RefreshTokenRequest.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java
(1 hunks)src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java
(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/main/java/com/usememo/jugger/global/security/JwtValidator.java (2)
src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java (1)
Component
(23-74)src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java (1)
Component
(7-65)
src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java (2)
src/main/java/com/usememo/jugger/global/security/JwtValidator.java (1)
Component
(15-66)src/main/java/com/usememo/jugger/global/security/AppleJwtGenerator.java (1)
Component
(23-74)
src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java (1)
src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java (1)
Schema
(15-26)
🔇 Additional comments (7)
src/main/java/com/usememo/jugger/global/exception/ErrorCode.java (1)
45-50
: 애플 OAuth 에러 코드 추가가 적절히 구현되었습니다.새로 추가된 애플 관련 에러 코드들이 기존 패턴과 일관성 있게 구현되었으며, HTTP 상태 코드도 적절히 선택되었습니다. 클라이언트 오류는 BAD_REQUEST, 서버 오류는 INTERNAL_SERVER_ERROR로 분류한 것이 올바릅니다.
src/main/java/com/usememo/jugger/global/security/token/domain/RefreshTokenRequest.java (1)
4-4
: 포맷팅 정리가 잘 되었습니다.불필요한 개행 제거로 코드가 더 깔끔해졌습니다.
src/main/java/com/usememo/jugger/global/security/token/domain/AppleUserResponse.java (1)
1-4
: 애플 사용자 응답 레코드가 적절히 구현되었습니다.
sub
(subject identifier)와src/main/java/com/usememo/jugger/global/exception/BaseException.java (1)
16-20
: 예외 체이닝을 지원하는 생성자 추가가 우수합니다.
Throwable cause
를 받는 새로운 생성자를 추가하여 원인 예외를 보존할 수 있게 되었습니다. 이는 디버깅과 로깅에 매우 유용하며, 예외 처리의 모범 사례를 잘 따르고 있습니다. 한국어 주석도 코드 이해에 도움이 됩니다.src/main/java/com/usememo/jugger/global/security/token/domain/AppleLoginRequest.java (1)
1-7
: 애플 로그인 요청 레코드가 잘 구현되었습니다.Swagger 어노테이션을 포함한 레코드 클래스로 API 문서화가 잘 되어 있습니다. 한국어 설명과 예시 값 제공으로 API 사용자가 이해하기 쉽게 작성되었습니다. 레코드 사용으로 불변성도 보장됩니다.
src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java (1)
105-117
: Apple OAuth 엔드포인트 구현 확인Apple 로그인 및 회원가입 엔드포인트가 다른 OAuth 제공자들과 일관된 패턴으로 구현되었습니다.
src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java (1)
46-47
: 중복 사용자 확인 로직 검토이메일, 도메인, 이름으로 모두 확인하는 로직이 너무 제한적일 수 있습니다. 일반적으로 동일한 도메인(apple)에서는 이메일만으로 중복을 확인하는 것이 일반적입니다.
- return userRepository.findByEmailAndDomainAndName(email, "apple", name) + return userRepository.findByEmailAndDomain(email, "apple")비즈니스 요구사항을 확인해 주세요. Apple 사용자의 경우 동일한 이메일로 다른 이름을 가진 계정을 허용해야 하는지 검토가 필요합니다.
src/main/java/com/usememo/jugger/global/security/ApplePublicKeyProvider.java
Show resolved
Hide resolved
@Value("${apple.client-id}") | ||
private String clientId; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
의존성 주입 방식 일관성 개선
@RequiredArgsConstructor
를 사용하면서 @Value
어노테이션을 함께 사용하는 것은 일관성이 없습니다. 생성자 주입으로 통일하는 것을 권장합니다.
- @Value("${apple.client-id}")
- private String clientId;
+ private final String clientId;
+
+ public JwtValidator(ApplePublicKeyProvider keyProvider,
+ @Value("${apple.client-id}") String clientId) {
+ this.keyProvider = keyProvider;
+ this.clientId = clientId;
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
@Value("${apple.client-id}") | |
private String clientId; | |
// replace field-injected clientId with constructor injection | |
- @Value("${apple.client-id}") | |
- private String clientId; | |
+ private final String clientId; | |
+ | |
+ public JwtValidator(ApplePublicKeyProvider keyProvider, | |
+ @Value("${apple.client-id}") String clientId) { | |
+ this.keyProvider = keyProvider; | |
+ this.clientId = clientId; | |
+ } |
🤖 Prompt for AI Agents
In src/main/java/com/usememo/jugger/global/security/JwtValidator.java around
lines 21 to 23, the clientId field is injected using @Value annotation, which is
inconsistent with the use of @RequiredArgsConstructor for dependency injection.
To fix this, remove the @Value annotation and the field injection, then add
clientId as a final field and include it as a constructor parameter so that it
is injected via constructor injection, maintaining consistency with the rest of
the class.
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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
입력 검증 및 구체적인 예외 처리 필요
현재 모든 예외를 IllegalArgumentException
으로 변환하고 있어 오류의 원인을 파악하기 어렵습니다. 또한 입력값 검증이 누락되어 있습니다.
public SignedJWT validate(String idToken) {
+ if (idToken == null || idToken.trim().isEmpty()) {
+ throw new IllegalArgumentException("ID 토큰이 제공되지 않았습니다");
+ }
+
try {
SignedJWT jwt = SignedJWT.parse(idToken);
// ... 기존 코드 ...
} catch (ParseException e) {
- throw new IllegalArgumentException("Apple ID Token 파싱 실패", e);
+ throw new BaseException(ErrorCode.APPLE_TOKEN_PARSE_FAILED, e);
} catch (Exception e) {
- throw new IllegalArgumentException("Apple ID Token 검증 실패", e);
+ if (e instanceof BaseException) {
+ throw e;
+ }
+ throw new BaseException(ErrorCode.APPLE_TOKEN_VALIDATION_FAILED, e);
}
}
🏁 Script executed:
#!/bin/bash
# ErrorCode enum에 Apple 관련 오류 코드가 정의되어 있는지 확인
ast-grep --pattern 'enum ErrorCode {
$$$
}'
Length of output: 47
Apple 관련 ErrorCode 정의 확인을 위해 추가 정보가 필요합니다.
🏁 Script executed:
#!/bin/bash
# ErrorCode enum 정의 위치와 Apple 관련 코드 확인
rg --max-count=1 "enum\s\+ErrorCode" -A 20
# APPLE_TOKEN_PARSE_FAILED 및 APPLE_TOKEN_VALIDATION_FAILED 상수 존재 여부 검사
rg "APPLE_TOKEN_PARSE_FAILED|APPLE_TOKEN_VALIDATION_FAILED"
Length of output: 106
입력 검증 및 구체적 예외 처리 추가
JwtValidator.validate 메서드에 다음 개선을 적용해주세요:
- 파일/위치: src/main/java/com/usememo/jugger/global/security/JwtValidator.java (24–65줄)
- 문제점
idToken
에 대한 null‧빈 문자열 검사 누락- 모든 예외를
IllegalArgumentException
으로 덮어써서, 실제 실패 원인(파싱 오류 vs 검증 실패 vs JOSE 처리 오류 등)을 파악하기 어려움
- 제안 리팩터링
- 메서드 시작부에 null‧blank 체크 추가
ParseException
, JOSE 관련 예외(JOSEException) 등 구체적 예외별로 별도 커스텀 예외를 던지도록 분리
(예:AppleTokenParseException
,AppleTokenValidationException
등)- 불필요한
catch (Exception e)
제거
[diff]
public SignedJWT validate(String idToken) {
+ if (idToken == null || idToken.isBlank()) {
+ throw new IllegalArgumentException("ID 토큰이 제공되지 않았습니다");
+ }
try {
SignedJWT jwt = SignedJWT.parse(idToken);
JWSHeader header = jwt.getHeader();
// … (기존 알고리즘·서명·클레임 검증 로직)
- } catch (ParseException e) {
- throw new IllegalArgumentException("Apple ID Token 파싱 실패", e);
- } catch (Exception e) {
- throw new IllegalArgumentException("Apple ID Token 검증 실패", e);
+ } catch (ParseException e) {
+ throw new AppleTokenParseException("Apple ID Token 파싱 실패", e);
+ } catch (JOSEException e) {
+ throw new AppleTokenValidationException("Apple ID Token 서명/클레임 검증 실패", e);
}
}
- 추가 작업
- 위에서 사용한 커스텀 예외 클래스(
AppleTokenParseException
,AppleTokenValidationException
)를 정의하고, 필요한 경우 전역 예외 처리기(GlobalExceptionHandler)에서 HTTP 응답코드 매핑도 함께 설정해주세요.
- 위에서 사용한 커스텀 예외 클래스(
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
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); | |
} | |
} | |
public SignedJWT validate(String idToken) { | |
if (idToken == null || idToken.isBlank()) { | |
throw new IllegalArgumentException("ID 토큰이 제공되지 않았습니다"); | |
} | |
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 AppleTokenParseException("Apple ID Token 파싱 실패", e); | |
} catch (JOSEException e) { | |
throw new AppleTokenValidationException("Apple ID Token 서명/클레임 검증 실패", e); | |
} | |
} |
🤖 Prompt for AI Agents
In src/main/java/com/usememo/jugger/global/security/JwtValidator.java lines 24
to 65, add null and blank string checks for the idToken parameter at the start
of the validate method to prevent invalid input. Replace the current generic
IllegalArgumentException handling by defining and throwing specific custom
exceptions such as AppleTokenParseException for parsing errors and
AppleTokenValidationException for validation failures, catching ParseException
and JOSEException separately. Remove the broad catch (Exception e) block to
avoid masking distinct error causes. Also, create these custom exception classes
and update the global exception handler to map them to appropriate HTTP response
codes.
src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java
Outdated
Show resolved
Hide resolved
private String privateKeyLocation; | ||
private String privateKey; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
privateKey와 privateKeyLocation 필드의 용도 명확화 필요
두 개의 비슷한 필드가 있어 혼란을 줄 수 있습니다. 각각의 용도를 문서화하거나 하나로 통합하는 것을 고려해주세요.
🏁 Script executed:
#!/bin/bash
# privateKey와 privateKeyLocation 필드가 실제로 어떻게 사용되는지 확인
rg -A 5 "getPrivateKey\(\)|getPrivateKeyLocation\(\)" --type java
Length of output: 2881
🏁 Script executed:
#!/bin/bash
# appleProperties.getPrivateKey() 호출 위치 확인
rg "appleProperties\\.getPrivateKey" --type java -n
Length of output: 224
privateKey와 privateKeyLocation 용도 명확화 및 정리 필요
현재 privateKeyLocation
은 파일 경로로부터 키를 읽어오는 데만 사용되고, privateKey
필드는 코드에서 전혀 참조되지 않습니다. 두 필드가 각각 어떤 상황에서, 어떤 형식(예: 파일 경로 vs. 인라인 Base64)으로 사용되어야 하는지 문서화하거나, 실제 사용하지 않는 필드를 제거/통합해주세요.
조치 항목:
- src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java
•privateKey
/privateKeyLocation
각 필드에 대한 JavaDoc 또는 주석 추가 - 프로젝트 설정 문서(application.yml 예시, README 등)에 두 속성 사용 방법 및 우선순위(예:
privateKey
설정 시privateKeyLocation
무시 여부) 기재 - 만약 인라인 키 제공 기능이 필요 없다면, 사용되지 않는
privateKey
필드를 제거하거나, 반대로 인라인 키 사용 로직을AppleJwtGenerator
에 구현
🤖 Prompt for AI Agents
In
src/main/java/com/usememo/jugger/global/security/token/domain/AppleProperties.java
around lines 14 to 15, clarify the purpose and usage of the privateKey and
privateKeyLocation fields by adding JavaDoc or comments explaining when and how
each should be used (e.g., file path vs. inline Base64 key). Update project
documentation such as application.yml examples or README to describe these
properties and their precedence (e.g., whether privateKey overrides
privateKeyLocation). If inline key usage is not needed, remove the unused
privateKey field; otherwise, implement the inline key handling logic in
AppleJwtGenerator accordingly.
src/main/java/com/usememo/jugger/global/security/token/controller/AuthController.java
Show resolved
Hide resolved
String email, | ||
|
||
@Schema(description = "약관 동의 정보", required = true) | ||
KakaoSignUpRequest.Terms terms |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
잘못된 클래스 참조 수정 필요
Apple 회원가입 요청에서 KakaoSignUpRequest.Terms
를 참조하고 있습니다. 자체 정의된 Terms
클래스를 사용해야 합니다.
- KakaoSignUpRequest.Terms terms
+ Terms terms
🤖 Prompt for AI Agents
In
src/main/java/com/usememo/jugger/global/security/token/domain/AppleSignUpRequest.java
at line 13, the code incorrectly references KakaoSignUpRequest.Terms. Replace
this with the locally defined Terms class specific to AppleSignUpRequest to
ensure correct class usage and avoid cross-class dependency.
src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/usememo/jugger/global/security/token/service/AppleTokenService.java
Show resolved
Hide resolved
src/main/java/com/usememo/jugger/global/security/token/service/AppleOAuthService.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생했어용 레빗이가 말하는 부분들만 조금 수정하면 될 듯요
this.message = errorCode.getMessage(); | ||
} | ||
|
||
public BaseException(ErrorCode errorCode, Throwable cause) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이건 어떤 식으로 쓰는건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AppleTokenService에서 APPLE_CLIENT_SECRET_FAILED 및APPLE_TOKEN_PARSE_ERROR 예외를 던질 때, 예외 로그를 남기기 위한 목적으로 cause(Throwable)를 포함했었습니다.
다만, 현재 프로젝트의 다른 예외 처리 흐름에서는 cause를 포함하지 않는 방식으로 통일되어 있어, 일관성을 위해 해당 부분도 제거해두었습니다.
추후 공통 로깅 처리 구조가 생기면 다시 포함하는 방향도 고려할 수 있을 것 같습니다.
Resource resource = resourceLoader.getResource(appleProperties.getPrivateKeyLocation()); | ||
try (BufferedReader reader = | ||
new BufferedReader(new InputStreamReader(resource.getInputStream()))) { | ||
String keyContent = reader.lines().collect(Collectors.joining("\n")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아마 이 부분때문에 애플로그인이 어렵다는 인식이 생긴 것 같네용 신기합니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
확인했습니다! 수고하셨어요~
PR이름만 기존에 작성한대로 바꿔주세요~
} | ||
|
||
public BaseException(ErrorCode errorCode, Throwable cause) { | ||
super(errorCode.getMessage(), cause); // RuntimeException에 메시지 + 원인 등록 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주석은 지워주세요~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 알겠습니다!
InvalidKeyException { | ||
Date now = new Date(); | ||
Date expiration = new Date(now.getTime() + 3600_000); // 유효시간: 1시간 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상동입니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영 했습니다~
throw new IllegalArgumentException("Apple ID Token 만료됨"); | ||
} | ||
|
||
if (!"https://appleid.apple.com".equals(claims.getIssuer())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
url 상수로 작성하면 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고정된 값이라 코드 내 상수로 바꾸는게 적합할 듯 합니다.
APPLE_ISSUER로 설정해두었습니다:)
private String privateKey; | ||
private String redirectUri; | ||
|
||
public String getClientId() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
하기 링크에 application.yml 바뀐사항들 산입 부탁드립니다.
https://www.notion.so/jadestone/application-yml-1c2b8fffcd6a80b98e0dc4ef38677f9a?source=copy_link -
ci/cd 파이프라인에도 환경변수 값들 산입 부탁드립니다.
#️⃣ Issue Number
📝 요약(Summary)
이번 PR에서는 Apple 로그인 기능을 완전히 구현했습니다.
🔑 주요 구현 사항
1. Apple OAuth 인증 플로우
AppleOAuthService
: Apple 로그인/회원가입 비즈니스 로직AppleTokenService
: Apple 서버와의 토큰 교환 및 사용자 정보 추출2. JWT 토큰 검증 시스템
JwtValidator
: Apple ID 토큰의 서명 및 클레임 검증ApplePublicKeyProvider
: Apple 공개키 캐싱 및 관리AppleJwtGenerator
: Apple client secret JWT 생성3. API 엔드포인트 추가
POST /auth/apple
: Apple 로그인POST /auth/apple/signup
: Apple 회원가입4. 설정 및 데이터 모델
AppleProperties
: Apple 인증 관련 설정값 바인딩AppleLoginRequest
,AppleSignUpRequest
,AppleUserResponse
: 요청/응답 모델WebClientConfig
: HTTP 클라이언트 설정5. 에러 처리
💬 공유사항 to 리뷰어
✅ PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes