Skip to content

Feat(#77): Access Token 블랙리스트 (Redis 기반) - #79

Merged
ch0412 merged 4 commits into
developfrom
feat/77-token-blacklist
Aug 6, 2026
Merged

Feat(#77): Access Token 블랙리스트 (Redis 기반)#79
ch0412 merged 4 commits into
developfrom
feat/77-token-blacklist

Conversation

@ch0412

@ch0412 ch0412 commented Aug 6, 2026

Copy link
Copy Markdown
Member

📋 작업 내용

  • Redis 컨테이너 추가 (docker-compose)
  • Spring Data Redis 의존성 추가 및 application.yml 설정
  • TokenBlacklistService 구현 (로그아웃 시 Access Token을 Redis에 TTL과 함께 저장)
  • JwtFilter에 블랙리스트 체크 로직 추가
  • AuthService.logout에서 Access Token 블랙리스트 등록 처리

🧪 테스트 결과

  • 로그인 → 로그아웃 → 동일 Access Token으로 API 호출 시 401 반환 확인

🔗 관련 이슈

Closes #77

✅ 체크리스트

  • 관련 이슈를 연결했습니다.
  • 구현 범위와 변경 이유를 설명했습니다.
  • 로컬 테스트를 통과했습니다.
  • API 변경 사항을 Swagger에 반영했습니다.
  • DB 변경 사항과 마이그레이션을 포함했습니다.
  • 민감 정보가 코드·로그·테스트 데이터에 포함되지 않았습니다.
  • 프론트엔드에 영향을 주는 응답 스키마 또는 Enum 변경을 공유했습니다.
  • 병합 전 작업 브랜치를 삭제하지 않았습니다.

Summary by CodeRabbit

  • New Features

    • Added Redis-backed token management with persistent storage and health checks.
    • Configurable Redis connection settings are now available for local, test, and production environments.
    • Logged-out access tokens are blocked until they expire.
  • Bug Fixes

    • Prevented authentication with tokens revoked during logout.
    • Improved logout handling by invalidating both refresh and access tokens.
    • Added Redis availability checks to support reliable authentication processing.

@ch0412 ch0412 self-assigned this Aug 6, 2026
@ch0412 ch0412 added the feat Implementing new features label Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ch0412, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cce2fcd-93a4-40fa-bede-d1ab51d6273a

📥 Commits

Reviewing files that changed from the base of the PR and between f86c012 and afb97f1.

📒 Files selected for processing (1)
  • src/main/java/com/gold/safefam/global/security/TokenBlacklistService.java
📝 Walkthrough

Walkthrough

The change adds Redis configuration and runtime wiring, introduces a Redis-backed access-token blacklist, registers access tokens during logout, and rejects blacklisted tokens in JwtFilter.

Changes

Redis token blacklist

Layer / File(s) Summary
Redis configuration and runtime wiring
.env.example, .env.prod.example, build.gradle, docker-compose.yml, .github/workflows/backend-ci.yml, src/main/resources/*.yml, src/test/resources/application-test.yml
The application, Docker Compose environment, and CI tests receive Redis connection settings. Docker Compose adds a password-protected Redis service with persistence, health checks, and backend startup ordering.
Redis-backed blacklist service
src/main/java/com/gold/safefam/global/security/TokenBlacklistService.java
TokenBlacklistService stores blacklisted tokens with their remaining lifetime and checks prefixed Redis keys.
Logout registration and JWT enforcement
src/main/java/com/gold/safefam/domain/auth/controller/AuthController.java, src/main/java/com/gold/safefam/domain/auth/service/AuthService.java, src/main/java/com/gold/safefam/global/security/JwtFilter.java
Logout receives the HTTP request, returns and blacklists the bearer access token, and JwtFilter skips authentication for blacklisted tokens.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AuthController
  participant AuthService
  participant TokenBlacklistService
  participant JwtFilter
  AuthController->>AuthService: pass logout request and refresh token
  AuthService-->>AuthController: return bearer access token
  AuthController->>TokenBlacklistService: store access token with remaining TTL
  JwtFilter->>TokenBlacklistService: check access token membership
  TokenBlacklistService-->>JwtFilter: return blacklist status
  JwtFilter-->>JwtFilter: skip authentication for blacklisted token
Loading

Possibly related PRs

Suggested reviewers: late-bl00mer, pearseona

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 The title clearly identifies the Redis-based access-token blacklist feature, which is the main change in the pull request.
Linked Issues check ✅ Passed The changes implement Redis configuration, token blacklist storage and lookup, logout registration, and JwtFilter blocking required by issue #77.
Out of Scope Changes check ✅ Passed The configuration, dependency, CI, Redis service, logout flow, and JwtFilter changes directly support issue #77 objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/77-token-blacklist

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.

@ch0412 ch0412 changed the title [Feat] Access Token 블랙리스트 (Redis 기반) #77 Feat(#77): Access Token 블랙리스트 (Redis 기반) Aug 6, 2026

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/gold/safefam/domain/auth/service/AuthService.java`:
- Around line 139-148: Update logout() so tokenBlacklistService.blacklist() is
not executed before the refresh-token database transaction commits; register the
access-token blacklist operation for after-commit processing using the project’s
transaction synchronization, durable outbox, or retry mechanism, and ensure
failed post-commit processing is retried reliably while preserving both
revocations.

In `@src/main/java/com/gold/safefam/global/security/JwtFilter.java`:
- Around line 49-53: Update JwtFilter’s
tokenBlacklistService.isBlacklisted(token) path so Redis failures are handled
intentionally rather than escaping before the existing try block and
filterChain.doFilter flow. Configure explicit Redis connection/command timeouts
in the relevant Redis client configuration, and apply one consistent policy for
blacklist lookup failures—preferably fail-closed by rejecting the request, or
preserve a documented fail-open behavior—without allowing the exception to abort
valid JWT processing unexpectedly.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f0d539a8-9828-454a-bb2e-28241e801df4

📥 Commits

Reviewing files that changed from the base of the PR and between 1894b0d and 483a86f.

📒 Files selected for processing (10)
  • .env.example
  • .env.prod.example
  • build.gradle
  • docker-compose.yml
  • src/main/java/com/gold/safefam/domain/auth/controller/AuthController.java
  • src/main/java/com/gold/safefam/domain/auth/service/AuthService.java
  • src/main/java/com/gold/safefam/global/security/JwtFilter.java
  • src/main/java/com/gold/safefam/global/security/TokenBlacklistService.java
  • src/main/resources/application-prod.yml
  • src/main/resources/application.yml

Comment on lines +139 to +148

// Access Token 블랙리스트 등록
String bearerToken = httpRequest.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
String accessToken = bearerToken.substring(7);
if (jwtUtil.validateToken(accessToken)) {
long remaining = jwtUtil.getExpiration(accessToken).getTime() - System.currentTimeMillis();
tokenBlacklistService.blacklist(accessToken, remaining);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== locate target files ==="
fd -a 'AuthService\.java$|TokenBlacklistService\.java$|RefreshToken|RefreshToken.*|JwtUtil\.java$' . | sed 's#^\./##' | head -100

echo "=== AuthService outline and relevant lines ==="
if [ -f src/main/java/com/gold/safefam/domain/auth/service/AuthService.java ]; then
  wc -l src/main/java/com/gold/safefam/domain/auth/service/AuthService.java
  sed -n '1,220p' src/main/java/com/gold/safefam/domain/auth/service/AuthService.java | cat -n
fi

echo "=== transactional/blacklist refresh token usages ==="
rg -n "blacklist|refresh|refreshToken|delete|remove|logout|`@Transactional`|TokenBlacklistService|jwtUtil" src/main/java || true

echo "=== service files with transaction annotations ==="
rg -n "`@Transactional`|class .*AuthService|interface TokenBlacklistService|class TokenBlacklistService" src/main/java || true

Repository: SafeFam/SafeFam_BE

Length of output: 33338


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== TokenBlacklistService ==="
sed -n '1,120p' src/main/java/com/gold/safefam/global/security/TokenBlacklistService.java | cat -n

echo "=== JwtFilter blacklist usage ==="
sed -n '1,120p' src/main/java/com/gold/safefam/global/security/JwtFilter.java | cat -n

echo "=== Outbox related files ==="
for f in $(fd -a 'Outbox.*|Event.*Service|Messaging.*' src/main/java/src/main/java 2>/dev/null || fd -a 'Outbox.*|Event.*Service|Messaging.*' src/main/java 2>/dev/null | head -50); do
  echo "--- $f"
  wc -l "$f"
  rg -n "Outbox|publish|event|TransactionalEventListener|blacklist|logout|refresh" "$f" || true
done

echo "=== refresh token entity/repository ==="
sed -n '1,120p' src/main/java/com/gold/safefam/domain/auth/entity/RefreshToken.java | cat -n
sed -n '1,120p' src/main/java/com/gold/safefam/domain/auth/repository/RefreshTokenRepository.java | cat -n

echo "=== build deps for Redis/transaction outbox ==="
find . -maxdepth 4 \( -name 'pom.xml' -o -name 'build.gradle' -o, -name 'build.gradle.kts' \) -print | while read -r f; do
  echo "--- $f"
  rg -n "spring-data-redis|spring-boot-starter-data-redis|redis|transaction|outbox|messaging|kafka|oracle" "$f" || true
done

Repository: SafeFam/SafeFam_BE

Length of output: 15496


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== TokenBlacklistService ==="
sed -n '1,120p' src/main/java/com/gold/safefam/global/security/TokenBlacklist/TokenBlacklistService.java | cat -n

echo "=== Outbox related files ==="
fd -a 'Outbox.*|Event.*Service|Messaging.*' src/main/java 2>/dev/null | while read -r f; do
  echo "--- $f"
  rg -n "Outbox|publish|event|TransactionalEventListener|blacklist|logout|refresh" "$f" || true
done

echo "=== build deps for Redis/transaction outbox ==="
find . -maxdepth 4 \( -name 'pom.xml' -o -name 'build.gradle' -o -name 'build.gradle.kts' \) -print | while read -r f; do
  echo "--- $f"
  rg -n "spring-data-redis|spring-boot-starter-data-redis|redis|transaction|outbox|messaging|kafka|oracle" "$f" || true
done

Repository: SafeFam/SafeFam_BE

Length of output: 238


Define an atomic logout policy across Redis and the database.

logout() deletes the refresh token in the DB transaction, but tokenBlacklistService.blacklist() writes Redis before commit. If the database commit fails after the Redis write, the refresh token can remain usable while the access token is blacklisted. Use an after-commit mechanism, such as a durable outbox or explicit retry policy, so both revocation records can be processed reliably.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/gold/safefam/domain/auth/service/AuthService.java` around
lines 139 - 148, Update logout() so tokenBlacklistService.blacklist() is not
executed before the refresh-token database transaction commits; register the
access-token blacklist operation for after-commit processing using the project’s
transaction synchronization, durable outbox, or retry mechanism, and ensure
failed post-commit processing is retried reliably while preserving both
revocations.

Comment thread src/main/java/com/gold/safefam/global/security/JwtFilter.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: 2

🧹 Nitpick comments (1)
.github/workflows/backend-ci.yml (1)

22-32: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Test the authenticated Redis contract in CI.

docker-compose.yml starts Redis with --requirepass, but this workflow starts Redis without authentication and sets REDIS_PASSWORD to an empty value. CI therefore cannot detect an incorrect or missing Redis password in src/main/resources/application-prod.yml. Configure the CI Redis instance and health check with a CI-only password, then pass the same value to the Gradle step, or add a separate authenticated integration test.

Also applies to: 53-56

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/backend-ci.yml around lines 22 - 32, Update the
services.redis CI configuration to start Redis with a non-empty CI-only password
and make its health check authenticate using that same password. Pass the
identical value as REDIS_PASSWORD to the Gradle test step so
application-prod.yml is validated against authenticated Redis.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/gold/safefam/domain/auth/controller/AuthController.java`:
- Around line 116-120: Update the logout flow around AuthController and
tokenBlacklistService.blacklist so blacklist registration is durable after
authService.logout commits. Enqueue the access-token blacklist operation in an
after-commit outbox or equivalent reliable retry mechanism, and process retries
until success so a blacklist failure cannot leave the token usable or require
the client to retry logout.
- Line 119: The TokenBlacklistService must not use the raw bearer token in Redis
keys. Update both blacklist and isBlacklisted to hash the token and construct
the key with the resulting digest, preserving consistent hashing so blacklist
lookups continue to work; the AuthController call should remain unchanged.

---

Nitpick comments:
In @.github/workflows/backend-ci.yml:
- Around line 22-32: Update the services.redis CI configuration to start Redis
with a non-empty CI-only password and make its health check authenticate using
that same password. Pass the identical value as REDIS_PASSWORD to the Gradle
test step so application-prod.yml is validated against authenticated Redis.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b54184e-bd90-4705-9320-528beeef5d66

📥 Commits

Reviewing files that changed from the base of the PR and between 483a86f and f86c012.

📒 Files selected for processing (5)
  • .github/workflows/backend-ci.yml
  • src/main/java/com/gold/safefam/domain/auth/controller/AuthController.java
  • src/main/java/com/gold/safefam/domain/auth/service/AuthService.java
  • src/main/java/com/gold/safefam/global/security/JwtFilter.java
  • src/test/resources/application-test.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/gold/safefam/global/security/JwtFilter.java

Comment on lines +116 to +120
String accessToken = authService.logout(userId, request.refreshToken(), httpRequest);
if (accessToken != null && jwtUtil.validateToken(accessToken)) {
long remaining = jwtUtil.getExpiration(accessToken).getTime() - System.currentTimeMillis();
tokenBlacklistService.blacklist(accessToken, remaining);
}

@coderabbitai coderabbitai Bot Aug 6, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major

Make blacklist registration recoverable after the database commit.

When this code receives the result from authService.logout(...), the refresh-token deletion has already committed. If tokenBlacklistService.blacklist(...) fails, the access token remains usable. The deleted refresh token prevents the client from retrying logout to create the missing blacklist entry. Use a durable after-commit outbox or a reliable retry mechanism.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/gold/safefam/domain/auth/controller/AuthController.java`
around lines 116 - 120, Update the logout flow around AuthController and
tokenBlacklistService.blacklist so blacklist registration is durable after
authService.logout commits. Enqueue the access-token blacklist operation in an
after-commit outbox or equivalent reliable retry mechanism, and process retries
until success so a blacklist failure cannot leave the token usable or require
the client to retry logout.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Known limitation, outbox 패턴으로 개선 예정

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ch0412, 확인했습니다. 이 PR의 범위에서는 해당 제한 사항을 유지하겠습니다.

후속 작업으로 GitHub 이슈를 생성할까요? 이슈에는 트랜잭션 커밋 후 블랙리스트 등록을 위한 outbox 처리, 재시도, 그리고 Redis 장애 후 복구 검증을 포함하겠습니다.

You are interacting with an AI system.

@ch0412
ch0412 merged commit 7bb9463 into develop Aug 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat Implementing new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Access Token 블랙리스트 (Redis 기반)

1 participant