[feat] #15 - 토큰 재발급, 로그아웃, 탈퇴 기능 구현 - #18
Conversation
…to feat/#15-reissue-logout-withdraw
|
Warning Review limit reached
Next review available in: 51 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Walkthrough토큰 재발급, 로그아웃, 회원 탈퇴 API가 추가되고, 인증 필터가 블랙리스트를 검사하도록 바뀌었습니다. 쿠키 생성/만료 유틸과 토큰 추출 유틸이 신설됐고, 에러·성공 코드와 응답 형식, OAuth 쿠키 발급 방식도 함께 조정되었습니다. Changes인증 API 확장
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant AuthController
participant AuthService
participant RefreshTokenService
participant BlackListService
participant JwtTokenProvider
Client->>AuthController: POST /reissue (refreshToken, sessionId cookies)
AuthController->>AuthService: reissue(refreshToken, sessionId)
AuthService->>RefreshTokenService: refreshToken/sessionId 검증
RefreshTokenService-->>AuthService: 검증 결과
AuthService->>RefreshTokenService: 기존 refreshToken 삭제
AuthService->>RefreshTokenService: 새 refreshToken/sessionId 저장
AuthService-->>AuthController: ReissueResult(accessToken, refreshToken, sessionId)
AuthController-->>Client: accessToken + Set-Cookie(refreshToken, sessionId)
Client->>AuthController: POST /logout or DELETE /withdraw
AuthController->>AuthController: TokenExtractor.resolveToken(request)
AuthController->>AuthService: logout/withdraw(accessToken, userId, sessionId)
AuthService->>JwtTokenProvider: getRemainingExpiry(accessToken)
AuthService->>BlackListService: addToBlacklist(accessToken, remainingExpiry)
AuthService->>RefreshTokenService: sessionId의 refreshToken 삭제
AuthController-->>Client: 성공 응답 + 쿠키 만료
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/java/com/Timo/Timo/global/auth/utils/CookieUtil.java (1)
8-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
createCookie/expireCookie중복 로직 제거 제안두 메서드가
httpOnly,secure,path,sameSite설정을 그대로 중복하고 있습니다.expireCookie는createCookie(name, "", 0, secure)호출로 대체할 수 있어 속성 값이 어긋날 위험을 줄일 수 있습니다.♻️ 제안 diff
public static ResponseCookie expireCookie(String name, boolean secure) { - return ResponseCookie.from(name, "") - .httpOnly(true) - .secure(secure) - .path("/api/v1/auth") - .maxAge(0) - .sameSite("Strict") - .build(); + return createCookie(name, "", 0, secure); }🤖 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/Timo/Timo/global/auth/utils/CookieUtil.java` around lines 8 - 26, `CookieUtil`의 `createCookie`와 `expireCookie`에서 `httpOnly`, `secure`, `path`, `sameSite` 설정이 중복되므로, `expireCookie`는 `createCookie(name, "", 0, secure)`를 호출하도록 변경해 중복을 제거하고 두 메서드의 쿠키 속성이 항상 일치하도록 정리하세요.
🤖 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/Timo/Timo/global/auth/service/AuthService.java`:
- Around line 93-107: `AuthService.withdraw`의 순서와 트랜잭션 경계를 조정해야 합니다. 먼저
`userRepository.findById(userId)`로 사용자 존재 여부를 확인해 `USER_NOT_FOUND`가 나면 즉시 중단하고,
그 다음에 `blacklistService.addToBlacklist`와
`refreshTokenService.deleteRefreshToken` 같은 부수효과를 수행하도록 `withdraw` 흐름을 바꾸세요. 또한
`withdraw`에 `@Transactional`을 적용해 `userRepository.delete(user)`까지의 DB 작업이 하나의 경계
안에서 처리되도록 하고, `accessToken`과 `sessionId`의 null 처리도 현재처럼 유지해 예외 없는 경우만 부수효과가 실행되게
하세요.
In `@src/main/java/com/Timo/Timo/global/auth/service/BlackListService.java`:
- Around line 16-23: `BlackListService.addToBlacklist` should skip Redis writes
when `remainingExpiry` is zero or negative, because
`redisTemplate.opsForValue().set(..., TimeUnit.SECONDS)` cannot use an invalid
TTL. Add an early return guard at the start of `addToBlacklist(String
accessToken, long remainingExpiry)` so only positive TTL values are stored,
keeping the blacklist flow safe for near-expiry tokens.
---
Nitpick comments:
In `@src/main/java/com/Timo/Timo/global/auth/utils/CookieUtil.java`:
- Around line 8-26: `CookieUtil`의 `createCookie`와 `expireCookie`에서 `httpOnly`,
`secure`, `path`, `sameSite` 설정이 중복되므로, `expireCookie`는 `createCookie(name, "",
0, secure)`를 호출하도록 변경해 중복을 제거하고 두 메서드의 쿠키 속성이 항상 일치하도록 정리하세요.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1e7e08e9-329c-4423-bc3f-37575ca67938
📒 Files selected for processing (15)
src/main/java/com/Timo/Timo/global/auth/controller/AuthController.javasrc/main/java/com/Timo/Timo/global/auth/dto/ReissueResult.javasrc/main/java/com/Timo/Timo/global/auth/dto/response/AuthTokenResponse.javasrc/main/java/com/Timo/Timo/global/auth/exception/AuthErrorCode.javasrc/main/java/com/Timo/Timo/global/auth/exception/AuthSuccessCode.javasrc/main/java/com/Timo/Timo/global/auth/handler/AuthErrorResponseWriter.javasrc/main/java/com/Timo/Timo/global/auth/handler/JwtAuthenticationEntryPoint.javasrc/main/java/com/Timo/Timo/global/auth/handler/OAuthSuccessHandler.javasrc/main/java/com/Timo/Timo/global/auth/principal/CustomUserDetails.javasrc/main/java/com/Timo/Timo/global/auth/service/AuthService.javasrc/main/java/com/Timo/Timo/global/auth/service/BlackListService.javasrc/main/java/com/Timo/Timo/global/auth/utils/CookieUtil.javasrc/main/java/com/Timo/Timo/global/auth/utils/TokenExtractor.javasrc/main/java/com/Timo/Timo/global/jwt/filter/JwtAuthenticationFilter.javasrc/main/java/com/Timo/Timo/global/jwt/provider/JwtTokenProvider.java
💤 Files with no reviewable changes (3)
- src/main/java/com/Timo/Timo/global/auth/dto/response/AuthTokenResponse.java
- src/main/java/com/Timo/Timo/global/auth/handler/AuthErrorResponseWriter.java
- src/main/java/com/Timo/Timo/global/auth/principal/CustomUserDetails.java
laura-jung
left a comment
There was a problem hiding this comment.
blacklist 구현까지 잘 되어있는 것 같습니다. 코멘트 남겨두었으니 확인 부탁드립니다.
| .accessToken(result.getAccessToken()) | ||
| .build(); | ||
|
|
||
| return ResponseEntity.ok() |
There was a problem hiding this comment.
p3) 현재 컨트롤러에서 AuthReissueResponse 생성뿐 아니라 refreshToken/sessionId 쿠키 생성, Set-Cookie 헤더 설정, cache-control 설정까지 함께 처리하고 있어 응답 조립 책임이 조금 큰 것 같습니다.
비즈니스 로직은 authService.reissue()에서 처리하고 있으니, 쿠키와 ResponseEntity를 조립하는 부분은 AuthResponseFactory나 별도 helper로 분리하면 컨트롤러가 더 얇아지고 다른 인증 응답에서도 재사용하기 좋을 것 같아요!
컨트롤러에서의 로직은 최소한으로 가져가는 것이 좋습니다.
There was a problem hiding this comment.
말씀해주신 대로 쿠키 생성 및 Set-Cookie 헤더 설정 로직을 AuthResponseFactory로 분리했습니다. 이를 통해 reissue, logout, withdraw에서 거의 동일하게 반복되던 처리 로직을 공통화하여 재사용성을 높였습니다.
| @PostMapping("/token") | ||
| public ResponseEntity<BaseResponse<AuthTokenResponse>> token( | ||
| @RequestBody Map<String, String> body | ||
| @RequestBody AuthTokenRequest request |
There was a problem hiding this comment.
p2) AuthTokenRequest.code에 @NotBlank가 추가된 점은 좋은데, 컨트롤러의 @RequestBody 파라미터에 @Valid가 없어 실제 검증이 실행되지 않을 수 있을 것 같습니다.
@Valid @RequestBody AuthTokenRequest request로 변경해야 빈 문자열/공백 code가 서비스 레이어로 넘어가기 전에 차단될 것 같아요.
There was a problem hiding this comment.
말씀해주신 내용 반영했습니다! @Valid를 추가하여 요청 단계에서 code 값 검증이 수행되도록 수정했습니다. 감사합니다-!!
aneykrap
left a comment
There was a problem hiding this comment.
이제 로그인쪽이 거의 끝을 향해 달려가네용 파이팅입니다!!
| } | ||
|
|
||
| @Transactional | ||
| public void withdraw(String accessToken, Long userId, String sessionId) { |
There was a problem hiding this comment.
p2) 회원 탈퇴 시 현재 요청의 sessionId에 해당하는 refresh token만 삭제하고 있어 같은 사용자의 다른 세션 refresh token은 Redis에 남을 수 있을 것 같아요 그리고 reissue()에서는 user 존재 여부를 확인하지 않고 있어서 탈퇴 후 남아 있는 refresh token으로 access token이 재발급될 가능성이 있지 않을까 싶습니당 탈퇴 시 해당 userId의 모든 refresh token을 삭제하거나 reissue()에서 사용자 존재 여부를 검증하는 처리가 필요하지 않을까 싶어요!
There was a problem hiding this comment.
그러네요 동의합니다..!! 말씀해주신 두 가지 내용 반영했습니다!!
withdraw(): 현재 세션 토큰만 지우던 걸 refreshTokenService.deleteAllRefreshTokens(userId)로 바꿔서 해당 유저의 모든 세션을 정리하도록 했습니다.
reissue(): userRepository.existsById(userId) 체크를 추가해서, 혹시 다른 경로로 refresh token이 살아있어도 이미 탈퇴한 유저면 재발급이 막히도록 했습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/main/java/com/Timo/Timo/global/auth/factory/AuthResponseFactory.java (1)
51-56: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value로그아웃/탈퇴 응답에
Cache-Control: no-store를 추가하는 것을 고려해 보세요.
tokenResponse/reissueResponse는no-store를 설정하지만, 쿠키 만료 응답인expiredCookieResponse에는 없습니다. 인증 관련Set-Cookie응답이 캐시되지 않도록 일관되게 헤더를 부여하는 편이 안전합니다.♻️ 제안
return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, CookieUtil.expireCookie("refreshToken", cookieSecure).toString()) .header(HttpHeaders.SET_COOKIE, CookieUtil.expireCookie("sessionId", cookieSecure).toString()) + .header("Cache-Control", "no-store") .body(BaseResponse.onSuccess(successCode, null));🤖 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/Timo/Timo/global/auth/factory/AuthResponseFactory.java` around lines 51 - 56, `expiredCookieResponse(AuthSuccessCode)` is missing the same cache-prevention header used by `tokenResponse` and `reissueResponse`. Update this method in `AuthResponseFactory` to add `Cache-Control: no-store` alongside the `Set-Cookie` headers so logout/withdrawal cookie-expiration responses are handled consistently and not cached.src/main/java/com/Timo/Timo/global/auth/service/RefreshTokenService.java (1)
40-45: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
redisTemplate.keys()는 프로덕션에서 Redis를 블로킹할 수 있습니다.
keys(...)는 내부적으로 RedisKEYS명령을 사용하며, 전체 키스페이스를 O(N)으로 순회하는 동안 서버가 블로킹됩니다. 사용자별 세션 수가 늘어나고 전체 키가 많아질수록 탈퇴 요청마다 다른 명령까지 지연될 수 있습니다.SCAN기반 순회(예:ScanOptions)로 교체하는 것을 권장합니다. As per path instructions, "성능 문제나 불필요한 중복 로직이 있는지 확인해 주세요."♻️ SCAN 기반 예시
public void deleteAllRefreshTokens(String userId) { ScanOptions options = ScanOptions.scanOptions() .match(KEY_PREFIX + userId + ":*") .count(100) .build(); try (Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection( conn -> conn.keyCommands().scan(options))) { List<String> keys = new ArrayList<>(); while (cursor.hasNext()) { keys.add(new String(cursor.next(), StandardCharsets.UTF_8)); } if (!keys.isEmpty()) { redisTemplate.delete(keys); } } }🤖 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/Timo/Timo/global/auth/service/RefreshTokenService.java` around lines 40 - 45, `RefreshTokenService.deleteAllRefreshTokens` currently uses `redisTemplate.keys(...)`, which can block Redis in production because it scans the full keyspace. Replace this with a `SCAN`-based lookup using `ScanOptions` and the existing `redisTemplate` to iterate matching refresh-token keys for the given userId, collect them, and delete them only if any are found. Keep the change localized to `deleteAllRefreshTokens` and preserve the `KEY_PREFIX` pattern while removing the direct `keys()` usage.Source: Path instructions
🤖 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/Timo/Timo/global/auth/controller/AuthController.java`:
- Around line 36-37: Remove the unused cookieSecure field from AuthController,
since cookie handling now lives in AuthResponseFactory and this injected setting
is no longer referenced. Update AuthController to drop the
`@Value`("${app.auth.cookie-secure}") injection and any related imports or
constructor/field usage, keeping the cookie security configuration only in the
new response factory path.
---
Nitpick comments:
In `@src/main/java/com/Timo/Timo/global/auth/factory/AuthResponseFactory.java`:
- Around line 51-56: `expiredCookieResponse(AuthSuccessCode)` is missing the
same cache-prevention header used by `tokenResponse` and `reissueResponse`.
Update this method in `AuthResponseFactory` to add `Cache-Control: no-store`
alongside the `Set-Cookie` headers so logout/withdrawal cookie-expiration
responses are handled consistently and not cached.
In `@src/main/java/com/Timo/Timo/global/auth/service/RefreshTokenService.java`:
- Around line 40-45: `RefreshTokenService.deleteAllRefreshTokens` currently uses
`redisTemplate.keys(...)`, which can block Redis in production because it scans
the full keyspace. Replace this with a `SCAN`-based lookup using `ScanOptions`
and the existing `redisTemplate` to iterate matching refresh-token keys for the
given userId, collect them, and delete them only if any are found. Keep the
change localized to `deleteAllRefreshTokens` and preserve the `KEY_PREFIX`
pattern while removing the direct `keys()` usage.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3756d520-a9d4-444c-82e8-875dcfde388f
⛔ Files ignored due to path filters (1)
src/main/java/com/Timo/Timo/global/auth/docs/AuthControllerDocs.javais excluded by!**/docs/**
📒 Files selected for processing (5)
src/main/java/com/Timo/Timo/global/auth/controller/AuthController.javasrc/main/java/com/Timo/Timo/global/auth/dto/request/AuthTokenRequest.javasrc/main/java/com/Timo/Timo/global/auth/factory/AuthResponseFactory.javasrc/main/java/com/Timo/Timo/global/auth/service/AuthService.javasrc/main/java/com/Timo/Timo/global/auth/service/RefreshTokenService.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/Timo/Timo/global/auth/dto/request/AuthTokenRequest.java
aneykrap
left a comment
There was a problem hiding this comment.
넵 리뷰 반영된거 다 확인했습니다!! 고생 많았어용 자윤이!!!
laura-jung
left a comment
There was a problem hiding this comment.
어푸 드립니다. 이제 머지합니다아!!!
관련 이슈 🛠
작업 내용 요약 ✏️
로그인 이후 인증 관련 API 구현
RefreshToken 기반 AccessToken 재발급, 로그아웃, 회원 탈퇴 기능 제공
주요 변경 사항 🛠️
AuthControllerDocs추가 및 Swagger 문서화 적용AuthTokenRequest,ReissueResponseDTO 추가 (Map 타입 제거,@NotBlank검증 추가)트러블 슈팅 ⚽️
테스트 결과 📄
스크린샷 📷 (테스트 진행 중)
리뷰 요구사항 📢
📎 참고 자료 (선택)
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes