Skip to content

Feat/2 jwt authentication - #13

Merged
mimimya merged 9 commits into
devfrom
feat/2-jwt-authentication
Apr 27, 2026
Merged

mimimya merged 9 commits into
devfrom
feat/2-jwt-authentication

Conversation

@mimimya

@mimimya mimimya commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

📝 작업 내용

이번 PR에서 작업한 내용을 설명해주세요.

  • Spring Cloud Gateway에 JWT 인증 필터 도입.
    다운스트림 서비스로 가는 요청에 인증된 유저 정보를 헤더(X-User-Id, X-User-Role)로 주입.

🚀 주요 변경 사항

완료한 이슈 번호
Close #3
관련된 이슈 번호 (닫고 싶지 않은 경우)
Related to #

✅ 자체 체크리스트 (필수)

  • ./gradlew build 실행 결과 정상 (인증샷 첨부)
  • Postman 테스트 완료 (인증샷 첨부)
  • 팀 내 컨벤션 준수 및 불필요한 로그, import 제거
  • 중요한 변경 사항이 팀에 공유되었는지

📸 테스트 인증샷

빌드 결과 및 Postman 실행 화면을 여기에 첨부해 주세요.

1. Public Path — 토큰 없이 통과
http://localhost:8080/api/v1/users
image
2. http://localhost:8080/api/v1/meetings
image
3. 유효하지 않은 토큰
image
4. 토큰 유효 (GW에서 막지 않고 라우팅 처리 해줌)
image

💬 리뷰어 전달사항 (선택)

특별히 봐주었으면 하는 부분이나 논의가 필요한 점을 적어주세요.

  • 논의점


📎 참고 자료

관련 문서, 레퍼런스 링크 등이 있다면 여기에 첨부해주세요.

Summary by CodeRabbit

릴리스 노트

  • New Features

    • JWT 기반 인증이 API 게이트웨이에 추가되었습니다.
    • 공개 경로에 대한 인증 우회 설정이 지원됩니다.
  • Chores

    • 자동 풀 리퀘스트 리뷰어 할당 설정이 추가되었습니다.
    • 브랜치 이름 검증 자동화가 구현되었습니다.
    • CI/CD 빌드 파이프라인이 구성되었습니다.
    • JWT 관련 의존성이 추가되고 환경 설정이 외부화되었습니다.

mimimya added 8 commits April 27, 2026 02:12
JWT 토큰 검증 및 클레임 파싱 책임을 담당하는 컴포넌트.

JwtTokenProvider:
- HMAC HS256 알고리즘 (대칭키 방식)
- jwt.secret 환경변수에서 키 초기화 (256비트 이상 보장)
- validateToken(): 서명/만료/형식 검증, 5가지 예외 구분 로깅
- parseClaims(): sub(UUID) + role(String) 추출

JwtClaims (record):
- 불변 DTO, 파싱 결과 전달용

application.yml:
- jwt.secret 환경변수 패턴 추가 (운영 시 외부 주입)
- 기본값은 개발 전용

build.gradle:
- jjwt 0.12.6 의존성 추가 (api/impl/jackson)
- ext 변수 jjwtVersion 으로 단일 관리

Gateway는 토큰 검증만 책임, 생성은 User(Auth) Service 담당.
양쪽이 동일한 JWT_SECRET 공유.

Related to: #5
JWT 인증 필터를 Spring Cloud Gateway의 GlobalFilter로 구현.

JwtProperties (record):
- @ConfigurationProperties로 jwt 섹션 자동 바인딩
- secret + publicPaths 통합 관리

AuthenticationFilter:
- Public Path 매칭 시 인증 우회 (AntPathMatcher 사용)
- Bearer 형식 강제 (RFC 6750)
- 검증 실패 시 401 + JSON 에러 응답
- 검증 성공 시 X-User-Id, X-User-Role 헤더 주입
- @order HIGHEST_PRECEDENCE+100 으로 가장 일찍 실행

application.yml:
- jwt.public-paths 추가 (회원가입/로그인/actuator/webhook)

JwtTokenProvider:
- @value 주입에서 JwtProperties 의존으로 변경 (일관성)

GatewayApplication:
- @EnableConfigurationProperties(JwtProperties.class)

Related to: #6
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

개요

Spring Cloud Gateway에 JWT 인증 필터를 도입하여 요청에 인증된 사용자 정보(UUID, Role)를 헤더로 주입합니다. JJWT 의존성 추가, 토큰 검증 및 클레임 파싱 로직 구현, 공개 경로 화이트리스트 설정, CI/CD 파이프라인 구성이 포함됩니다.

변경 사항

구성 / 파일(들) 요약
GitHub 자동 할당 설정
.github/auto_assign.yml, .github/workflows/auto-assign.yml
PR 작성자를 자동 할당하고 지정된 리뷰어 6명을 구성하는 GitHub Actions 자동 할당 워크플로우 및 설정 파일 추가
브랜치 검증 워크플로우
.github/workflows/branch-name-check.yml
type/issueNumber-description 형식의 브랜치명 검증 워크플로우 추가 (허용 타입: feat, fix, refactor, docs, chore, test)
CI 워크플로우
.github/workflows/ci.yml
dev, main 브랜치로의 PR 시 JDK 21 및 Gradle을 사용한 빌드 워크플로우 추가
환경 설정
.gitignore
.envsrc/main/resources/.env 파일을 Git 추적에서 제외
Gradle 의존성
build.gradle
JJWT(jjwt-api, jjwt-impl, jjwt-jackson) 의존성 추가 및 버전 변수 정의
애플리케이션 부트스트랩
src/main/java/com/pagley/gateway/GatewayApplication.java
@EnableConfigurationProperties(JwtProperties.class) 어노테이션 추가로 JWT 설정 속성 바인딩 활성화
JWT 설정 속성
src/main/java/com/pagley/gateway/infrastructure/security/JwtProperties.java
@ConfigurationProperties(prefix = "jwt")로 JWT 시크릿과 공개 경로 목록을 application.yml에서 바인딩하는 레코드 추가
JWT 클레임 모델
src/main/java/com/pagley/gateway/infrastructure/security/JwtClaims.java
JWT 검증 후 추출되는 사용자 ID(UUID) 및 역할(String)을 캡슐화하는 레코드 추가
JWT 토큰 제공자
src/main/java/com/pagley/gateway/infrastructure/security/JwtTokenProvider.java
토큰 검증(validateToken) 및 클레임 파싱(parseClaims) 메서드를 갖춘 Spring 컴포넌트 구현. HMAC 시크릿 키 초기화 및 JJWT 예외 처리 포함
인증 필터
src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java
Spring Cloud Gateway GlobalFilter 구현: 공개 경로 패턴 우회, Bearer 토큰 추출 검증, JWT 토큰 검증, 검증 성공 시 X-User-Id, X-User-Role 헤더 주입, 검증 실패 시 401 응답 반환
애플리케이션 설정
src/main/resources/application.yml
JWT 시크릿(JWT_SECRET 환경 변수), 공개 경로 화이트리스트(/api/v1/users, /api/v1/auth/*, /actuator/*, /api/v1/payment/webhook), .env 파일 선택적 import 추가
테스트 토큰 생성기
src/test/java/com/pagley/gateway/JwtTestTokenGenerator.java
.env 파일에서 JWT_SECRET을 로드하여 유효한 토큰(1시간 유효)과 만료된 토큰 2개를 생성하는 개발용 유틸리티 클래스 추가

시퀀스 다이어그램

sequenceDiagram
    participant Client as 클라이언트
    participant Gateway as API Gateway<br/>(AuthenticationFilter)
    participant Provider as JwtTokenProvider
    participant Service as 백엔드 서비스
    
    Client->>Gateway: HTTP 요청<br/>(Authorization: Bearer token)
    Gateway->>Gateway: 경로 확인<br/>(공개 경로 여부)
    
    alt 공개 경로
        Gateway->>Service: 요청 전달
        Service->>Client: 응답
    else 보호된 경로
        Gateway->>Gateway: Authorization 헤더<br/>검증
        
        alt Bearer 토큰 없음
            Gateway->>Client: 401 UNAUTHORIZED
        else Bearer 토큰 존재
            Gateway->>Provider: validateToken(token)
            Provider->>Provider: JJWT 파싱 및<br/>시그니처 검증
            
            alt 검증 실패
                Provider->>Gateway: false
                Gateway->>Client: 401 UNAUTHORIZED
            else 검증 성공
                Provider->>Gateway: true
                Gateway->>Provider: parseClaims(token)
                Provider->>Provider: sub 클레임 파싱<br/>(UUID 변환)<br/>role 클레임 추출
                Provider->>Gateway: JwtClaims(userId, role)
                Gateway->>Gateway: X-User-Id,<br/>X-User-Role 헤더<br/>주입
                Gateway->>Service: 요청 전달<br/>(인증 정보 포함)
                Service->>Client: 응답
            end
        end
    end
Loading

예상 코드 리뷰 소요 시간

🎯 3 (중간) | ⏱️ ~25분

시 🐰

게이트웨이 앞에 필터 세우고,
토큰 검증하며 역할 지키고,
X-User 헤더 실어 보내네.
공개 경로 환영하며,
무단침입자는 401로 차단하노라! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목 'Feat/2 jwt authentication'은 변경사항의 주요 목적인 JWT 인증 필터 추가를 명확히 요약하고 있습니다.
Linked Issues check ✅ Passed 모든 코딩 요구사항이 충족되었습니다. JWT 라이브러리 의존성 추가, JwtTokenProvider 구현, AuthenticationFilter 구현, 공개 경로 설정, CI 구성이 완료되었으며, X-User-Id와 X-User-Role 헤더 주입도 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #3 이슈의 요구사항 범위 내에 있습니다. GitHub 워크플로우, 설정 파일, JWT 관련 코드, 그리고 테스트 도구 추가 등이 모두 JWT 인증 구현에 필요한 변경입니다.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/2-jwt-authentication

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

@mimimya
mimimya marked this pull request as draft April 27, 2026 06:42

@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: 6

🧹 Nitpick comments (2)
.github/workflows/auto-assign.yml (1)

16-16: 외부 GitHub Action은 태그 대신 커밋 SHA로 고정하세요.

uses: kentaro-m/auto-assign-action@v1.2.5는 변경 가능한 참조이므로 공급망 리스크가 있습니다. 릴리스 태그가 가리키는 실제 커밋 SHA로 고정하는 것이 안전합니다.

수정 예시
-        uses: kentaro-m/auto-assign-action@v1.2.5
+        uses: kentaro-m/auto-assign-action@<v1.2.5가 가리키는 커밋 SHA>

GitHub 저장소에서 릴리스 페이지나 Git 태그를 통해 정확한 SHA를 확인하세요.

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

In @.github/workflows/auto-assign.yml at line 16, Replace the mutable tag
reference "uses: kentaro-m/auto-assign-action@v1.2.5" with the exact commit SHA
for that release; locate the repository release or tag for
kentaro-m/auto-assign-action, copy the full commit SHA for the v1.2.5 tag, and
update the workflow line to use that SHA (e.g., uses:
kentaro-m/auto-assign-action@<commit-sha>) so the external action is pinned to
an immutable commit.
src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java (1)

113-120: 공개 경로 매칭이 HTTP 메서드를 고려하지 않습니다.

현재 구현은 경로만 확인하고 HTTP 메서드는 고려하지 않습니다. 연결된 이슈 #3의 요구사항에 따르면 특정 메서드만 공개되어야 합니다 (예: POST /api/v1/users).

HTTP 메서드까지 고려한 화이트리스트가 필요하다면, 설정 구조와 매칭 로직을 함께 수정해야 합니다.

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

In
`@src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java`
around lines 113 - 120, The current isPublicPath(String path) only matches paths
and ignores HTTP methods; update the API to match method+path: change
isPublicPath to accept the HTTP method (e.g., isPublicPath(String method, String
path)) and update calls in AuthenticationFilter to pass request.getMethod();
extend jwtProperties.publicPaths() to carry method info (either change to a
List<PublicPath> POJO with getMethod()/getPattern() or keep strings like "POST
/api/v1/users" and parse them), then adjust the matching logic in isPublicPath
to first compare the method (allowing wildcards like ANY or *) and then
pathMatcher.match(pattern, path) against the pattern part (or
PublicPath.getPattern()). Ensure backward compatibility or validation for
existing config entries.
🤖 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/com/pagley/gateway/infrastructure/security/JwtClaims.java`:
- Around line 10-14: JwtClaims currently allows role to be null/empty/invalid
which can lead to 500 errors; add validation in the record by implementing a
compact canonical constructor in JwtClaims that checks role is non-null and
non-blank and is one of the allowed values (validate against an existing Role
enum or a defined Set<String> ALLOWED_ROLES), and throw IllegalArgumentException
if the check fails; ensure the constructor still assigns userId and role as
before so the record invariants are enforced.

In `@src/main/java/com/pagley/gateway/infrastructure/security/JwtProperties.java`:
- Around line 15-17: JwtProperties currently exposes publicPaths as
List<String>, which cannot express HTTP method+path combinations; change the
model to represent routes as method+pathPattern pairs (e.g., introduce a
PublicRoute record/class with String method and String pathPattern) and replace
the publicPaths field with List<PublicRoute> publicRoutes in JwtProperties;
update any code that constructs or checks JwtProperties (parsing, filtering, and
authorization checks) to use PublicRoute.method and PublicRoute.pathPattern
instead of raw strings and ensure matching logic honors both method and pattern.

In
`@src/main/java/com/pagley/gateway/infrastructure/security/JwtTokenProvider.java`:
- Around line 87-91: JwtTokenProvider currently reads the role claim into a
nullable String which propagates as the literal "null" downstream; change the
role extraction in JwtTokenProvider (where claims.get(CLAIM_ROLE, String.class)
is used) to normalize nulls (e.g. String role =
Optional.ofNullable(claims.get(CLAIM_ROLE, String.class)).orElse("") or use a
default like "USER"), ensure JwtClaims stores a non-null role, and update
AuthenticationFilter to skip adding the X-User-Role header when
JwtClaims.getRole() is empty (or use the chosen default) so the literal "null"
is never injected into headers.

In `@src/main/resources/.env`:
- Around line 1-20: The committed .env in src/main/resources contains real
secrets (DB_PASSWORD, TTBKEY, JWT_SECRET) and must be removed and rotated:
delete the file from the repo, add src/main/resources/.env to .gitignore,
replace it with a non-secret template named .env.example (showing variable names
DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, AUTH_DB_NAME, TTBKEY, JWT_SECRET but
no values), and instruct ops to immediately revoke/rotate the exposed
credentials (rotate DB_PASSWORD, invalidate/regenerate TTBKEY and JWT_SECRET).
Also purge the secrets from git history using a history-rewrite tool (git
filter-repo or BFG), and update deployment config to read secrets from a secure
store (env vars, Vault/secret manager) instead of committing them.

In `@src/main/resources/application.yml`:
- Around line 89-93: The current public-path matching in
AuthenticationFilter.isPublicPath() only checks URL patterns and therefore
allows all HTTP methods for those paths; update the design so
isPublicPath(HttpServletRequest) also inspects the request method and the
configured public routes include method information (e.g., map of HTTP method →
allowed path patterns) or add a new method-aware matcher (e.g.,
isPublicPathByMethod or extend isPublicPath to accept method) and change the
config shape to specify method+path entries (or tighten application.yml entries
to exact method-specific endpoints like POST /api/v1/users and limit actuator to
safe subsets such as /actuator/health and /actuator/metrics); ensure
AuthenticationFilter uses the new method-aware logic to prevent GET/PUT/DELETE
on POST-only public endpoints and to restrict sensitive actuator endpoints.

In `@src/test/java/com/pagley/gateway/JwtTestTokenGenerator.java`:
- Around line 29-37: In JwtTestTokenGenerator where you load Properties using
new FileInputStream(envPath), replace the manual FileInputStream usage with a
try-with-resources (or otherwise ensure the stream is closed) so the
FileInputStream is automatically closed on exit; update the block that assigns
secret = props.getProperty("JWT_SECRET") inside the try-with-resources and keep
the existing IOException catch unchanged so there is no resource leak.

---

Nitpick comments:
In @.github/workflows/auto-assign.yml:
- Line 16: Replace the mutable tag reference "uses:
kentaro-m/auto-assign-action@v1.2.5" with the exact commit SHA for that release;
locate the repository release or tag for kentaro-m/auto-assign-action, copy the
full commit SHA for the v1.2.5 tag, and update the workflow line to use that SHA
(e.g., uses: kentaro-m/auto-assign-action@<commit-sha>) so the external action
is pinned to an immutable commit.

In
`@src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java`:
- Around line 113-120: The current isPublicPath(String path) only matches paths
and ignores HTTP methods; update the API to match method+path: change
isPublicPath to accept the HTTP method (e.g., isPublicPath(String method, String
path)) and update calls in AuthenticationFilter to pass request.getMethod();
extend jwtProperties.publicPaths() to carry method info (either change to a
List<PublicPath> POJO with getMethod()/getPattern() or keep strings like "POST
/api/v1/users" and parse them), then adjust the matching logic in isPublicPath
to first compare the method (allowing wildcards like ANY or *) and then
pathMatcher.match(pattern, path) against the pattern part (or
PublicPath.getPattern()). Ensure backward compatibility or validation for
existing config entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 354c7629-2e10-47fb-b0f5-5182dc867ad8

📥 Commits

Reviewing files that changed from the base of the PR and between 84c0d4f and e76c1f1.

📒 Files selected for processing (14)
  • .github/auto_assign.yml
  • .github/workflows/auto-assign.yml
  • .github/workflows/branch-name-check.yml
  • .github/workflows/ci.yml
  • .gitignore
  • build.gradle
  • src/main/java/com/pagley/gateway/GatewayApplication.java
  • src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java
  • src/main/java/com/pagley/gateway/infrastructure/security/JwtClaims.java
  • src/main/java/com/pagley/gateway/infrastructure/security/JwtProperties.java
  • src/main/java/com/pagley/gateway/infrastructure/security/JwtTokenProvider.java
  • src/main/resources/.env
  • src/main/resources/application.yml
  • src/test/java/com/pagley/gateway/JwtTestTokenGenerator.java

Comment on lines +10 to +14
public record JwtClaims(
UUID userId,
String role
) {
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

role 무검증 허용으로 유효 토큰 요청이 500으로 터질 수 있습니다.

Line 12의 role이 null/빈값/허용 외 값이어도 통과됩니다. 현재 증상(유효 토큰 500)과도 맞물리는 구간이라, 레코드에서 불변식을 강제해 방어하는 게 안전합니다.

수정 예시
 package com.pagley.gateway.infrastructure.security;
 
+import java.util.Objects;
+import java.util.Set;
 import java.util.UUID;
@@
 public record JwtClaims(
         UUID userId,
         String role
 ) {
+    private static final Set<String> ALLOWED_ROLES = Set.of("USER", "MASTER", "CREATOR");
+
+    public JwtClaims {
+        Objects.requireNonNull(userId, "userId must not be null");
+        if (role == null || role.isBlank()) {
+            throw new IllegalStateException("JWT role 클레임이 비어있습니다.");
+        }
+        if (!ALLOWED_ROLES.contains(role)) {
+            throw new IllegalStateException("지원하지 않는 role 값입니다: " + role);
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/pagley/gateway/infrastructure/security/JwtClaims.java`
around lines 10 - 14, JwtClaims currently allows role to be null/empty/invalid
which can lead to 500 errors; add validation in the record by implementing a
compact canonical constructor in JwtClaims that checks role is non-null and
non-blank and is one of the allowed values (validate against an existing Role
enum or a defined Set<String> ALLOWED_ROLES), and throw IllegalArgumentException
if the check fails; ensure the constructor still assigns userId and role as
before so the record invariants are enforced.

Comment on lines +15 to +17
public record JwtProperties(
String secret,
List<String> publicPaths

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

공개 경로를 List<String>로만 두면 메서드 제한 요구사항을 충족하지 못합니다.

Line 17 구조로는 POST /api/v1/users만 허용 같은 정책을 표현할 수 없어, 같은 경로의 다른 메서드가 의도치 않게 우회될 수 있습니다. 공개 라우트는 최소 method + pathPattern 조합으로 모델링하는 쪽이 안전합니다.

구조 개선 예시
 public record JwtProperties(
         String secret,
-        List<String> publicPaths
+        List<PublicRoute> publicRoutes
 ) {
+    public record PublicRoute(
+            String method,
+            String pathPattern
+    ) {}
 }
📝 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.

Suggested change
public record JwtProperties(
String secret,
List<String> publicPaths
public record JwtProperties(
String secret,
List<PublicRoute> publicRoutes
) {
public record PublicRoute(
String method,
String pathPattern
) {}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/pagley/gateway/infrastructure/security/JwtProperties.java`
around lines 15 - 17, JwtProperties currently exposes publicPaths as
List<String>, which cannot express HTTP method+path combinations; change the
model to represent routes as method+pathPattern pairs (e.g., introduce a
PublicRoute record/class with String method and String pathPattern) and replace
the publicPaths field with List<PublicRoute> publicRoutes in JwtProperties;
update any code that constructs or checks JwtProperties (parsing, filtering, and
authorization checks) to use PublicRoute.method and PublicRoute.pathPattern
instead of raw strings and ensure matching logic honors both method and pattern.

Comment on lines +87 to +91
UUID userId = parseUserId(claims.getSubject());
String role = claims.get(CLAIM_ROLE, String.class);

return new JwtClaims(userId, role);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

role 클레임이 null일 수 있습니다.

role 클레임이 존재하지 않으면 null이 반환되고, 이후 AuthenticationFilter에서 X-User-Role 헤더에 "null" 문자열이 주입됩니다. 다운스트림 서비스에서 문제가 발생할 수 있습니다.

🛡️ 수정 제안
         UUID userId = parseUserId(claims.getSubject());
         String role = claims.get(CLAIM_ROLE, String.class);
+        if (role == null || role.isBlank()) {
+            throw new IllegalStateException("JWT role 클레임이 비어있습니다.");
+        }

         return new JwtClaims(userId, role);
📝 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.

Suggested change
UUID userId = parseUserId(claims.getSubject());
String role = claims.get(CLAIM_ROLE, String.class);
return new JwtClaims(userId, role);
}
UUID userId = parseUserId(claims.getSubject());
String role = claims.get(CLAIM_ROLE, String.class);
if (role == null || role.isBlank()) {
throw new IllegalStateException("JWT role 클레임이 비어있습니다.");
}
return new JwtClaims(userId, role);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/pagley/gateway/infrastructure/security/JwtTokenProvider.java`
around lines 87 - 91, JwtTokenProvider currently reads the role claim into a
nullable String which propagates as the literal "null" downstream; change the
role extraction in JwtTokenProvider (where claims.get(CLAIM_ROLE, String.class)
is used) to normalize nulls (e.g. String role =
Optional.ofNullable(claims.get(CLAIM_ROLE, String.class)).orElse("") or use a
default like "USER"), ensure JwtClaims stores a non-null role, and update
AuthenticationFilter to skip adding the X-User-Role header when
JwtClaims.getRole() is empty (or use the chosen default) so the literal "null"
is never injected into headers.

Comment on lines +89 to +93
public-paths:
- /api/v1/users
- /api/v1/auth/**
- /actuator/**
- /api/v1/payment/webhook

Copy link
Copy Markdown

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:

#!/bin/bash
# actuator 관련 설정 확인
rg -n "actuator|management" --type=yaml

Repository: Pagely-wisely/gateway-server

Length of output: 129


🏁 Script executed:

# Check the full application.yml file to understand the complete context
cat -n src/main/resources/application.yml

Repository: Pagely-wisely/gateway-server

Length of output: 3253


🏁 Script executed:

# Search for Spring Security configuration
find . -type f \( -name "*Security*.java" -o -name "*Security*.kt" -o -name "*Filter*.java" \) | head -20

Repository: Pagely-wisely/gateway-server

Length of output: 157


🏁 Script executed:

# Check for any JWT or authentication filter configuration
rg -n "jwt|JwtToken|Filter|Security" --type=java -A 2 | head -100

Repository: Pagely-wisely/gateway-server

Length of output: 5858


🏁 Script executed:

cat -n src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java

Repository: Pagely-wisely/gateway-server

Length of output: 6775


🏁 Script executed:

# Check if there's Spring Security configuration with method-level restrictions
find . -type f \( -name "*.java" -o -name "*.kt" \) -exec grep -l "HttpMethod\|@PostMapping\|@GetMapping\|SecurityConfig\|method()" {} \; | head -10

Repository: Pagely-wisely/gateway-server

Length of output: 54


🏁 Script executed:

# Check for any WebFlux or MVC security configuration
rg -n "SecurityConfig|WebSecurityConfig|HttpSecurity|authorizeExchanges" --type=java | head -20

Repository: Pagely-wisely/gateway-server

Length of output: 54


🏁 Script executed:

# Search for user service controller or route definitions
find . -type f \( -name "*User*Controller.java" -o -name "*Auth*Controller.java" -o -name "*User*Routes.java" \) -o -path "*/user-service/*" 2>/dev/null | head -10

Repository: Pagely-wisely/gateway-server

Length of output: 54


🏁 Script executed:

# Check the PR or issue description to understand requirements
git log --oneline -20

Repository: Pagely-wisely/gateway-server

Length of output: 121


🏁 Script executed:

# Search for any method-based request matching in Spring Config
rg -n "HttpMethod|POST|GET.*api/v1/users" --type=java | head -30

Repository: Pagely-wisely/gateway-server

Length of output: 54


경로 기반 필터링만으로는 HTTP 메서드를 구분하지 않습니다.

AuthenticationFilterisPublicPath() 메서드는 경로 패턴만 확인하며 HTTP 메서드를 검사하지 않습니다. 따라서 /api/v1/users는 POST뿐 아니라 GET, PUT, DELETE 등 모든 HTTP 메서드에 대해 인증을 우회하게 됩니다. 이는 사용자 목록 조회 등 인증이 필요한 요청을 노출시킵니다.

/actuator/** 공개 설정도 마찬가지로 모든 actuator 엔드포인트(예: /actuator/env, /actuator/beans)를 인증 없이 접근 가능하게 하여 민감한 정보 노출 위험이 있습니다.

필터 레벨에서 메서드 기반 필터링을 추가하거나, 공개 경로를 더 구체적으로 정의해야 합니다.

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

In `@src/main/resources/application.yml` around lines 89 - 93, The current
public-path matching in AuthenticationFilter.isPublicPath() only checks URL
patterns and therefore allows all HTTP methods for those paths; update the
design so isPublicPath(HttpServletRequest) also inspects the request method and
the configured public routes include method information (e.g., map of HTTP
method → allowed path patterns) or add a new method-aware matcher (e.g.,
isPublicPathByMethod or extend isPublicPath to accept method) and change the
config shape to specify method+path entries (or tighten application.yml entries
to exact method-specific endpoints like POST /api/v1/users and limit actuator to
safe subsets such as /actuator/health and /actuator/metrics); ensure
AuthenticationFilter uses the new method-aware logic to prevent GET/PUT/DELETE
on POST-only public endpoints and to restrict sensitive actuator endpoints.

Comment on lines +29 to +37
Properties props = new Properties();
props.load(new FileInputStream(envPath));

// 2. .env 파일에서 JWT_SECRET 키로 값을 가져옴
secret = props.getProperty("JWT_SECRET");

} catch (IOException e) {
System.err.println(".env 파일을 찾을 수 없습니다: " + e.getMessage());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

리소스 누수: FileInputStream이 닫히지 않습니다.

FileInputStream을 열지만 닫지 않아 리소스 누수가 발생합니다. try-with-resources를 사용하세요.

🔧 수정 제안
-            Properties props = new Properties();
-            props.load(new FileInputStream(envPath));
-
-            // 2. .env 파일에서 JWT_SECRET 키로 값을 가져옴
-            secret = props.getProperty("JWT_SECRET");
-
-        } catch (IOException e) {
+            Properties props = new Properties();
+            try (FileInputStream fis = new FileInputStream(envPath)) {
+                props.load(fis);
+            }
+
+            // 2. .env 파일에서 JWT_SECRET 키로 값을 가져옴
+            secret = props.getProperty("JWT_SECRET");
+
+        } catch (IOException e) {
📝 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.

Suggested change
Properties props = new Properties();
props.load(new FileInputStream(envPath));
// 2. .env 파일에서 JWT_SECRET 키로 값을 가져옴
secret = props.getProperty("JWT_SECRET");
} catch (IOException e) {
System.err.println(".env 파일을 찾을 수 없습니다: " + e.getMessage());
}
Properties props = new Properties();
try (FileInputStream fis = new FileInputStream(envPath)) {
props.load(fis);
}
// 2. .env 파일에서 JWT_SECRET 키로 값을 가져옴
secret = props.getProperty("JWT_SECRET");
} catch (IOException e) {
System.err.println(".env 파일을 찾을 수 없습니다: " + e.getMessage());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/pagley/gateway/JwtTestTokenGenerator.java` around lines 29
- 37, In JwtTestTokenGenerator where you load Properties using new
FileInputStream(envPath), replace the manual FileInputStream usage with a
try-with-resources (or otherwise ensure the stream is closed) so the
FileInputStream is automatically closed on exit; update the block that assigns
secret = props.getProperty("JWT_SECRET") inside the try-with-resources and keep
the existing IOException catch unchanged so there is no resource leak.

@mimimya
mimimya force-pushed the feat/2-jwt-authentication branch from e76c1f1 to b0d853a Compare April 27, 2026 06:48
@mimimya
mimimya force-pushed the feat/2-jwt-authentication branch 2 times, most recently from d8f485a to 247d1c5 Compare April 27, 2026 06:55
@mimimya
mimimya marked this pull request as ready for review April 27, 2026 06:55

@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.

♻️ Duplicate comments (3)
src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java (1)

113-120: ⚠️ Potential issue | 🔴 Critical

Public 경로 매칭이 HTTP 메서드를 구분하지 않습니다.

Line 113~120의 isPublicPath는 path만 검사해서, 예를 들어 POST /api/v1/users만 열어야 하는 요구사항이 GET/PUT/DELETE /api/v1/users까지 인증 우회로 확장됩니다. 공개 경로는 메서드+패턴 기준으로 매칭하도록 바꿔야 합니다.

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

In
`@src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java`
around lines 113 - 120, isPublicPath currently only matches the request path so
a public rule like "POST /api/v1/users" will incorrectly allow GET/PUT/DELETE;
change the matching to be method-aware by updating isPublicPath to accept the
HTTP method (e.g., isPublicPath(String method, String path)) and update
jwtProperties.publicPaths() to return method+pattern pairs (or a structure with
getMethod()/getPattern()) instead of plain patterns; then iterate those entries
and return true only when entry.method.equalsIgnoreCase(method) &&
pathMatcher.match(entry.pattern, path) (or parse "METHOD pattern" strings and
compare both parts) so both method and path are required to match.
src/main/java/com/pagley/gateway/infrastructure/security/JwtTokenProvider.java (1)

87-90: ⚠️ Potential issue | 🟠 Major

role 클레임 null/blank 검증이 필요합니다.

Line 88에서 role을 nullable로 읽어 바로 반환하면, downstream 헤더 주입 시 500 또는 "null" 전파가 발생할 수 있습니다. 여기서 명시적으로 예외 처리해 401 흐름으로 보내는 게 안전합니다.

🔧 제안 변경
         UUID userId = parseUserId(claims.getSubject());
         String role = claims.get(CLAIM_ROLE, String.class);
+        if (role == null || role.isBlank()) {
+            throw new IllegalStateException("JWT role 클레임이 비어있습니다.");
+        }
 
         return new JwtClaims(userId, role);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/pagley/gateway/infrastructure/security/JwtTokenProvider.java`
around lines 87 - 90, JwtTokenProvider에서 claims로부터 읽은 role(CLAIM_ROLE)을 그대로 반환하면
null/blank가 downstream으로 전파되어 500이나 "null" 헤더가 생길 수 있으므로, parseUserId(...) 호출 직후
JwtTokenProvider#CLAIM_ROLE로 읽은 role 값을 null 또는 blank 체크하고 유효하지 않으면 JwtClaims를
반환하지 않고 인증 실패를 유도하는 예외(예: JwtAuthenticationException 또는 적절한
AuthenticationException)를 던져 401 흐름으로 보내도록 변경하세요; 참고 지점: 클래스 JwtTokenProvider,
상수 CLAIM_ROLE, 메서드 parseUserId(...) 및 반환 타입 JwtClaims를 수정해 role 검증 로직을 추가하세요.
src/main/resources/application.yml (1)

89-93: ⚠️ Potential issue | 🟠 Major

Actuator 전체 공개 경로는 과도합니다.

Line 92의 /actuator/**는 민감한 운영 정보 노출로 이어질 수 있습니다. 최소한 health/info 등 필요한 엔드포인트만 화이트리스트로 제한해 주세요.

🔧 제안 변경
 jwt:
   public-paths:
     - /api/v1/users
     - /api/v1/auth/**
-    - /actuator/**
+    - /actuator/health
+    - /actuator/info
     - /api/v1/payment/webhook
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/application.yml` around lines 89 - 93, The public-paths
entry currently exposes all actuator endpoints via the `/actuator/**` pattern;
restrict this by removing `/actuator/**` and explicitly whitelist only the safe
actuator endpoints required (e.g., `/actuator/health`, `/actuator/info`, and any
specific readiness/liveness endpoints you actually need) in the `public-paths`
list in application.yml so sensitive operational endpoints are not publicly
accessible; update the `public-paths` array to include only those specific
actuator paths instead of the wildcard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java`:
- Around line 113-120: isPublicPath currently only matches the request path so a
public rule like "POST /api/v1/users" will incorrectly allow GET/PUT/DELETE;
change the matching to be method-aware by updating isPublicPath to accept the
HTTP method (e.g., isPublicPath(String method, String path)) and update
jwtProperties.publicPaths() to return method+pattern pairs (or a structure with
getMethod()/getPattern()) instead of plain patterns; then iterate those entries
and return true only when entry.method.equalsIgnoreCase(method) &&
pathMatcher.match(entry.pattern, path) (or parse "METHOD pattern" strings and
compare both parts) so both method and path are required to match.

In
`@src/main/java/com/pagley/gateway/infrastructure/security/JwtTokenProvider.java`:
- Around line 87-90: JwtTokenProvider에서 claims로부터 읽은 role(CLAIM_ROLE)을 그대로 반환하면
null/blank가 downstream으로 전파되어 500이나 "null" 헤더가 생길 수 있으므로, parseUserId(...) 호출 직후
JwtTokenProvider#CLAIM_ROLE로 읽은 role 값을 null 또는 blank 체크하고 유효하지 않으면 JwtClaims를
반환하지 않고 인증 실패를 유도하는 예외(예: JwtAuthenticationException 또는 적절한
AuthenticationException)를 던져 401 흐름으로 보내도록 변경하세요; 참고 지점: 클래스 JwtTokenProvider,
상수 CLAIM_ROLE, 메서드 parseUserId(...) 및 반환 타입 JwtClaims를 수정해 role 검증 로직을 추가하세요.

In `@src/main/resources/application.yml`:
- Around line 89-93: The public-paths entry currently exposes all actuator
endpoints via the `/actuator/**` pattern; restrict this by removing
`/actuator/**` and explicitly whitelist only the safe actuator endpoints
required (e.g., `/actuator/health`, `/actuator/info`, and any specific
readiness/liveness endpoints you actually need) in the `public-paths` list in
application.yml so sensitive operational endpoints are not publicly accessible;
update the `public-paths` array to include only those specific actuator paths
instead of the wildcard.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c68e1251-d456-4fa5-9b51-d22a612787d5

📥 Commits

Reviewing files that changed from the base of the PR and between e76c1f1 and 247d1c5.

📒 Files selected for processing (12)
  • .github/auto_assign.yml
  • .github/workflows/auto-assign.yml
  • .github/workflows/branch-name-check.yml
  • .github/workflows/ci.yml
  • .gitignore
  • src/main/java/com/pagley/gateway/GatewayApplication.java
  • src/main/java/com/pagley/gateway/infrastructure/security/AuthenticationFilter.java
  • src/main/java/com/pagley/gateway/infrastructure/security/JwtClaims.java
  • src/main/java/com/pagley/gateway/infrastructure/security/JwtProperties.java
  • src/main/java/com/pagley/gateway/infrastructure/security/JwtTokenProvider.java
  • src/main/resources/application.yml
  • src/test/java/com/pagley/gateway/JwtTestTokenGenerator.java
✅ Files skipped from review due to trivial changes (7)
  • .gitignore
  • src/main/java/com/pagley/gateway/infrastructure/security/JwtClaims.java
  • .github/auto_assign.yml
  • .github/workflows/auto-assign.yml
  • .github/workflows/branch-name-check.yml
  • .github/workflows/ci.yml
  • src/main/java/com/pagley/gateway/infrastructure/security/JwtProperties.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/com/pagley/gateway/JwtTestTokenGenerator.java
  • src/main/java/com/pagley/gateway/GatewayApplication.java

@Pagely-wisely Pagely-wisely deleted a comment from coderabbitai Bot Apr 27, 2026

@hhegi hhegi 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.

👍👍

@mimimya
mimimya merged commit c807288 into dev Apr 27, 2026
4 checks passed
@mimimya
mimimya deleted the feat/2-jwt-authentication branch May 18, 2026 01:30
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.

[FEAT] Gateway JWT 인증 필터 및 X-User-Id 헤더 주입

3 participants