Conversation
Walkthrough탈퇴 상태를 JWT 클레임에 추가하고 인증 필터에서 탈퇴 사용자를 차단하되 복구 엔드포인트는 허용합니다. 로그아웃·탈퇴·복구 API와 소프트 삭제/복구 로직, OAuth2 핸들러·보안 설정 연동이 추가되었습니다. Changes
sequenceDiagram
actor Client
participant Filter as JwtAuthenticationFilter
participant Token as JwtHandler/TokenProvider
participant Auth as SecurityContext
participant API as UserController / UserApi
participant Service as UserService
participant Repo as UserRepository
participant RTokenRepo as RefreshTokenRepository
Client->>Filter: 요청 + JWT
Filter->>Token: 토큰 파싱 -> JwtUserClaim(withdrawn)
Token-->>Filter: JwtAuthentication(withdrawn=true)
Filter->>Filter: isWithdrawn() 검사 && !isRestoreEndpoint()
alt withdrawn && non-restore
Filter-->>Client: USER_WITHDRAWN 예외 응답
else restore endpoint or not withdrawn
Filter->>Auth: SecurityContext에 인증 설정
Client->>API: 컨트롤러 호출 진행
end
Note over Client,Service: 복구 흐름
Client->>API: POST /api/v1/user/restore
API->>Service: restoreUser(userId)
Service->>Repo: 탈퇴 사용자 조회
Repo-->>Service: User(삭제 상태, prefixed fields)
Service->>Service: removeDeletedPrefix -> user.restore(...)
Service->>RTokenRepo: RefreshToken 생성/저장
Service-->>API: TokenResponse 반환
API-->>Client: 토큰 포함 응답
Note over Client,Service: 탈퇴 흐름
Client->>API: DELETE /api/v1/user/withdraw
API->>Service: withdrawUser(userId)
Service->>RTokenRepo: 사용자 관련 RefreshToken 삭제
Service->>Repo: soft delete 실행
Service-->>API: 성공 응답
API-->>Client: 성공 응답
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 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: 7
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/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java (1)
59-66: 주석 업데이트 필요Line 62의 주석이 "FARMER"를 언급하고 있지만 실제 코드는
ADMIN역할을 검증합니다. 주석을 현재 로직에 맞게 수정해 주세요.private void validateAdminRole(JwtUserClaim claims) { Long userId = claims.userId(); - // 토큰의 권한은 FARMER지만 DB에 저장된 권한이 FARMER가 아닌 경우 예외 반환 + // 토큰의 권한은 ADMIN이지만 DB에 저장된 권한이 ADMIN이 아닌 경우 예외 반환 if (UserRole.ADMIN.equals(claims.role()) && !userService.isAdmin(userId)) { throw new JwtAccessDeniedException(); } }
🧹 Nitpick comments (6)
src/main/java/com/gpt/geumpumtabackend/user/repository/UserRepository.java (1)
15-21:findByProviderIdAndDeletedAtIsNull반환 타입 일관성 검토 제안
existsBy.../findByProviderAndProviderIdAndDeletedAtIsNull는Optional<User>로 null 가능성을 표현하고 있는데,findByProviderIdAndDeletedAtIsNull만User를 직접 반환하고 있습니다. 항상 존재한다는 강한 보장이 있는게 아니라면, 이 메서드도Optional<User>로 통일해 두는 편이 NPE나 실수 방지에 유리해 보입니다.src/main/java/com/gpt/geumpumtabackend/global/config/security/SecurityConfig.java (1)
6-7: OAuth2 실패 핸들러 리다이렉트 전략 점검 권장
OAuth2AuthenticationFailureHandler를failureHandler로 연결한 것은 적절해 보입니다. 다만 현재 실패 시 항상"/login?error"로 리다이렉트하고 있어서, 프론트엔드가 SPA/외부 도메인 콜백(예:redirectUri기반)으로 동작하는 경우 UX가 어색해질 수 있습니다.
추후 필요하다면 성공 핸들러와 유사하게 state/redirectUri를 활용해 실패 시에도 프론트 콜백으로 돌려보내는 구조를 검토해 보셔도 좋겠습니다.Also applies to: 33-38, 63-64
src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationFailureHandler.java (1)
7-8: 실패 로그 출력 방식 개선 제안 (System.out→ 로거)
@Component로 빈 등록한 변경은 SecurityConfig 연동 목적에 잘 맞습니다.
다만onAuthenticationFailure내부에서System.out.println과exception.printStackTrace()를 사용하고 있어 운영 환경에서는 로그 레벨/포맷 제어가 어렵습니다.추후 다음과 같이
Slf4j등의 로거를 사용하도록 정리하는 것을 권장드립니다.@Slf4j @Component public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { log.warn("❌ OAuth2 로그인 실패: {}", exception.getMessage(), exception); response.sendRedirect("/login?error"); } }Also applies to: 11-22
src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthenticationFilter.java (1)
5-6: 탈퇴 계정의 API 접근 차단 로직은 명확하나, 복구 엔드포인트 매칭 방식은 조금 더 유연하게 하는 것이 좋습니다
JwtAuthentication인 경우에만jwtAuth.isWithdrawn()을 검사하고,/api/v1/user/restore(POST)에 대해서만 예외를 두는 구조는 PR 설명과 정확히 일치합니다.다만:
return method.equalsIgnoreCase("POST") && uri.equals("/api/v1/user/restore");
- 앱에 contextPath가 붙거나, REST URL 버전이 변경되거나, trailing slash 차이(
/api/v1/user/restore/)가 생기면 매칭이 쉽게 깨질 수 있습니다.가능하다면
- 컨트롤러에서 사용하는 URL을 상수로 분리해 공유하거나,
AntPathRequestMatcher("/api/v1/user/restore", "POST")같은 RequestMatcher를 활용하는 쪽이 유지보수에 더 안전해 보입니다.정책 자체는 좋은데, URI 문자열 하드코딩 부분만 한 번 더 점검해 보시면 좋겠습니다.
Also applies to: 57-63, 89-95
src/main/java/com/gpt/geumpumtabackend/user/service/UserService.java (1)
126-131: 메서드 가시성을private으로 변경 권장
removeDeletedPrefix는 클래스 내부에서만 사용되므로private으로 선언하는 것이 캡슐화에 더 적합합니다.- public String removeDeletedPrefix(String value) { + private String removeDeletedPrefix(String value) {src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java (1)
7-16:withdrawn필드는Boolean보다는 원시형boolean으로 고정하는 편이 안전해 보입니다.도메인 상 “탈퇴 여부”는 null 이 의미가 없는 값이라 항상 true/false 둘 중 하나여야 할 것 같습니다. 그런데 현재 시그니처는
Boolean을 사용하고 있어,create(Long userId, UserRole role, Boolean withdrawn)호출 시 null 이 들어가면 이후 언박싱 과정에서 NPE 가 발생할 수 있습니다(예:JwtAuthentication생성 시boolean으로 다루는 경우).선호하시는 방향 두 가지 중 하나를 추천드립니다.
null 을 허용하지 않는 경우(권장)
레코드와 팩토리 메서드 시그니처를 모두boolean withdrawn으로 변경해 null 을 모델링하지 않도록 강제합니다.
- public record JwtUserClaim(
Long userId,UserRole role,Boolean withdrawn- ) {
- public record JwtUserClaim(
Long userId,UserRole role,boolean withdrawn- ) {
@@
- public static JwtUserClaim create(Long userId, UserRole role, Boolean withdrawn) {
- public static JwtUserClaim create(Long userId, UserRole role, boolean withdrawn) {
return new JwtUserClaim(userId, role, withdrawn);
}2. **이전 토큰 호환 등으로 null 을 일부 허용해야 하는 경우** 타입은 `Boolean` 을 유지하되, `JwtAuthentication` / 필터에서 `null` 을 명시적으로 처리해 주세요(예: `Boolean.TRUE.equals(claim.withdrawn())` 형태로만 비교). 둘 중 어떤 전략을 쓸지 결정하신 뒤, 해당 의미에 맞춰 전체 사용처에서 일관되게 처리되는지 한 번 더 확인해 주시면 좋겠습니다. </blockquote></details> </blockquote></details> <details> <summary>📜 Review details</summary> **Configuration used**: CodeRabbit UI **Review profile**: CHILL **Plan**: Pro <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 781595a33836d0d3564b0744c7278e95b60371bb and 00ccf1814745bfd4489306c231db212100553d23. </details> <details> <summary>📒 Files selected for processing (15)</summary> * `src/main/java/com/gpt/geumpumtabackend/global/config/security/SecurityConfig.java` (3 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/exception/ExceptionType.java` (1 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthentication.java` (2 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthenticationFilter.java` (3 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtHandler.java` (3 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java` (1 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java` (4 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationFailureHandler.java` (1 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java` (1 hunks) * `src/main/java/com/gpt/geumpumtabackend/global/oauth/service/CustomOAuth2UserService.java` (1 hunks) * `src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java` (2 hunks) * `src/main/java/com/gpt/geumpumtabackend/user/controller/UserController.java` (1 hunks) * `src/main/java/com/gpt/geumpumtabackend/user/domain/User.java` (2 hunks) * `src/main/java/com/gpt/geumpumtabackend/user/repository/UserRepository.java` (2 hunks) * `src/main/java/com/gpt/geumpumtabackend/user/service/UserService.java` (5 hunks) </details> <details> <summary>🧰 Additional context used</summary> <details> <summary>🧠 Learnings (1)</summary> <details> <summary>📚 Learning: 2025-11-10T08:36:54.339Z</summary>Learnt from: kon28289
Repo: Geumpumta/backend PR: 14
File: src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java:54-73
Timestamp: 2025-11-10T08:36:54.339Z
Learning: In the Geumpumta backend project (Java/Spring Boot), a hierarchical role structure is configured where ADMIN > USER. This means ADMIN users automatically inherit USER role permissions through Spring Security's RoleHierarchy, so endpoints with PreAuthorize("hasRole('USER')") are accessible by both USER and ADMIN users.**Applied to files:** - `src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java` </details> </details><details> <summary>🧬 Code graph analysis (3)</summary> <details> <summary>src/main/java/com/gpt/geumpumtabackend/user/controller/UserController.java (1)</summary><blockquote> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/response/ResponseUtil.java (1)</summary> * `ResponseUtil` (5-28) </details> </blockquote></details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationFailureHandler.java (1)</summary><blockquote> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java (1)</summary> * `Component` (24-59) </details> </blockquote></details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java (2)</summary><blockquote> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/jwt/exception/JwtAccessDeniedException.java (1)</summary> * `JwtAccessDeniedException` (6-10) </details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/jwt/exception/JwtTokenInvalidException.java (1)</summary> * `JwtTokenInvalidException` (6-14) </details> </blockquote></details> </details> </details> <details> <summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)</summary> * GitHub Check: build </details> <details> <summary>🔇 Additional comments (8)</summary><blockquote> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/exception/ExceptionType.java (1)</summary><blockquote> `35-36`: **`USER_WITHDRAWN` 예외 타입 추가 방향 적절** 탈퇴 사용자 상태를 구분하는 `USER_WITHDRAWN(FORBIDDEN, "U005", "탈퇴한 사용자입니다.")` 추가는 JWT 필터/인증 흐름과도 잘 맞고, 코드/메시지/상태값 구성도 일관적입니다. </blockquote></details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java (1)</summary><blockquote> `42-48`: **탈퇴 상태 플래그 전달 방식 적절** `principal.getUser().getDeletedAt() != null`로 탈퇴 여부를 계산해 `JwtUserClaim`에 함께 넣는 구조는 이후 `JwtAuthenticationFilter`에서 `/api/v1/user/restore`만 허용하는 정책과 잘 맞습니다. 토큰 재발급/수명 전략(예: 탈퇴 계정용 액세스 토큰 TTL을 더 짧게 가져갈지 등)은 운영 정책에 따라 조정하시면 될 것 같고, 현재 구현 자체에는 큰 문제 없어 보입니다. </blockquote></details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/oauth/service/CustomOAuth2UserService.java (2)</summary><blockquote> `108-128`: **LGTM!** `resolveProvider`의 switch 표현식과 `createNewUser`의 빌더 패턴 사용이 깔끔하고 가독성이 좋습니다. 기존 인라인 로직을 별도 메서드로 분리하여 코드 구조가 개선되었습니다. --- `100-106`: **No issue found - withdrawn user handling is correctly implemented** The code properly handles withdrawn users as intended. `findByProviderAndProviderId` correctly returns all users (including withdrawn ones), and `OAuth2AuthenticationSuccessHandler.onAuthenticationSuccess` (line 45) explicitly checks `deletedAt` and flags withdrawn users in the JWT claim. The recovery token is issued with `isWithdrawn=true` in the `JwtUserClaim`, which aligns with the PR objective. </blockquote></details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/user/controller/UserController.java (1)</summary><blockquote> `79-107`: **LGTM!** 새로운 엔드포인트들이 기존 패턴을 일관되게 따르고 있으며, `UserService`로의 위임과 응답 처리가 적절합니다. </blockquote></details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthentication.java (1)</summary><blockquote> `11-23`: **LGTM!** `withdrawn` 필드 추가와 `JwtUserClaim`으로부터의 생성자 매핑이 적절하게 구현되었습니다. 기존 `Authentication` 인터페이스와의 통합도 잘 유지되고 있습니다. </blockquote></details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/user/service/UserService.java (1)</summary><blockquote> `110-124`: No action needed. The User entity does not have a `@Where` annotation, so `findById()` will successfully return soft-deleted users. The soft-delete implementation uses only `@SQLDelete` to mark deletions, and does not filter out deleted records at the ORM level. The `restoreUser()` method works as intended. > Likely an incorrect or invalid review comment. </blockquote></details> <details> <summary>src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java (1)</summary><blockquote> `168-173`: The `/restore` endpoint security is properly implemented. `JwtAuthenticationFilter` explicitly handles withdrawn users by checking `if (jwtAuth.isWithdrawn() && !isRestoreEndpoint(request))` at lines 59-61. This allows withdrawn users to access only the `/restore` endpoint while blocking them from other endpoints. The `@PreAuthorize("hasRole('USER')")` constraint works correctly since withdrawn users retain their USER role. No additional exception handling is needed—the existing filter logic already enforces the intended behavior from the PR objectives. > Likely an incorrect or invalid review comment. </blockquote></details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtAuthentication.java
Show resolved
Hide resolved
src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java (1)
42-43: ADMIN 권한 검증 로직과 주석을 일치시키는 것이 좋겠습니다.
validateAdminRole에서는 ADMIN 권한 토큰에 대해서만userService.isAdmin을 확인하는데, 주석에는 여전히 FARMER 기준으로 적혀 있어 의미가 조금 혼동될 수 있습니다. 주석을 ADMIN 기준으로 수정해 두면 이후 유지보수 시 의도가 더 명확할 것 같습니다.Also applies to: 58-65
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/main/java/com/gpt/geumpumtabackend/global/base/BaseEntity.java(1 hunks)src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtHandler.java(3 hunks)src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java(1 hunks)src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java(3 hunks)src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java(2 hunks)src/main/java/com/gpt/geumpumtabackend/user/domain/User.java(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-10T08:36:54.339Z
Learnt from: kon28289
Repo: Geumpumta/backend PR: 14
File: src/main/java/com/gpt/geumpumtabackend/user/api/UserApi.java:54-73
Timestamp: 2025-11-10T08:36:54.339Z
Learning: In the Geumpumta backend project (Java/Spring Boot), a hierarchical role structure is configured where ADMIN > USER. This means ADMIN users automatically inherit USER role permissions through Spring Security's RoleHierarchy, so endpoints with PreAuthorize("hasRole('USER')") are accessible by both USER and ADMIN users.
Applied to files:
src/main/java/com/gpt/geumpumtabackend/global/jwt/TokenProvider.java
🧬 Code graph analysis (1)
src/main/java/com/gpt/geumpumtabackend/user/domain/User.java (1)
src/main/java/com/gpt/geumpumtabackend/global/base/BaseEntity.java (1)
Getter(16-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
src/main/java/com/gpt/geumpumtabackend/global/base/BaseEntity.java (1)
33-35: 소프트 삭제 복구용 restore() 구현이 요구사항과 잘 맞습니다.
deletedAt만 null 로 명확히 초기화해 상속 엔티티들이 공통으로 복구 동작을 사용할 수 있어 보입니다. User.restore()와의 연계도 자연스럽습니다.src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtHandler.java (1)
30-31: WITHDRAWN 클레임 처리 방식이 구 토큰까지 고려해 잘 정리되었습니다.
createClaims에서IS_WITHDRAWN클레임을 추가해 새 토큰에 탈퇴 여부가 포함되고,convert에서claims.get(IS_WITHDRAWN, Boolean.class)결과를withdrawn != null ? withdrawn : false로 기본값 처리해 예전 토큰에서도 NPE 없이false로 동작하도록 되어 있습니다.토큰 스키마 변경 시 필요한 backward compatibility 를 잘 충족하고 있어 보입니다.
Also applies to: 65-70, 91-97
src/main/java/com/gpt/geumpumtabackend/global/jwt/JwtUserClaim.java (1)
7-16: withdrawn 플래그 생성 로직이 삭제 여부와 일관되게 수정되었습니다.
create(User user)에서user.getDeletedAt() != null로 withdrawn 을 세팅하고, 별도 팩토리 메서드로 수동 생성도 명시적으로 처리해 JWT 쪽isWithdrawn()의미와 잘 맞습니다.src/main/java/com/gpt/geumpumtabackend/user/domain/User.java (1)
11-24: User 소프트 삭제/복구 흐름이 deletedAt·고유값 정책과 잘 맞습니다.
@SQLDelete로 삭제 시deleted_at타임스탬프를 찍고, 이메일/학교이메일/닉네임/학번에"deleted_"prefix 를 부여해 이후 동일 값으로 다른 계정 가입이 가능해집니다.restore(...)에서 전달받은 값으로 필드를 되살린 뒤super.restore()를 호출하여deletedAt을 null 로 초기화하므로, JWT 측에서 다시 활성 사용자로 인식할 수 있습니다.탈퇴 → 재로그인 → 복구까지의 도메인 정책을 엔티티 레벨에서 잘 반영한 구현으로 보입니다.
Also applies to: 90-96
🚀 1. 개요
📝 2. 주요 변경 사항
📸 3. 스크린샷 (API 테스트 결과)
Summary by CodeRabbit
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.