Conversation
- JwtTokenProvider: createSessionTokenPair() 추가, access/refresh 토큰이 동일한 sid 공유 - User: updateCurrentSessionid() 메서드 추가 - AuthService: 로그인/재발급 시 sid DB 저장, 로그아웃 시 sid 초기화 - AuthService.refresh(): refreshToken sid 검증으로 탈취된 기기의 재발급 시도 차단 (Refresh Token Rotation) - JwtAuthenticationFilter: sid 불일치 시 DuplicateSessionException 발생, sendJsonError 예외 타입별 에러코드 분리, isCommitted 방어 코드 추가 - AppErrorCode: DUPLICATE_LOGIN 에러코드 추가 - DuplicateSessionException: 신규 예외 클래스 추가 - GlobalExceptionHandler: 보안 예외(ExpiredToken, InvalidToken, DuplicateSession) 핸들러 추가 - CustomUserDetails: currentSessionid 필드 추가 - RefreshResponse: refreshToken 필드 추가 (@JsonIgnore) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Walkthrough세션 기반 토큰 페어(SessionTokenPair)를 도입해 로그인·구글 로그인·토큰 갱신 흐름에서 SID를 포함한 토큰을 발급·회전하며, 사용자 엔티티의 Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant AuthController
participant AuthService
participant JwtTokenProvider
participant UserRepo as User Repository
participant Database
rect rgba(100,150,200,0.5)
Note over Client,Database: Refresh token rotation 및 세션 검증 흐름
Client->>AuthController: POST /auth/refresh (refreshToken cookie)
AuthController->>AuthService: refresh(refreshToken)
AuthService->>JwtTokenProvider: getSidFromToken(refreshToken)
JwtTokenProvider-->>AuthService: sid
AuthService->>UserRepo: findById(userId)
UserRepo->>Database: SELECT user (currentSessionid)
Database-->>UserRepo: user
UserRepo-->>AuthService: User
alt sid == user.currentSessionid
AuthService->>JwtTokenProvider: createSessionTokenPair(userId, email, role)
JwtTokenProvider->>JwtTokenProvider: generate new sessionId (UUID)
JwtTokenProvider-->>AuthService: SessionTokenPair(access, refresh, sessionId)
AuthService->>UserRepo: updateCurrentSessionid(new sessionId)
UserRepo->>Database: UPDATE user.current_sessionid
Database-->>UserRepo: OK
AuthService-->>AuthController: RefreshResponse(accessToken, refreshToken)
AuthController->>Client: 200 OK + Set-Cookie(refreshToken)
else sid != user.currentSessionid
AuthService-->>AuthController: throw AppException(DUPLICATE_LOGIN)
AuthController-->>Client: 401 DUPLICATE_LOGIN
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/team/wego/wegobackend/common/security/CustomUserDetails.java (1)
30-33:⚠️ Potential issue | 🟠 Major
getAuthorities()가null을 반환하므로 Spring Security 인증 토큰에 권한이 설정되지 않습니다.생성자에서
this.authorities를 정상적으로 초기화하고 있지만(라인 26-27),getAuthorities()오버라이드 메서드가null을 반환합니다(라인 31-33).@Getter애너테이션이 있더라도 명시적 오버라이드 메서드가 우선 적용됩니다. JwtAuthenticationFilter의 라인 70에서userDetails.getAuthorities()가 호출되어UsernamePasswordAuthenticationToken생성자에 전달되므로, 인증 객체가 권한 정보를 갖지 않게 되어 권한 검사에 실패합니다.수정 제안
`@Override` public Collection<? extends GrantedAuthority> getAuthorities() { - return null; + return authorities; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/team/wego/wegobackend/common/security/CustomUserDetails.java` around lines 30 - 33, The getAuthorities() override in CustomUserDetails currently returns null, so authorities set in the constructor (this.authorities) are never exposed to Spring Security; update the getAuthorities() method in class CustomUserDetails to return the instance field (this.authorities) instead of null so JwtAuthenticationFilter's call to userDetails.getAuthorities() supplies the proper GrantedAuthority collection when creating the UsernamePasswordAuthenticationToken.src/main/java/team/wego/wegobackend/auth/presentation/AuthController.java (1)
100-114:⚠️ Potential issue | 🟠 Major로그아웃 시
@AuthenticationPrincipal의존으로 인해 이미 세션이 무효화된 사용자는 로그아웃 API를 호출할 수 없습니다.다른 기기에서 로그인하여 세션이 무효화된 사용자는
JwtAuthenticationFilter에서DuplicateSessionException이 발생하여 이 엔드포인트에 도달하지 못합니다. 현재 클라이언트 가이드 문서(docs/채팅서비스_클라이언트_가이드문서.md)에는 이 DUPLICATE_LOGIN 에러 처리 방법이 명시되어 있지 않습니다. 의도된 동작이라면 클라이언트 가이드 문서에 이 시나리오의 처리 방법(예: 로컬 상태 클리어, 토큰 폐기)이 명시되어야 합니다.
🧹 Nitpick comments (2)
src/main/java/team/wego/wegobackend/user/domain/User.java (1)
88-89: 컬럼 길이 제약 추가를 권장합니다.
current_sessionid에length제약이 없어 기본값 255가 적용됩니다. UUID 문자열(36자)만 저장하므로 명시적 길이를 지정하면 스키마 의도가 명확해집니다.♻️ 수정 제안
- `@Column`(name = "current_sessionid", nullable = true) + `@Column`(name = "current_sessionid", length = 36, nullable = true)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/team/wego/wegobackend/user/domain/User.java` around lines 88 - 89, The `@Column` for field currentSessionid lacks an explicit length so the schema defaults to 255; update the `@Column` annotation on the User.currentSessionid field to include a length suitable for a UUID (e.g., length = 36) so the database column size reflects the intended content and documents the intent.src/main/java/team/wego/wegobackend/auth/application/AuthService.java (1)
28-28:UserNotFoundException클래스가 두 패키지에 중복 존재합니다.
team.wego.wegobackend.auth.exception.UserNotFoundException과team.wego.wegobackend.user.exception.UserNotFoundException이 동일한 구현을 가지고 있습니다. 현재 codebase에서 auth 패키지 버전이 8곳, user 패키지 버전이 4곳에서 사용 중이므로, 하나로 통합하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/team/wego/wegobackend/auth/application/AuthService.java` at line 28, There are two identical UserNotFoundException classes; remove duplication by keeping a single canonical class (e.g., team.wego.wegobackend.user.exception.UserNotFoundException), delete the duplicate in team.wego.wegobackend.auth.exception, then update all imports and references that currently use team.wego.wegobackend.auth.exception.UserNotFoundException (including AuthService.java and the ~8 auth usages) to point to team.wego.wegobackend.user.exception.UserNotFoundException (including the 4 existing user usages); run a build to fix remaining import/compiler errors and adjust any package-private visibility if needed.
🤖 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/team/wego/wegobackend/auth/application/AuthService.java`:
- Around line 216-220: The comparison of refresh SID is unsafe and can throw
NullPointerException because jwtTokenProvider.getSidFromToken(refreshToken) may
return null and user.getCurrentSessionid() may also be null; replace the direct
equals check with a null-safe comparison (e.g., use Objects.equals(refreshSid,
user.getCurrentSessionid()) or explicitly handle nulls) and keep the existing
behavior of throwing new AppException(AppErrorCode.DUPLICATE_LOGIN) when the
SIDs do not match; refer to jwtTokenProvider.getSidFromToken, the local variable
refreshSid, user.getCurrentSessionid, and the
AppException(AppErrorCode.DUPLICATE_LOGIN) throw site.
In
`@src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java`:
- Around line 42-58: In GlobalExceptionHandler, remove the unnecessary
`@ExceptionHandler` methods handleDuplicateSession(DuplicateSessionException) and
handleInvalidToken(InvalidTokenException) since those exceptions are handled in
the JWT filter and never reach this controller advice; keep
handleExpiredToken(ExpiredTokenException) because AuthService.refresh() can
throw it. Delete the two methods (and any now-unused imports) and run a build to
ensure no remaining references; if any tests or code reference those handlers,
update callers to rely on the filter or existing AppException usage (e.g.,
AuthService's AppException(AppErrorCode.DUPLICATE_LOGIN)) instead.
In
`@src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java`:
- Around line 86-88: getSidFromToken currently returns null when the "sid" claim
is missing which can cause NPEs in JwtAuthenticationFilter; change
getSidFromToken in JwtTokenProvider to be null-safe by wrapping the claim
retrieval with Optional (e.g., return
Optional.ofNullable(getClaims(token).get("sid", String.class))) or explicitly
checking containsKey/get returning null and returning Optional.empty()/a safe
default, and update the caller JwtAuthenticationFilter to handle
Optional<String> (or the chosen safe return) instead of assuming a non-null sid.
In
`@src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java`:
- Around line 61-67: The comparison between tokenSid and currentSessionid can
throw NPE because getSidFromToken(jwt) may return null (and currentSessionid can
also be null); update the check in JwtAuthenticationFilter to use a null-safe
comparison (e.g., Objects.equals(tokenSid, currentSessionid)) instead of
tokenSid.equals(...), add the java.util.Objects import if missing, and keep the
existing behavior of throwing DuplicateSessionException when the two values are
not equal.
---
Outside diff comments:
In `@src/main/java/team/wego/wegobackend/common/security/CustomUserDetails.java`:
- Around line 30-33: The getAuthorities() override in CustomUserDetails
currently returns null, so authorities set in the constructor (this.authorities)
are never exposed to Spring Security; update the getAuthorities() method in
class CustomUserDetails to return the instance field (this.authorities) instead
of null so JwtAuthenticationFilter's call to userDetails.getAuthorities()
supplies the proper GrantedAuthority collection when creating the
UsernamePasswordAuthenticationToken.
---
Nitpick comments:
In `@src/main/java/team/wego/wegobackend/auth/application/AuthService.java`:
- Line 28: There are two identical UserNotFoundException classes; remove
duplication by keeping a single canonical class (e.g.,
team.wego.wegobackend.user.exception.UserNotFoundException), delete the
duplicate in team.wego.wegobackend.auth.exception, then update all imports and
references that currently use
team.wego.wegobackend.auth.exception.UserNotFoundException (including
AuthService.java and the ~8 auth usages) to point to
team.wego.wegobackend.user.exception.UserNotFoundException (including the 4
existing user usages); run a build to fix remaining import/compiler errors and
adjust any package-private visibility if needed.
In `@src/main/java/team/wego/wegobackend/user/domain/User.java`:
- Around line 88-89: The `@Column` for field currentSessionid lacks an explicit
length so the schema defaults to 255; update the `@Column` annotation on the
User.currentSessionid field to include a length suitable for a UUID (e.g.,
length = 36) so the database column size reflects the intended content and
documents the intent.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/main/java/team/wego/wegobackend/auth/application/AuthService.javasrc/main/java/team/wego/wegobackend/auth/application/dto/response/RefreshResponse.javasrc/main/java/team/wego/wegobackend/auth/presentation/AuthController.javasrc/main/java/team/wego/wegobackend/auth/presentation/AuthControllerDocs.javasrc/main/java/team/wego/wegobackend/common/exception/AppErrorCode.javasrc/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.javasrc/main/java/team/wego/wegobackend/common/security/CustomUserDetails.javasrc/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.javasrc/main/java/team/wego/wegobackend/common/security/exception/DuplicateSessionException.javasrc/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.javasrc/main/java/team/wego/wegobackend/user/domain/User.java
| @ExceptionHandler(ExpiredTokenException.class) | ||
| public ResponseEntity<ErrorResponse> handleExpiredToken(ExpiredTokenException ex, | ||
| HttpServletRequest request) { | ||
| return handleApp(new AppException(AppErrorCode.EXPIRED_TOKEN), request); | ||
| } | ||
|
|
||
| @ExceptionHandler(InvalidTokenException.class) | ||
| public ResponseEntity<ErrorResponse> handleInvalidToken(InvalidTokenException ex, | ||
| HttpServletRequest request) { | ||
| return handleApp(new AppException(AppErrorCode.INVALID_TOKEN), request); | ||
| } | ||
|
|
||
| @ExceptionHandler(DuplicateSessionException.class) | ||
| public ResponseEntity<ErrorResponse> handleDuplicateSession(DuplicateSessionException ex, | ||
| HttpServletRequest request) { | ||
| return handleApp(new AppException(AppErrorCode.DUPLICATE_LOGIN), request); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for DuplicateSessionException being thrown
rg -n "throw new DuplicateSessionException" --type=java -C3
echo "---"
# Search for DUPLICATE_LOGIN usage
rg -n "DUPLICATE_LOGIN" --type=java -C2
echo "---"
# Search for ExpiredTokenException throws in services
rg -n "throw new ExpiredTokenException" --type=java -C2
echo "---"
# Search for InvalidTokenException throws in services
rg -n "throw new InvalidTokenException" --type=java -C2Repository: WeGo-Together/WeGo_BackEnd
Length of output: 6594
필터에서 발생하는 예외는 @RestControllerAdvice에서 처리되지 않습니다.
JwtAuthenticationFilter(서블릿 필터)에서 발생하는 ExpiredTokenException, InvalidTokenException, DuplicateSessionException은 Spring MVC 디스패치 체인 밖이므로 이 핸들러에 도달하지 않습니다. 필터는 자체 예외 처리 로직(getErrorCodeFromException())으로 처리합니다.
다만 handleExpiredToken은 AuthService.refresh()에서 서비스 레이어에서 ExpiredTokenException이 발생할 수 있으므로 필요합니다. 반면 handleDuplicateSession은 실질적 사용처가 없습니다: DuplicateSessionException은 필터에서만 발생하고(이미 필터가 처리), 서비스 레이어(AuthService.java:219)에서는 AppException(AppErrorCode.DUPLICATE_LOGIN)을 사용하므로 이 핸들러를 거치지 않습니다. handleInvalidToken도 현재 모든 발생 지점이 필터나 JWT 제공자에 있어 GlobalExceptionHandler까지 도달하지 않습니다.
불필요한 핸들러(handleDuplicateSession, handleInvalidToken)는 제거하는 것이 좋습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java`
around lines 42 - 58, In GlobalExceptionHandler, remove the unnecessary
`@ExceptionHandler` methods handleDuplicateSession(DuplicateSessionException) and
handleInvalidToken(InvalidTokenException) since those exceptions are handled in
the JWT filter and never reach this controller advice; keep
handleExpiredToken(ExpiredTokenException) because AuthService.refresh() can
throw it. Delete the two methods (and any now-unused imports) and run a build to
ensure no remaining references; if any tests or code reference those handlers,
update callers to rely on the filter or existing AppException usage (e.g.,
AuthService's AppException(AppErrorCode.DUPLICATE_LOGIN)) instead.
| public String getSidFromToken(String token) { | ||
| return getClaims(token).get("sid", String.class); | ||
| } |
There was a problem hiding this comment.
sid 클레임 미존재 시 null 반환 가능성 고려 필요
getClaims(token).get("sid", String.class)는 해당 클레임이 없을 경우 null을 반환합니다. 이 기능 배포 이전에 발급된 토큰에는 sid 클레임이 없으므로, 호출자(JwtAuthenticationFilter)에서 NPE가 발생할 수 있습니다. 필터 쪽 리뷰 코멘트에서 이미 언급한 null-safe 처리로 대응 가능합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/team/wego/wegobackend/common/security/jwt/JwtTokenProvider.java`
around lines 86 - 88, getSidFromToken currently returns null when the "sid"
claim is missing which can cause NPEs in JwtAuthenticationFilter; change
getSidFromToken in JwtTokenProvider to be null-safe by wrapping the claim
retrieval with Optional (e.g., return
Optional.ofNullable(getClaims(token).get("sid", String.class))) or explicitly
checking containsKey/get returning null and returning Optional.empty()/a safe
default, and update the caller JwtAuthenticationFilter to handle
Optional<String> (or the chosen safe return) instead of assuming a non-null sid.
src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java
Show resolved
Hide resolved
- AuthService.refresh(): refreshSid null 체크 추가 - JwtAuthenticationFilter: tokenSid null 체크 추가 - GlobalExceptionHandler: dead code인 handleDuplicateSession 제거 (DuplicateSessionException은 필터 전용, 서비스 레이어는 AppException 사용) - docs: 동시 로그인 제한 클라이언트 가이드 문서 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
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/team/wego/wegobackend/auth/application/AuthService.java (1)
200-229:⚠️ Potential issue | 🟠 Major동시
refresh요청 시 TOCTOU 경합 조건이 발생할 수 있습니다.
@Transactional의 기본 격리 수준(READ_COMMITTED)에서, 동일한 refresh 토큰으로 두 요청이 동시에 들어오면 다음 흐름이 발생합니다:
- 두 트랜잭션 모두
user.getCurrentSessionid() = "sid-A"읽기 → 검증 통과- 트랜잭션 A:
sid-B발급 →updateCurrentSessionid("sid-B")→ 커밋- 트랜잭션 B:
sid-C발급 →updateCurrentSessionid("sid-C")→ 커밋- 결과: DB에는
sid-C만 유효 → 트랜잭션 A가 반환한 토큰은 즉시 무효화됨보안 취약점이라기보다는 UX 신뢰성 문제이지만, 정상 사용자가 별다른 이유 없이 재인증을 요구받을 수 있습니다.
UserRepository에 비관적 락(pessimistic lock) 쿼리를 추가하거나 엔티티에@Version낙관적 락을 적용하는 방식으로 해결할 수 있습니다.🔒 비관적 락 적용 예시
UserRepository에 락 쿼리 추가:+ `@Lock`(LockModeType.PESSIMISTIC_WRITE) + `@Query`("SELECT u FROM User u WHERE u.email = :email") + Optional<User> findByEmailWithLock(`@Param`("email") String email);
AuthService.refresh()에서 변경:- User user = userRepository.findByEmail(email) + User user = userRepository.findByEmailWithLock(email) .orElseThrow(UserNotFoundException::new);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/team/wego/wegobackend/auth/application/AuthService.java` around lines 200 - 229, The refresh flow in AuthService.refresh has a TOCTOU race when concurrent refreshes read and update user.currentSessionid; fix by serializing access to the User row: add a repository method (e.g., UserRepository.findByEmailForUpdate or findByEmailWithPessimisticLock) that loads the User with a PESSIMISTIC_WRITE lock, then in AuthService.refresh replace the current userRepository.findByEmail(...) call with that locked loader inside the `@Transactional` method so only one transaction can validate/update the session id at a time; alternatively, add an `@Version` field to the User entity and handle OptimisticLockException by retrying the refresh once, but prefer the pessimistic-lock repository method for immediate correctness.
♻️ Duplicate comments (2)
src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java (1)
61-67:⚠️ Potential issue | 🟡 Minor이전 리뷰의 null 체크 반영 — 단,
nulltokenSid 시 발생하는 예외 코드가 의미론적으로 부정확
tokenSid == null조건 추가로 NPE는 방지되었으나,sid클레임이 없는 토큰(레거시 토큰 포함)은 실질적으로 유효하지 않은 토큰임에도DUPLICATE_LOGIN에러 코드로 응답됩니다. 클라이언트가 이를 "다른 기기에서 로그인됨"으로 오해할 수 있습니다.
sid가 없는 경우는InvalidTokenException으로 분리하는 것을 권장합니다.🛠️ 개선 제안
- if (tokenSid == null || !tokenSid.equals(currentSessionid)) { - throw new DuplicateSessionException(); - } + if (tokenSid == null) { + throw new InvalidTokenException(); + } + if (!tokenSid.equals(currentSessionid)) { + throw new DuplicateSessionException(); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java` around lines 61 - 67, The current check in JwtAuthenticationFilter uses tokenSid == null to throw DuplicateSessionException which mislabels tokens missing the sid claim; change the logic so that when jwtTokenProvider.getSidFromToken(jwt) returns null you throw an InvalidTokenException (or create one if missing) to indicate a malformed/invalid token, and only throw DuplicateSessionException when tokenSid is non-null but does not equal userDetails.getCurrentSessionid(); update any related error codes/messages accordingly.src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java (1)
47-51:handleInvalidToken은 여전히 도달 불가능한 핸들러입니다.
InvalidTokenException은JwtAuthenticationFilter(서블릿 필터 레이어)에서만 던져집니다.DispatcherServlet앞단에서 발생한 예외는@ControllerAdvice로 처리할 수 없습니다. 이번 커밋에서handleDuplicateSession은 제거되었지만handleInvalidToken은 아직 남아 있습니다. 제거를 권장합니다.♻️ 제안: 도달 불가능한 핸들러 제거
- `@ExceptionHandler`(InvalidTokenException.class) - public ResponseEntity<ErrorResponse> handleInvalidToken(InvalidTokenException ex, - HttpServletRequest request) { - return handleApp(new AppException(AppErrorCode.INVALID_TOKEN), request); - } -
import team.wego.wegobackend.common.security.exception.InvalidTokenException;임포트도 함께 제거하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java` around lines 47 - 51, Remove the unreachable exception handler method handleInvalidToken(...) from GlobalExceptionHandler and also remove its unused import team.wego.wegobackend.common.security.exception.InvalidTokenException; since InvalidTokenException is thrown from JwtAuthenticationFilter (a servlet filter before DispatcherServlet) and cannot be handled by `@ControllerAdvice`, delete the handleInvalidToken method and the corresponding import to keep the class clean.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/동시_로그인_제한_클라이언트_가이드.md`:
- Around line 133-146: The logout function is using await but is not declared
async; update the function declaration for logout to be async (i.e., make
function logout() -> async function logout()) so the await on axios.post works
without a SyntaxError, and keep the existing try/catch/finally behavior that
calls clearLocalToken() and redirectToLogin().
- Around line 165-187: The response interceptor destructures error.response
without checking for its existence which crashes on network errors; update the
axios.interceptors.response.use error handler to first guard for undefined
error.response (e.g., if (!error.response) return Promise.reject(error) or
handle network errors appropriately) before accessing status/data, then proceed
with the existing logic that uses status, data, clearLocalToken(),
redirectToLogin(), and originalRequest.
- Around line 100-105: Add a language specifier to the fenced code block (or
convert it to an indented block) so the markdown linter MD040 is satisfied:
change the triple-backtick fence that surrounds the sequence starting with "기기A
사용 중" and ending with "→ 401 DUPLICATE_LOGIN 반환" to use ```text (or reformat as
an indented block) so the block is explicitly marked as plain text.
---
Outside diff comments:
In `@src/main/java/team/wego/wegobackend/auth/application/AuthService.java`:
- Around line 200-229: The refresh flow in AuthService.refresh has a TOCTOU race
when concurrent refreshes read and update user.currentSessionid; fix by
serializing access to the User row: add a repository method (e.g.,
UserRepository.findByEmailForUpdate or findByEmailWithPessimisticLock) that
loads the User with a PESSIMISTIC_WRITE lock, then in AuthService.refresh
replace the current userRepository.findByEmail(...) call with that locked loader
inside the `@Transactional` method so only one transaction can validate/update the
session id at a time; alternatively, add an `@Version` field to the User entity
and handle OptimisticLockException by retrying the refresh once, but prefer the
pessimistic-lock repository method for immediate correctness.
---
Duplicate comments:
In
`@src/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.java`:
- Around line 47-51: Remove the unreachable exception handler method
handleInvalidToken(...) from GlobalExceptionHandler and also remove its unused
import team.wego.wegobackend.common.security.exception.InvalidTokenException;
since InvalidTokenException is thrown from JwtAuthenticationFilter (a servlet
filter before DispatcherServlet) and cannot be handled by `@ControllerAdvice`,
delete the handleInvalidToken method and the corresponding import to keep the
class clean.
In
`@src/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java`:
- Around line 61-67: The current check in JwtAuthenticationFilter uses tokenSid
== null to throw DuplicateSessionException which mislabels tokens missing the
sid claim; change the logic so that when jwtTokenProvider.getSidFromToken(jwt)
returns null you throw an InvalidTokenException (or create one if missing) to
indicate a malformed/invalid token, and only throw DuplicateSessionException
when tokenSid is non-null but does not equal userDetails.getCurrentSessionid();
update any related error codes/messages accordingly.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
docs/동시_로그인_제한_클라이언트_가이드.mdsrc/main/java/team/wego/wegobackend/auth/application/AuthService.javasrc/main/java/team/wego/wegobackend/common/exception/GlobalExceptionHandler.javasrc/main/java/team/wego/wegobackend/common/security/JwtAuthenticationFilter.java
| ``` | ||
| 기기A 사용 중 | ||
| → 기기B에서 동일 계정으로 로그인 (기기A 세션 무효화) | ||
| → 기기A에서 refresh 시도 | ||
| → 401 DUPLICATE_LOGIN 반환 | ||
| ``` |
There was a problem hiding this comment.
코드 블록에 언어 지정자가 없습니다.
마크다운 린터(MD040)가 경고한 항목입니다. 일반 텍스트 시퀀스이므로 text를 지정하거나, 해당 내용을 들여쓰기 블록으로 표현하는 것이 좋습니다.
✏️ 수정 제안
-```
+```text
기기A 사용 중
→ 기기B에서 동일 계정으로 로그인 (기기A 세션 무효화)
→ 기기A에서 refresh 시도
→ 401 DUPLICATE_LOGIN 반환
-```
+```🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 100-100: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/동시_로그인_제한_클라이언트_가이드.md` around lines 100 - 105, Add a language specifier
to the fenced code block (or convert it to an indented block) so the markdown
linter MD040 is satisfied: change the triple-backtick fence that surrounds the
sequence starting with "기기A 사용 중" and ending with "→ 401 DUPLICATE_LOGIN 반환" to
use ```text (or reformat as an indented block) so the block is explicitly marked
as plain text.
| ```javascript | ||
| function logout() { | ||
| try { | ||
| await axios.post('/api/v1/auth/logout', null, { | ||
| headers: { Authorization: `Bearer ${accessToken}` } | ||
| }); | ||
| } catch (e) { | ||
| // 401이어도 로컬 상태는 반드시 초기화 | ||
| } finally { | ||
| clearLocalToken(); | ||
| redirectToLogin(); | ||
| } | ||
| } | ||
| ``` |
There was a problem hiding this comment.
logout 함수에 async 키워드가 누락되었습니다.
await를 사용하고 있으므로 함수를 async로 선언해야 합니다. 현재 코드를 그대로 복사하면 SyntaxError가 발생합니다.
✏️ 수정 제안
-function logout() {
+async function logout() {
try {
await axios.post('/api/v1/auth/logout', null, {📝 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.
| ```javascript | |
| function logout() { | |
| try { | |
| await axios.post('/api/v1/auth/logout', null, { | |
| headers: { Authorization: `Bearer ${accessToken}` } | |
| }); | |
| } catch (e) { | |
| // 401이어도 로컬 상태는 반드시 초기화 | |
| } finally { | |
| clearLocalToken(); | |
| redirectToLogin(); | |
| } | |
| } | |
| ``` | |
| async function logout() { | |
| try { | |
| await axios.post('/api/v1/auth/logout', null, { | |
| headers: { Authorization: `Bearer ${accessToken}` } | |
| }); | |
| } catch (e) { | |
| // 401이어도 로컬 상태는 반드시 초기화 | |
| } finally { | |
| clearLocalToken(); | |
| redirectToLogin(); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/동시_로그인_제한_클라이언트_가이드.md` around lines 133 - 146, The logout function is
using await but is not declared async; update the function declaration for
logout to be async (i.e., make function logout() -> async function logout()) so
the await on axios.post works without a SyntaxError, and keep the existing
try/catch/finally behavior that calls clearLocalToken() and redirectToLogin().
| axios.interceptors.response.use( | ||
| (response) => response, | ||
| async (error) => { | ||
| const { status, data } = error.response; | ||
| const originalRequest = error.config; | ||
|
|
||
| if (status !== 401) return Promise.reject(error); | ||
|
|
||
| const errorCode = data?.errorCode; | ||
|
|
||
| // 재시도 없이 로그인으로 보내야 하는 케이스 | ||
| if ( | ||
| errorCode === 'DUPLICATE_LOGIN' || | ||
| errorCode === 'INVALID_TOKEN' || | ||
| errorCode === 'NOT_FOUND_TOKEN' | ||
| ) { | ||
| if (errorCode === 'DUPLICATE_LOGIN') { | ||
| alert('다른 기기에서 로그인되었습니다.'); | ||
| } | ||
| clearLocalToken(); | ||
| redirectToLogin(); | ||
| return Promise.reject(error); | ||
| } |
There was a problem hiding this comment.
error.response가 undefined일 때 인터셉터가 크래시될 수 있습니다.
네트워크 오류(타임아웃, 연결 거부 등)의 경우 error.response가 undefined이므로 구조 분해 할당 시 TypeError가 발생합니다.
✏️ 수정 제안
- const { status, data } = error.response;
- const originalRequest = error.config;
-
- if (status !== 401) return Promise.reject(error);
+ const originalRequest = error.config;
+
+ if (!error.response) return Promise.reject(error);
+ const { status, data } = error.response;
+
+ if (status !== 401) return Promise.reject(error);📝 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.
| axios.interceptors.response.use( | |
| (response) => response, | |
| async (error) => { | |
| const { status, data } = error.response; | |
| const originalRequest = error.config; | |
| if (status !== 401) return Promise.reject(error); | |
| const errorCode = data?.errorCode; | |
| // 재시도 없이 로그인으로 보내야 하는 케이스 | |
| if ( | |
| errorCode === 'DUPLICATE_LOGIN' || | |
| errorCode === 'INVALID_TOKEN' || | |
| errorCode === 'NOT_FOUND_TOKEN' | |
| ) { | |
| if (errorCode === 'DUPLICATE_LOGIN') { | |
| alert('다른 기기에서 로그인되었습니다.'); | |
| } | |
| clearLocalToken(); | |
| redirectToLogin(); | |
| return Promise.reject(error); | |
| } | |
| axios.interceptors.response.use( | |
| (response) => response, | |
| async (error) => { | |
| const originalRequest = error.config; | |
| if (!error.response) return Promise.reject(error); | |
| const { status, data } = error.response; | |
| if (status !== 401) return Promise.reject(error); | |
| const errorCode = data?.errorCode; | |
| // 재시도 없이 로그인으로 보내야 하는 케이스 | |
| if ( | |
| errorCode === 'DUPLICATE_LOGIN' || | |
| errorCode === 'INVALID_TOKEN' || | |
| errorCode === 'NOT_FOUND_TOKEN' | |
| ) { | |
| if (errorCode === 'DUPLICATE_LOGIN') { | |
| alert('다른 기기에서 로그인되었습니다.'); | |
| } | |
| clearLocalToken(); | |
| redirectToLogin(); | |
| return Promise.reject(error); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/동시_로그인_제한_클라이언트_가이드.md` around lines 165 - 187, The response interceptor
destructures error.response without checking for its existence which crashes on
network errors; update the axios.interceptors.response.use error handler to
first guard for undefined error.response (e.g., if (!error.response) return
Promise.reject(error) or handle network errors appropriately) before accessing
status/data, then proceed with the existing logic that uses status, data,
clearLocalToken(), redirectToLogin(), and originalRequest.
📝 Pull Request
📌 PR 종류
해당하는 항목에 체크해주세요.
✨ 변경 내용
핵심 설계: 공유 Session ID
Access Token과 Refresh Token이 로그인 시 동일한
sid(session ID)를 공유합니다.신규 로그인 시 DB의
current_sessionid가 갱신되어 이전 기기의 세션이 즉시 무효화됩니다.JwtTokenProvider:createSessionTokenPair()추가 — 두 토큰이 동일한 UUID sid를 공유User:updateCurrentSessionid()메서드 추가CustomUserDetails:currentSessionid필드 추가 — 로그인 시 DB에서 로드JwtAuthenticationFilter: 매 요청마다 JWT의sid와 DB의current_sessionid비교, 불일치 시DuplicateSessionException발생AuthService.login() / googleLogin(): 로그인 시 공유 sid를 DB에 저장AuthService.refresh(): Refresh Token의 sid를 DB와 비교하여 탈취된 기기의 재발급 시도 차단, 검증 통과 시 새 sid 발급 및 DB 갱신 (Refresh Token Rotation 적용)AuthService.logout(): 로그아웃 시 DBcurrent_sessionid를 null로 초기화AuthController.logout():@AuthenticationPrincipal추가, 서비스 로그아웃 호출AuthController.refresh(): 재발급된 Refresh Token을 새 HttpOnly 쿠키로 세팅DuplicateSessionException: 중복 로그인 전용 예외 클래스 추가AppErrorCode:DUPLICATE_LOGIN에러 코드 추가GlobalExceptionHandler:ExpiredTokenException,InvalidTokenException,DuplicateSessionException핸들러 추가 — 서비스 레이어에서 발생 시 500 대신 적절한 401 응답 반환JwtAuthenticationFilter.sendJsonError(): 예외 타입별 에러 코드 분리,response.isCommitted()방어 코드 추가docs/동시_로그인_제한_클라이언트_가이드.md: 프론트엔드 적용 가이드 문서 추가🔍 관련 이슈
해당 PR이 해결하는 이슈가 있다면 연결해주세요.
예:
Closes #12,Fixes #5🧪 테스트
변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.
DUPLICATE_LOGIN응답 확인DUPLICATE_LOGIN응답 확인DUPLICATE_LOGIN응답 확인🚨 확인해야 할 사항 (Checklist)
PR을 제출하기 전에 아래 항목들을 확인해주세요.
🙋 기타 참고 사항
프론트엔드 적용 필요
이번 변경으로 프론트엔드에서 반드시 대응이 필요한 항목이 있습니다.
상세 내용은 Wiki에 작성해두었습니다.
DUPLICATE_LOGIN(401) — refresh 재시도 없이 로그인 화면으로 이동/auth/logoutAuthorization헤더 필수로 변경/auth/refresh보안 개선 포인트
Summary by CodeRabbit
새로운 기능
버그 수정
보안
문서