Skip to content

[Fix] JWT 예외 통일#449

Merged
david-parkk merged 3 commits intodevelopfrom
fix/#448
Mar 2, 2026
Merged

[Fix] JWT 예외 통일#449
david-parkk merged 3 commits intodevelopfrom
fix/#448

Conversation

@david-parkk
Copy link
Contributor

@david-parkk david-parkk commented Mar 2, 2026

#️⃣ 연관된 이슈

closes #448

📝작업 내용

  • JWT 예외 통일

작업 상세 내용

상세 내용을 입력해주세요.

💬리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요.

Summary by CodeRabbit

릴리스 노트

  • Bug Fixes

    • JWT 토큰 검증 및 예외 처리 흐름 안정화 — 다양한 토큰 오류를 보다 일관되게 처리하도록 개선
  • Refactor

    • 토큰 관련 에러 응답 구조 통일 및 처리 매핑 개선
    • 만료된 토큰의 HTTP 응답 상태를 Forbidden으로 조정
  • Tests

    • 토큰 관련 단위테스트의 예외 기대 동작 갱신
  • Chores

    • 구성 하위모듈 포인터 갱신

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6cefe4 and 483f436.

📒 Files selected for processing (1)
  • src/main/resources/config

Walkthrough

Jwt 예외 처리를 통합했습니다: JwtTokenProvider는 검증 오류로 GlobalException을 던지도록 변경되고, JwtTokenAuthenticationFilter는 예외를 BaseErrorResponse로 매핑하여 응답을 반환하며, EXPIRED_TOKEN의 HTTP 상태 코드가 FORBIDDEN으로 변경되었습니다. GlobalExceptionControllerAdvice에 import가 추가되었습니다.

Changes

Cohort / File(s) Summary
JWT 예외 처리 통합
src/main/java/ku_rum/backend/global/security/JwtTokenProvider.java, src/main/java/ku_rum/backend/global/security/JwtTokenAuthenticationFilter.java
토큰 검증 실패를 GlobalException으로 통일하고, 필터에서 예외를 BaseErrorResponse로 매핑하는 로직 및 resolveTokenException 추가. 에러 응답 포맷 및 처리 흐름 변경(응답 본문은 구조화된 BaseErrorResponse).
예외 상태 코드 변경
src/main/java/ku_rum/backend/global/support/status/BaseExceptionResponseStatus.java
EXPIRED_TOKEN의 연관 HTTP 상태를 UNAUTHORIZED에서 FORBIDDEN으로 변경.
글로벌 예외 처리 경미 변경
src/main/java/ku_rum/backend/global/handler/GlobalExceptionControllerAdvice.java
HttpMessageNotReadableException import 추가 (로직 변경 없음).
테스트 업데이트
src/test/java/ku_rum/backend/global/security/jwt/JwtTokenProviderTest.java
만료 토큰 관련 테스트가 JwtException 대신 GlobalException을 기대하도록 수정.
구성 포인터 업데이트
src/main/resources/config
서브모듈 포인터 커밋 해시 업데이트 (기록된 참조 변경, 기능적 변경 없음).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Filter as JwtTokenAuthenticationFilter
    participant Provider as JwtTokenProvider
    participant Response as ResponseWriter

    Client->>Filter: HTTP Request (JWT)
    Filter->>Provider: validateToken(jwt)
    alt 토큰 유효
        Provider-->>Filter: Claims
        Filter->>Filter: 인증 생성 및 SecurityContext 설정
        Filter-->>Client: 다음 필터 체인 진행
    else 토큰 오류(만료/형식/기타)
        Provider-->>Filter: throw GlobalException
        Filter->>Filter: resolveTokenException() -> BaseExceptionResponseStatus
        Filter->>Response: setStatus(SC_OK) 및 write(BaseErrorResponse)
        Response-->>Client: JSON 에러 응답
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • [Fix] 불필요한 파일 삭제 #444: GlobalExceptionControllerAdvice 관련 import/예외 처리 변경과 겹치는 파일 수정 — 동일 파일 변경으로 코드 수준 연관성 높음.

Suggested reviewers

  • kmw10693

Poem

🐰 토큰 길 따라 달려가며
오류들 모아 한 줄로 정리하네
필터가 다듬어 응답을 보내고
만료와 오타도 이름을 얻었네
통합된 길에 당근 한 개 놓으리 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive 필수 섹션 '작업 상세 내용'과 '리뷰 요구사항'이 기본 템플릿 텍스트 상태로 비어있어 실질적인 설명이 부족합니다. 작업 상세 내용에 JWT 예외 통일의 구체적인 구현 사항을 추가하고, 필요시 리뷰 요구사항을 기재해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목 '[Fix] JWT 예외 통일'은 JWT 예외 처리 통일이라는 주요 변경 사항을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 변경사항(JWT 예외를 JwtException에서 GlobalException으로 통일, BaseExceptionResponseStatus 상수 사용, EXPIRED_TOKEN 상태코드 변경)이 issue #448의 'JWT 에러 통합' 목적과 일치합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 JWT 예외 처리 통일과 관련된 범위 내에 있으며, 불필요한 범위 외 변경은 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#448

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Test Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit 483f436.

♻️ This comment has been updated with latest results.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/ku_rum/backend/global/security/JwtTokenAuthenticationFilter.java (1)

29-45: ⚠️ Potential issue | 🔴 Critical

doFilter의 예외 포착 범위가 너무 넓어 비JWT 예외까지 토큰 오류로 변환됩니다.

Line [44]의 chain.doFiltertry 내부라서, 하위 레이어 예외도 Line [45]에서 잡혀 JWT 오류 응답으로 바뀔 수 있습니다. JWT 처리 예외만 잡고 나머지는 전파해야 합니다.

🔧 제안 수정안
+import ku_rum.backend.global.exception.global.GlobalException;
@@
-        try {
+        try {
             StringBuffer path = ((HttpServletRequest) request).getRequestURL();
@@
             if (token != null && jwtTokenProvider.validateToken(token) && isNotLogout(token)) {
                 Authentication authentication = jwtTokenProvider.getAuthentication(token);
                 SecurityContextHolder.getContext().setAuthentication(authentication);
             }
-            chain.doFilter(request, response);
-        } catch (Exception e) {
+        } catch (GlobalException e) {
             HttpServletResponse httpServletResponse = (HttpServletResponse) response;
             httpServletResponse.setContentType("application/json;charset=UTF-8");
@@
             BaseErrorResponse errorResponse = new BaseErrorResponse(status);
             httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));
+            return;
         }
+        chain.doFilter(request, response);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/ku_rum/backend/global/security/JwtTokenAuthenticationFilter.java`
around lines 29 - 45, In JwtTokenAuthenticationFilter.doFilter, the try/catch
currently wraps the entire filter including chain.doFilter causing non-JWT
exceptions to be converted into token errors; narrow the scope so only
JWT-specific operations (resolveToken, jwtTokenProvider.validateToken,
isNotLogout, jwtTokenProvider.getAuthentication and
SecurityContextHolder.setAuthentication) are wrapped and catch only JWT-related
exceptions (e.g., JwtException or your provider's specific exception), then
allow other exceptions thrown by chain.doFilter(request, response) to propagate
(or rethrow them) instead of converting them to token errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/ku_rum/backend/global/security/JwtTokenAuthenticationFilter.java`:
- Around line 49-50: The filter JwtTokenAuthenticationFilter currently overrides
the HTTP status to HttpServletResponse.SC_OK in the exception path; change the
logic that handles exceptions from resolveTokenException(e) so that the response
uses the HTTP status from the returned BaseExceptionResponseStatus (e.g.,
status.getHttpStatus()/getStatusCode()) rather than hard-coding 200, and ensure
httpServletResponse.setStatus(...) is called with that status value so
EXPIRED_TOKEN(403) and other statuses are preserved.
- Around line 57-69: The resolveTokenException method currently parses exception
messages; instead, replace that logic with instanceof checks: if e is null
return JWT_ERROR; if e instanceof GlobalException return the embedded
BaseExceptionResponseStatus from the GlobalException (e.g., call its accessor to
retrieve the status); if e instanceof ExpiredJwtException (or JwtException
subtype representing expiration) return EXPIRED_TOKEN; if e instanceof
MalformedJwtException return MALFORMED_TOKEN; otherwise return INVALID_TOKEN.
Update resolveTokenException and ensure JwtTokenProvider.validateToken()’s
thrown GlobalException is handled via instanceof rather than message parsing.

In `@src/main/java/ku_rum/backend/global/security/JwtTokenProvider.java`:
- Around line 86-89: In JwtTokenProvider replace the current instanceof
SecurityException check with handling for
io.jsonwebtoken.security.SignatureException (or add an explicit catch for
SignatureException) so signature verification failures are mapped to
BaseExceptionResponseStatus.INVALID_TOKEN; specifically detect
SignatureException (e.g., if (e instanceof SignatureException)), log a clear
"[SignatureException] 잘못된 토큰" message and throw new
GlobalException(BaseExceptionResponseStatus.INVALID_TOKEN) just like the
intended SecurityException handling.

---

Outside diff comments:
In
`@src/main/java/ku_rum/backend/global/security/JwtTokenAuthenticationFilter.java`:
- Around line 29-45: In JwtTokenAuthenticationFilter.doFilter, the try/catch
currently wraps the entire filter including chain.doFilter causing non-JWT
exceptions to be converted into token errors; narrow the scope so only
JWT-specific operations (resolveToken, jwtTokenProvider.validateToken,
isNotLogout, jwtTokenProvider.getAuthentication and
SecurityContextHolder.setAuthentication) are wrapped and catch only JWT-related
exceptions (e.g., JwtException or your provider's specific exception), then
allow other exceptions thrown by chain.doFilter(request, response) to propagate
(or rethrow them) instead of converting them to token errors.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb089c3 and fe4d5aa.

📒 Files selected for processing (4)
  • src/main/java/ku_rum/backend/global/handler/GlobalExceptionControllerAdvice.java
  • src/main/java/ku_rum/backend/global/security/JwtTokenAuthenticationFilter.java
  • src/main/java/ku_rum/backend/global/security/JwtTokenProvider.java
  • src/main/java/ku_rum/backend/global/support/status/BaseExceptionResponseStatus.java

Comment on lines 86 to +89
if (e instanceof SecurityException) {
log.debug("[SecurityException] 잘못된 토큰");
throw new JwtException("[SecurityException] 잘못된 토큰입니다.");
throw new GlobalException(BaseExceptionResponseStatus.INVALID_TOKEN);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "JwtTokenProvider.java" -type f

Repository: KU-rum/backend

Length of output: 126


🏁 Script executed:

cat -n src/main/java/ku_rum/backend/global/security/JwtTokenProvider.java

Repository: KU-rum/backend

Length of output: 8550


🌐 Web query:

JJWT 0.12.3 parseSignedClaims exception handling signature validation

💡 Result:

In JJWT 0.12.3, JwtParser.parseSignedClaims(...) is the “I expect a signed JWS with Claims” entry point, and it performs signature verification as part of parsing (assuming you configured a verification key/locator).

Correct usage (signature is validated)

try {
    Jws<Claims> jws = Jwts.parser()
        .verifyWith(key)          // SecretKey or PublicKey
        .build()
        .parseSignedClaims(token);

    Claims claims = jws.getPayload(); // only reached if signature + validations passed
} catch (JwtException e) {
    // invalid / untrusted token (includes bad signature, expired, malformed, etc.)
}

The project README explicitly notes that if signature verification fails, SignatureException (a JwtException) is thrown. [1]

What to catch / how to distinguish failures

parseSignedClaims(CharSequence) in 0.12.3 declares:

  • throws JwtException (umbrella for parse/validation problems, including signature failure)
  • throws IllegalArgumentException (null/empty/blank input)
    and documents UnsupportedJwtException when the input is not a signed Claims JWT. [2]

So typically:

catch (SignatureException e) { /* signature invalid */ }
catch (ExpiredJwtException e) { /* exp in past */ }
catch (UnsupportedJwtException e) { /* not a signed Claims JWS (wrong token type/method) */ }
catch (MalformedJwtException e) { /* bad compact serialization / invalid structure */ }
catch (SecurityException e) { /* actually a JWE and decryption failed (when using parse(..) variants) */ }
catch (JwtException e) { /* anything else JWT-related */ }
catch (IllegalArgumentException e) { /* null/blank token */ }

(Those specific exception types are the standard JJWT parse/validate failures surfaced by the parser APIs; parse(CharSequence) lists them explicitly, while parseSignedClaims groups them under JwtException.) [2]

Why parseSignedClaims (vs parse)

  • parseSignedClaims will throw UnsupportedJwtException if the token isn’t a signed Claims JWS, which helps avoid accidentally accepting an unexpected token kind. [2]
  • A long-standing concern is that the generic parse(...) can successfully parse tokens that are not signed (depending on configuration/what’s encountered), so using the more specific parse method is safer when you require a signature. [3]

Sources:
[1] JJWT GitHub README (signature verification + SignatureException guidance)
[2] JJWT 0.12.3 Javadoc: JwtParser.parseSignedClaims and parser exception contracts
[3] JJWT GitHub issue #775 discussion about parse behavior and preferring signed-parse methods


🌐 Web query:

JJWT 0.12.3 SignatureException vs SecurityException signature verification parseSignedClaims

💡 Result:

In JJWT 0.12.3, parseSignedClaims(...) is for JWS (signed) JWTs. When something goes wrong during verification/parsing, you’ll typically see one of these (both are JwtExceptions):

io.jsonwebtoken.security.SignatureException

Thrown when a JWS signature is present but cannot be verified (e.g., wrong key, token was tampered with, algorithm/key mismatch for verification). The JJWT README explicitly calls out that signature verification failure during parseSignedClaims results in a SignatureException. [1]

io.jsonwebtoken.security.SecurityException

A broader “crypto/security” JwtException category: problems with security-related elements like keys/algorithms/JCA provider, etc. (i.e., not necessarily “the signature doesn’t match”, but “something security/crypto-related failed”). [2]
Also, in the parser API docs, SecurityException is explicitly used when the compact token is actually a JWE (encrypted JWT) and decryption fails. [3]

Practical guidance

  • If you only care “token signature invalid vs everything else”, catch:
    • SignatureException → treat as bad/forged token
    • JwtException (or SecurityException) → treat as other invalid token / crypto config problem
  • If you unexpectedly see SecurityException while you believe you’re parsing a JWS, double-check you’re not receiving a JWE (encrypted token) or misconfigured crypto/provider. [3]

Sources: JJWT README (verification + parseSignedClaims) [1], JJWT security package summary (exception meanings) [2], JJWT 0.12.3 JwtParser javadoc (SecurityException for JWE decrypt failure) [3].


서명 검증 실패 예외가 INVALID_TOKEN으로 매핑되지 않습니다.

Line 86의 SecurityException은 JJWT 0.12.3에서 서명 검증 실패 시 발생하는 io.jsonwebtoken.security.SignatureException을 받지 못합니다. 현재 코드는 서명 오류를 처리하지 않아 JWT_ERROR로 폴(fall-through)됩니다.

🔧 제안 수정안
+import io.jsonwebtoken.security.SignatureException;

     } catch (Exception e) {
+        if (e instanceof SignatureException) {
+            log.debug("[SignatureException] 잘못된 토큰");
+            throw new GlobalException(BaseExceptionResponseStatus.INVALID_TOKEN);
+
+        } else if (e instanceof SecurityException) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/ku_rum/backend/global/security/JwtTokenProvider.java` around
lines 86 - 89, In JwtTokenProvider replace the current instanceof
SecurityException check with handling for
io.jsonwebtoken.security.SignatureException (or add an explicit catch for
SignatureException) so signature verification failures are mapped to
BaseExceptionResponseStatus.INVALID_TOKEN; specifically detect
SignatureException (e.g., if (e instanceof SignatureException)), log a clear
"[SignatureException] 잘못된 토큰" message and throw new
GlobalException(BaseExceptionResponseStatus.INVALID_TOKEN) just like the
intended SecurityException handling.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/test/java/ku_rum/backend/global/security/jwt/JwtTokenProviderTest.java (1)

145-147: ⚠️ Potential issue | 🟠 Major

JWT 관련 예외 타입 불일치: createTokenvalidateToken의 예외 통일이 필요합니다.

현재 코드에서 JWT 관련 메서드들이 서로 다른 예외 타입을 던지고 있습니다:

  • validateToken(): 모든 토큰 검증 실패 시 GlobalException 사용 (INVALID_TOKEN, MALFORMED_TOKEN, EXPIRED_TOKEN 등)
  • createToken(): Redis 저장 실패 시 JwtException 사용

두 메서드 모두 JWT 작업 실패이므로, 예외 타입을 통일하여 GlobalException으로 정렬하거나, 명시적인 예외 전략을 문서화해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/ku_rum/backend/global/security/jwt/JwtTokenProviderTest.java`
around lines 145 - 147, The test and implementation are inconsistent:
createToken throws JwtException while validateToken uses GlobalException; unify
them by changing createToken to throw GlobalException on Redis save failures
(use the same INVALID_TOKEN / relevant enum or create a new JWT-related
GlobalError) and update JwtTokenProviderTest (the assertion on createToken) to
expect GlobalException with the appropriate message ("레디스에 리프레시 토큰 저장 실패");
locate createToken and its Redis-save error handling and replace JwtException
usage with GlobalException construction consistent with validateToken's pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/test/java/ku_rum/backend/global/security/jwt/JwtTokenProviderTest.java`:
- Around line 112-113: Update the assertion in JwtTokenProviderTest so it not
only checks that jwtTokenProvider.validateToken(expiredToken) throws
GlobalException but also asserts the exception's error identifier equals the JWT
expiration code (e.g., EXPIRED_TOKEN or the corresponding error code/property).
Capture the thrown GlobalException from the validateToken call, then assert on
its errorCode/errorStatus/getErrorId (whichever accessor exists) to match the
expected EXPIRED_TOKEN constant to ensure the test verifies both type and
specific error identity.

---

Outside diff comments:
In `@src/test/java/ku_rum/backend/global/security/jwt/JwtTokenProviderTest.java`:
- Around line 145-147: The test and implementation are inconsistent: createToken
throws JwtException while validateToken uses GlobalException; unify them by
changing createToken to throw GlobalException on Redis save failures (use the
same INVALID_TOKEN / relevant enum or create a new JWT-related GlobalError) and
update JwtTokenProviderTest (the assertion on createToken) to expect
GlobalException with the appropriate message ("레디스에 리프레시 토큰 저장 실패"); locate
createToken and its Redis-save error handling and replace JwtException usage
with GlobalException construction consistent with validateToken's pattern.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe4d5aa and e6cefe4.

📒 Files selected for processing (1)
  • src/test/java/ku_rum/backend/global/security/jwt/JwtTokenProviderTest.java

Comment on lines 112 to +113
assertThatThrownBy(() -> jwtTokenProvider.validateToken(expiredToken))
.isInstanceOf(JwtException.class);
.isInstanceOf(GlobalException.class);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

만료 토큰 케이스는 예외 “타입” 외에 에러 식별자까지 검증해 주세요.

현재는 GlobalException 여부만 확인해서, 다른 원인의 GlobalException도 통과할 수 있습니다. JWT 예외 통일 목적에 맞게 EXPIRED_TOKEN(또는 대응되는 에러 코드/상태)까지 함께 검증하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/ku_rum/backend/global/security/jwt/JwtTokenProviderTest.java`
around lines 112 - 113, Update the assertion in JwtTokenProviderTest so it not
only checks that jwtTokenProvider.validateToken(expiredToken) throws
GlobalException but also asserts the exception's error identifier equals the JWT
expiration code (e.g., EXPIRED_TOKEN or the corresponding error code/property).
Capture the thrown GlobalException from the validateToken call, then assert on
its errorCode/errorStatus/getErrorId (whichever accessor exists) to match the
expected EXPIRED_TOKEN constant to ensure the test verifies both type and
specific error identity.

@david-parkk david-parkk merged commit 3c5e3dd into develop Mar 2, 2026
1 of 3 checks passed
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 2, 2026

@coderabbitai coderabbitai bot mentioned this pull request Mar 2, 2026
1 task
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.

JWT 에러 통합

1 participant