Skip to content

[FIX] 토큰 발행 방 법 변경(NEW: 개인/공개키 방식(비대칭 키))#53

Merged
Lee-Jong-Jin merged 5 commits into
mainfrom
SPM-384
Nov 11, 2025
Merged

[FIX] 토큰 발행 방 법 변경(NEW: 개인/공개키 방식(비대칭 키))#53
Lee-Jong-Jin merged 5 commits into
mainfrom
SPM-384

Conversation

@Lee-Jong-Jin

@Lee-Jong-Jin Lee-Jong-Jin commented Nov 10, 2025

Copy link
Copy Markdown
Contributor

📣 Related Issue

  • close # SPM-384

📝 Summary

  • 토큰 발행 및 검증 방법 변경: 비밀키(대칭키) -> 개인/공개키(비대칭키)
  • properties 비밀 적용 완료

🙏 Question & PR point

📬 Reference

Summary by CodeRabbit

  • 보안 개선

    • JWT 검증이 대칭키(HS256)에서 RSA 공개키 기반으로 전환되어 서명 검증이 강화되었습니다.
  • 설정 변경

    • 공개키를 Base64 문자열로 입력하도록 설정 항목이 변경되었습니다(유효성 검사 및 길이 제한 포함).
  • 오류 처리 개선

    • 토큰 null/공백 에러를 통합했고 공개키 유효성 실패 및 만료 토큰에 대한 별도 오류 처리와 메시지가 추가되었습니다.
  • 리팩터

    • 일부 내부 메서드/명칭이 변경되어 코드 가독성과 일관성이 개선되었습니다.

@coderabbitai

coderabbitai Bot commented Nov 10, 2025

Copy link
Copy Markdown

Walkthrough

대칭 HS256 비밀키 검증에서 RSA 공개키(Base64) 검증으로 전환하고 공개키 로드/유효성 검사, 관련 에러 상태 통합·추가 및 일부 엔티티 메서드명/공백 수정이 적용되었습니다. JwtAuthFilter의 토큰 빈값 에러 상태가 NULL_BLANK_TOKEN으로 변경되었습니다.

Changes

Cohort / File(s) Summary
JWT 제공자 (공개키 로드 및 검증 변경)
src/main/java/com/sampoom/user/common/jwt/JwtProvider.java
• 대칭 키(Key key) → RSA 공개키(PublicKey publicKey)로 변경
• 생성자: ${jwt.secret}${jwt.public-key-base64}로 변경 및 null/blank 검사(INVALID_PUBLIC_KEY)
• 추가: private PublicKey loadPublicKey(String base64) — Base64 디코딩, X509EncodedKeySpec/KeyFactory로 PublicKey 생성, 2048비트 이상 검사(SHORT_PUBLIC_KEY), 예외 매핑
• 토큰 파싱: setSigningKey(key)setSigningKey(publicKey) 변경
• 예외 매핑: 만료 → EXPIRED_TOKEN, 그 외 → INVALID_TOKEN/INVALID_PUBLIC_KEY
• 암호화 관련 import 추가
인증 필터 (오류 상태 사용 변경)
src/main/java/com/sampoom/user/common/jwt/JwtAuthFilter.java
• 액세스 토큰이 공백인 경우 에러 상태를 BLANK_TOKENNULL_BLANK_TOKEN으로 변경. 나머지 흐름은 유지
오류 상태(enum) 수정 및 추가
src/main/java/com/sampoom/user/common/response/ErrorStatus.java
BLANK_TOKEN, NULL_TOKEN, TOO_SHORT_SECRET_KEY 등 일부 항목 통합/제거
• 추가: NULL_BLANK_TOKEN(12400), SHORT_PUBLIC_KEY(12401), INVALID_PUBLIC_KEY(12406)
소프트 삭제 / 시간 관련 엔티티
src/main/java/com/sampoom/user/common/entity/SoftDeleteEntity.java, src/main/java/com/sampoom/user/common/entity/BaseEmployeeEntity.java, src/main/java/com/sampoom/user/common/entity/BaseTimeEntity.java
reactive()reactivation()으로 메서드명 변경 (SoftDeleteEntity)
BaseEmployeeEntity.onUpdateStatus에서 ACTIVE 분기 호출을 reactivation()으로 변경
BaseTimeEntity 선언부의 추가 공백 제거(코드 스타일)
빌드/메타
pom.xml
• 변경 내역 없음(분석 대상에 포함됨)

Sequence Diagram(s)

sequenceDiagram
    participant App as 애플리케이션
    participant JwtProvider as JwtProvider
    participant KeyFactory as KeyFactory
    participant JWTParser as JWT 파서

    rect rgb(235, 245, 255)
    note right of JwtProvider: 초기화
    App->>JwtProvider: new JwtProvider(publicKeyBase64)
    JwtProvider->>JwtProvider: null/blank 검사
    JwtProvider->>KeyFactory: Base64 디코딩 → X509EncodedKeySpec → generatePublic
    KeyFactory-->>JwtProvider: PublicKey (RSAPublicKey)
    JwtProvider->>JwtProvider: 공개키 길이 검사 (>=2048bit)
    end

    rect rgb(235, 255, 235)
    note right of JWTParser: 토큰 검증
    App->>JwtProvider: parse(token)
    JwtProvider->>JWTParser: setSigningKey(publicKey)
    JWTParser->>JWTParser: 서명 검증 (RSA) / 만료 검사
    alt valid
        JWTParser-->>JwtProvider: Claims
        JwtProvider-->>App: Claims
    else expired
        JWTParser-->>JwtProvider: ExpiredJwtException
        JwtProvider-->>App: EXPIRED_TOKEN 예외
    else invalid
        JWTParser-->>JwtProvider: Exception
        JwtProvider-->>App: INVALID_TOKEN / INVALID_PUBLIC_KEY 예외
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 집중 검토 항목:
    • loadPublicKey의 Base64 디코딩/KeyFactory 예외 처리 및 에러 매핑(INVALID_PUBLIC_KEY)
    • RSAPublicKey 캐스팅 및 2048비트 이상 검사 안정성 및 메시지/코드 정합성(SHORT_PUBLIC_KEY)
    • 환경 변수명 변경(jwt.secretjwt.public-key-base64)과 설정·배포 영향
    • ErrorStatus 코드 변경(통합/추가)에 따른 전체 프로젝트 영향
    • reactive()reactivation() 호출 전파(테스트/레퍼런스) 확인

Possibly related PRs

Suggested labels

ready-to-merge

Suggested reviewers

  • CHOOSLA
  • Sangyoon98
  • yangjiseonn
  • taemin3
  • vivivim

Poem

🐰
공개키 빛에 귀 기울이며,
Base64 춤춰 열쇠를 만들고,
토큰의 숨결 하나하나 살피니,
키래빗이 깡총 검증을 끝내네,
당근 한 입, 배포 준비 완료 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 토큰 검증 방식 변경(비대칭키 방식 도입)을 명확하게 설명하고 있습니다.
Description check ✅ Passed PR 설명이 필수 섹션을 포함하고 있으나 'Question & PR point'와 'Reference' 섹션이 비어있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SPM-384

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (2)

13-15: 사용하지 않는 임포트를 제거하세요.

비대칭 키 방식으로 전환하면서 SecretKeySpecKey 임포트는 더 이상 필요하지 않습니다.

-import javax.crypto.spec.SecretKeySpec;
 import java.nio.charset.StandardCharsets;
-import java.security.Key;

36-43: 주석 처리된 레거시 코드를 제거하세요.

더 이상 사용하지 않는 대칭키 관련 코드는 제거하는 것이 코드 가독성에 좋습니다. Git 히스토리를 통해 언제든 이전 코드를 확인할 수 있습니다.

-    // Auth에서 들어오는 Key의 초기화 (캐싱용 객체)
-    public JwtProvider(@Value("${jwt.secret}") String secret) {
-        if (secret.getBytes(StandardCharsets.UTF_8).length < 32) {
-            throw new BadRequestException(ErrorStatus.TOO_SHORT_SECRET_KEY);
-        }
-        this.key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),
-                SignatureAlgorithm.HS256.getJcaName());
-    }
-
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ac5967 and 52483a8.

📒 Files selected for processing (1)
  • src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (3)
src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (3)

16-19: 새로운 임포트가 적절합니다.

RSA 공개키 처리를 위해 필요한 클래스들이 올바르게 임포트되었습니다.


23-24: 필드 변경이 적절합니다.

대칭키(Key)에서 공개키(PublicKey)로의 전환이 올바르게 적용되었습니다.


53-53: 토큰 검증 로직이 올바르게 구현되었습니다.

검증 결과, 이 서비스는 토큰 검증 전용이며 토큰 생성은 별도의 auth-service에서 처리하는 올바른 마이크로서비스 아키텍처입니다. 공개키를 사용한 JWT 서명 검증 구현이 적절하게 완료되었습니다.

Comment thread src/main/java/com/sampoom/user/common/jwt/JwtProvider.java Outdated
Comment thread src/main/java/com/sampoom/user/common/jwt/JwtProvider.java Outdated
Comment thread src/main/java/com/sampoom/user/common/jwt/JwtProvider.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52483a8 and 023dd40.

📒 Files selected for processing (3)
  • src/main/java/com/sampoom/user/common/jwt/JwtAuthFilter.java (1 hunks)
  • src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (1 hunks)
  • src/main/java/com/sampoom/user/common/response/ErrorStatus.java (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-16T10:32:56.782Z
Learnt from: Lee-Jong-Jin
Repo: 33-Auto/Sampoom-Management-Backend-User PR: 7
File: src/main/java/com/sampoom/backend/user/jwt/JwtAuthFilter.java:34-37
Timestamp: 2025-10-16T10:32:56.782Z
Learning: In JWT authentication filters for the Sampoom Management Backend User service, when userId or role is null in the token claims, log a warning with both values using log.warn and call chain.doFilter(req, res) before returning, rather than throwing an exception. This allows the request to continue without authentication.

Applied to files:

  • src/main/java/com/sampoom/user/common/jwt/JwtAuthFilter.java
📚 Learning: 2025-10-16T10:22:30.658Z
Learnt from: Lee-Jong-Jin
Repo: 33-Auto/Sampoom-Management-Backend-User PR: 7
File: src/main/java/com/sampoom/backend/user/common/exception/ForbiddenException.java:15-17
Timestamp: 2025-10-16T10:22:30.658Z
Learning: In the com.sampoom.backend.user.common.exception package, exception class constructors accepting ErrorStatus parameter (e.g., ForbiddenException, BadRequestException, UnauthorizedException) do not perform null checks on the errorStatus parameter before calling its getters.

Applied to files:

  • src/main/java/com/sampoom/user/common/jwt/JwtAuthFilter.java
  • src/main/java/com/sampoom/user/common/response/ErrorStatus.java
  • src/main/java/com/sampoom/user/common/jwt/JwtProvider.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (java-kotlin)

Comment thread src/main/java/com/sampoom/user/common/jwt/JwtProvider.java
Comment thread src/main/java/com/sampoom/user/common/jwt/JwtProvider.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (1)

24-32: BadRequestException을 다시 감싸면서 상세 에러 코드가 사라집니다.

loadPublicKeySHORT_PUBLIC_KEY 같은 BadRequestException을 던져도, 생성자와 메소드의 catch (Exception)이 모두 INVALID_PUBLIC_KEY로 덮어써 버려 신규 에러 코드가 절대 노출되지 않습니다. BadRequestException은 그대로 재전파하고, 그 외 예외만 INVALID_PUBLIC_KEY로 매핑해 주세요.

         try {
             this.publicKey = loadPublicKey(publicKeyBase64);
-        } catch (Exception e) {
-            throw new BadRequestException(ErrorStatus.INVALID_PUBLIC_KEY);
+        } catch (BadRequestException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new BadRequestException(ErrorStatus.INVALID_PUBLIC_KEY);
         }
@@
-        }  catch (Exception e) {
-            throw new BadRequestException(ErrorStatus.INVALID_PUBLIC_KEY);
+        } catch (BadRequestException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new BadRequestException(ErrorStatus.INVALID_PUBLIC_KEY);
         }

Also applies to: 47-49

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 023dd40 and cbf115e.

📒 Files selected for processing (2)
  • src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (1 hunks)
  • src/main/java/com/sampoom/user/common/response/ErrorStatus.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-16T10:22:30.658Z
Learnt from: Lee-Jong-Jin
Repo: 33-Auto/Sampoom-Management-Backend-User PR: 7
File: src/main/java/com/sampoom/backend/user/common/exception/ForbiddenException.java:15-17
Timestamp: 2025-10-16T10:22:30.658Z
Learning: In the com.sampoom.backend.user.common.exception package, exception class constructors accepting ErrorStatus parameter (e.g., ForbiddenException, BadRequestException, UnauthorizedException) do not perform null checks on the errorStatus parameter before calling its getters.

Applied to files:

  • src/main/java/com/sampoom/user/common/response/ErrorStatus.java
  • src/main/java/com/sampoom/user/common/jwt/JwtProvider.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (java-kotlin)

Comment thread src/main/java/com/sampoom/user/common/response/ErrorStatus.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/com/sampoom/user/common/response/ErrorStatus.java (1)

13-13: 과거 리뷰 코멘트가 이미 반영되었습니다.

이전 리뷰에서 지적된 "2048바이트" → "2048비트" 수정이 현재 코드에 정확히 적용되어 있습니다.

🧹 Nitpick comments (3)
src/main/java/com/sampoom/user/common/entity/SoftDeleteEntity.java (1)

23-27: 메서드 이름을 동사형으로 변경하는 것을 권장합니다.

reactivation()은 명사형이지만, Java 메서드 명명 규칙에 따르면 동사나 동사구를 사용하는 것이 더 관례적입니다. 같은 클래스의 softDelete() (Line 17)와 일관성을 맞추기 위해 reactivate()로 변경하는 것을 고려해보세요.

-    public void reactivation(){
+    public void reactivate(){
         if(!this.deleted) return;
         this.deleted = false;
         this.deletedAt = null;
     }
src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (2)

21-21: 주석 처리된 레거시 코드를 제거하세요.

대칭키 방식에서 사용하던 필드가 주석으로 남아 있습니다. 마이그레이션이 완료되었으므로 코드 가독성을 위해 제거하는 것이 좋습니다.

다음 diff를 적용하세요:

-//    private final Key key;
     private final PublicKey publicKey;

38-54: RSA 키 타입 검증을 명시적으로 수행하는 것을 고려하세요.

현재 구현에서는 instanceof RSAPublicKey 체크가 실패하면 키 길이 검증을 건너뜁니다 (lines 43-47). KeyFactory.getInstance("RSA")는 유효한 키에 대해 항상 RSAPublicKey를 반환해야 하지만, 명시적으로 타입을 강제하면 보안성이 더 강화됩니다.

다음과 같이 개선할 수 있습니다:

     private PublicKey loadPublicKey(String base64) throws Exception {
         try {
             byte[] keyBytes = Base64.getDecoder().decode(base64);
             PublicKey key = KeyFactory.getInstance("RSA")
                     .generatePublic(new X509EncodedKeySpec(keyBytes));
-            if (key instanceof RSAPublicKey rsaKey) {
-                if (rsaKey.getModulus().bitLength() < 2048) {
-                    throw new BadRequestException(ErrorStatus.SHORT_PUBLIC_KEY);
-                }
+            if (!(key instanceof RSAPublicKey rsaKey)) {
+                throw new BadRequestException(ErrorStatus.INVALID_PUBLIC_KEY);
+            }
+            if (rsaKey.getModulus().bitLength() < 2048) {
+                throw new BadRequestException(ErrorStatus.SHORT_PUBLIC_KEY);
             }
             return key;
         } catch (BadRequestException e) {

참고: 예외 처리 로직 (lines 49-52)은 이전 리뷰 피드백을 잘 반영하여 BadRequestException을 올바르게 전파하고 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbf115e and 7ac158b.

⛔ Files ignored due to path filters (2)
  • src/main/java/com/sampoom/user/common/config/security/.DS_Store is excluded by !**/.DS_Store
  • src/main/resources/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (5)
  • src/main/java/com/sampoom/user/common/entity/BaseEmployeeEntity.java (1 hunks)
  • src/main/java/com/sampoom/user/common/entity/BaseTimeEntity.java (1 hunks)
  • src/main/java/com/sampoom/user/common/entity/SoftDeleteEntity.java (1 hunks)
  • src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (1 hunks)
  • src/main/java/com/sampoom/user/common/response/ErrorStatus.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/com/sampoom/user/common/entity/BaseTimeEntity.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-16T10:22:30.658Z
Learnt from: Lee-Jong-Jin
Repo: 33-Auto/Sampoom-Management-Backend-User PR: 7
File: src/main/java/com/sampoom/backend/user/common/exception/ForbiddenException.java:15-17
Timestamp: 2025-10-16T10:22:30.658Z
Learning: In the com.sampoom.backend.user.common.exception package, exception class constructors accepting ErrorStatus parameter (e.g., ForbiddenException, BadRequestException, UnauthorizedException) do not perform null checks on the errorStatus parameter before calling its getters.

Applied to files:

  • src/main/java/com/sampoom/user/common/response/ErrorStatus.java
  • src/main/java/com/sampoom/user/common/jwt/JwtProvider.java
📚 Learning: 2025-10-14T11:21:55.975Z
Learnt from: vivivim
Repo: 33-Auto/Sampoom-Management-Backend-User PR: 6
File: src/main/java/com/sampoom/backend/user/service/UserService.java:24-84
Timestamp: 2025-10-14T11:21:55.975Z
Learning: In the com.sampoom.backend.user project, use custom exceptions from com.sampoom.backend.user.common.exception package (BadRequestException, NotFoundException, etc.) instead of Java built-in exceptions (IllegalArgumentException, EntityNotFoundException) for consistent error handling and proper HTTP status code mapping in service layers.

Applied to files:

  • src/main/java/com/sampoom/user/common/response/ErrorStatus.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (3)
src/main/java/com/sampoom/user/common/entity/BaseEmployeeEntity.java (1)

55-55: 변경사항이 일관성 있게 적용되었습니다.

SoftDeleteEntity의 메서드명 변경에 맞춰 호출 코드가 올바르게 업데이트되었습니다. 만약 SoftDeleteEntity에서 reactivate()로 메서드명을 변경한다면, 이 부분도 함께 업데이트해야 합니다.

src/main/java/com/sampoom/user/common/jwt/JwtProvider.java (2)

24-36: 예외 처리 개선이 잘 반영되었습니다.

생성자의 예외 처리가 이전 리뷰 피드백을 반영하여 개선되었습니다:

  • BadRequestException을 별도로 catch하여 그대로 전파 (lines 30-31)
  • 기타 예외만 INVALID_PUBLIC_KEY로 래핑 (lines 32-33)

이를 통해 SHORT_PUBLIC_KEY 같은 구체적인 에러 상태가 올바르게 전달됩니다.


56-71: 토큰 파싱 로직이 올바르게 구현되었습니다.

공개키 기반 검증으로의 전환이 정확하게 적용되었습니다:

  • 토큰 null/blank 검증 추가 (lines 57-59)
  • publicKey를 사용한 서명 검증 (line 61)
  • 적절한 예외 처리로 만료/유효하지 않은 토큰 구분

@vivivim vivivim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿

@Lee-Jong-Jin Lee-Jong-Jin merged commit 4285514 into main Nov 11, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants