Skip to content

[feat] 다중 기기 세션 관리 및 토큰 폐기 강화 - #131

Merged
You-Hyuk merged 4 commits into
developfrom
feat/#130-multi-device-refresh-session
Sep 27, 2026
Merged

You-Hyuk merged 4 commits into
developfrom
feat/#130-multi-device-refresh-session

Conversation

@You-Hyuk

Copy link
Copy Markdown
Contributor

관련 이슈

Closes #130


변경 개요

Refresh Token(RT)이 사용자당 1개(RT:{userId})만 저장되어 여러 기기에서 동시에 로그인할 수 없었다. 새 기기에서 로그인하면 기존 기기의 RT를 덮어써서, 기존 기기는 30분 뒤 로그아웃됐다. 한 기기에서 로그아웃하면 모든 기기의 RT가 함께 삭제되는 문제도 있었다.

RT를 기기(세션) 단위로 저장하도록 바꾸고, 보안 리뷰에서 나온 토큰 폐기 관련 빈틈도 함께 막는다.

  • RT를 Bearer로 제출하면 인증이 통과하던 문제를 차단한다.
  • 탈취된 RT로 먼저 회전하는 공격을 재사용 감지로 막는다.

정책은 명세에서 먼저 확정했다: spec/policy/auth-policy.md "다중 기기 로그인 정책" (Cominggg/Specification@03edf79, @18a1a27).

변경사항

파일 변경 내용
auth/jwt/JwtProvider.java RT에 세션 식별자 jti, AT/RT에 typ claim(access/refresh) 추가. parseRefreshToken(typ·jti 검증 → userId·sessionId), isAccessToken 추가. getUserId는 쓰는 곳이 없어져 삭제
auth/jwt/RefreshTokenClaims.java 신규 record (userId, sessionId)
auth/jwt/JwtAuthenticationFilter.java typ=access 토큰만 인증
auth/repository/TokenRepository.java RT:{userId}:{sessionId} + 세션 목록 ZSET RT_SESSIONS:{userId}. 최대 5세션(초과 시 가장 오래된 세션 제거), Lua 스크립트로 원자적 회전·재사용 감지
auth/service/AuthService.java refresh: 해당 세션만 검증·교체하고 재사용이면 WARN 로그 후 401. logout: 현재 기기 세션만 삭제. withdraw: 전체 세션 삭제
auth/controller/AuthController.java /logout에서 RT 쿠키 수신, RT 쿠키의 secure를 app.cookie.secure 설정으로 변경
auth/oauth2/OAuth2SuccessHandler.java, auth/dev/DevAuthService.java 로그인마다 새 세션 저장

주요 구현 내용

세션 저장 구조

  • RT:{userId}:{sessionId} → 세션별 RT (TTL 7일)
  • RT_SESSIONS:{userId} → 세션 목록 ZSET (score = 생성 시각)
  • 로그인 시 만료된 세션을 목록에서 정리한 뒤, 5개를 넘으면 오래된 세션부터 제거한다.
    • 만료 여부는 score가 아니라 RT 키가 남아 있는지로 판단한다. refresh로 7일 넘게 연장된 세션을 만료로 잘못 판단하지 않기 위해서다.
    • RT를 회전할 때 세션 목록의 TTL도 함께 연장한다.

회전·재사용 감지 (Lua, 원자적)

  • 저장값과 일치하면 교체한다 → ROTATED
  • 저장값은 있는데 제출값과 다르면 이미 회전된 RT의 재사용으로 본다. 해당 세션만 폐기한다(DEL + ZREM) → REUSE_DETECTED
  • 저장값이 없으면 → SESSION_NOT_FOUND
  • 이전 RT를 몇 초간 허용하는 유예 구간은 두지 않는다. 재사용 감지와 충돌하기 때문이다.

토큰 종류 구분

  • 필터는 typ=access 토큰만 인증하고, refresh와 logout은 typ=refresh 토큰만 받는다.
  • 로그아웃하거나 세션이 제거된 RT는 서명이 유효해도 API 인증에 쓸 수 없다.

테스트

  • 로컬 실행 확인 — FE 세션이 이 브랜치로 BE를 띄워 Dev Login·새로고침·탭 2개 동시 refresh를 확인했다. 단 토큰 폐기 강화(1ac5d1f, 2bc7e03) 이전 상태였다. 최종 상태는 로컬에서 실행 확인하지 않았다.
  • 단위 테스트 추가/수정 — ./gradlew test 779건 통과
  • 예외 케이스 확인 — jti·typ 없는 기존 토큰, AT를 refresh에 제출, RT를 Bearer로 제출, 재사용 감지, 세션 없음, 다른 사용자 RT로 logout, 쿠키 없는 logout, 쿠키 secure on/off
  • 회전 Lua 스크립트를 로컬 Redis에서 EVAL로 직접 실행해 확인 — 일치 / 재사용(해당 세션만 폐기, 다른 세션 유지) / 세션 없음

리뷰어 참고사항

  • 배포 순서: [fix] refresh 동시 호출로 인한 간헐적 로그아웃 수정 Frontend#172(#171)를 반드시 먼저 배포해야 한다. 재사용 감지 때문에 같은 브라우저에서 refresh가 동시에 나가면 그 기기 세션이 폐기되는데, FE#172가 refresh를 순서대로 보내도록 직렬화한다.
  • 배포 직후 기존 로그인 사용자는 한 번 다시 로그인해야 한다. 기존 AT/RT에는 typ·jti가 없어서 거부된다. 기존 RT:{userId} 키는 옮기지 않고 TTL 7일 뒤 자연히 사라진다. 공지는 하지 않기로 했다.
  • API 요청·응답 형식과 에러 코드는 바뀌지 않았다. FE 코드 변경도 없다 (FE 세션이 확인함).
  • 커밋 단위는 각 커밋이 빌드되도록 나눴다. TokenRepository의 API를 바꾸면 호출부도 같은 커밋에서 바뀌어야 해서, 세션 저장과 호출부 변경은 한 커밋에 묶었다.

코드 리뷰

변경사항 요약

  • main 8개 파일(신규 RefreshTokenClaims 포함), 테스트 6개 파일
  • 사전 리뷰 게이트 결과
    • /be-review: 🔴 0건
    • security-reviewer 2회(세션 구조, 토큰 폐기 강화): 모두 critical 0 / warning 0
    • /security-review: 신뢰도 8 이상 취약점 없음

검토 결과

🟡 warning

  • TokenRepository.java: rotate()의 매개변수가 5개, saveSession()이 4개다. 프로젝트 규칙은 3개를 넘으면 DTO로 묶도록 한다.
    → 호출하는 곳이 1~2곳이고 기존 save(userId, token, ttl)과 같은 형태라서 유지했다. 한 곳에서만 쓸 record를 만들지 않기 위해서다.

🔵 suggestion

  • AuthService.java: 재사용을 감지하거나 탈퇴해도, 이미 발급된 다른 AT는 최대 30분 동안 유효하다.
    → 30분 AT 정책 범위라 받아들였다. 막으려면 요청마다 Redis 조회가 늘어난다.
  • OAuth2SuccessHandler.java, DevAuthService.java: 방금 발급한 RT를 다시 파싱해 sessionId를 얻는다.
    → 서명 검증이 한 번 더 드는 정도라 유지했다.
  • TokenRepository.java: 회전 Lua 스크립트가 서로 다른 hash slot의 키 2개를 함께 다룬다.
    → 지금은 Redis 한 대라 문제없다. Redis Cluster로 옮기면 키에 {userId} hash tag가 필요하다.
  • DevAuthService.java: 테스트가 없다.
    → 원래 없었고 local 프로필 전용이라 이번 범위에서 제외했다.

You-Hyuk and others added 4 commits September 27, 2026 20:31
- 새 로그인마다 UUID jti를 발급하고, 회전 시 같은 jti로 재발급하는 오버로드 추가
- getSessionId로 jti 조회 (jti 도입 이전 토큰은 null)
- 같은 초에 발급된 Refresh Token 값이 동일해지던 문제 해소

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- TokenRepository: RT:{userId}:{sessionId} + 세션 목록 ZSET(RT_SESSIONS:{userId})으로 변경, 사용자당 최대 5세션(초과 시 가장 오래된 세션 제거)
- 회전은 Lua 스크립트로 저장값 비교 후 교체해 동시 refresh 중 1건만 성공, 세션 목록 TTL도 함께 연장
- 로그인(OAuth2·Dev)마다 새 세션 저장 — 다른 기기의 Refresh Token을 덮어쓰지 않음
- refresh: 해당 세션만 검증·교체, jti 없는 기존 토큰은 REFRESH_TOKEN_INVALID(재로그인)
- logout: Refresh Token 쿠키로 현재 기기 세션만 삭제 (쿠키 없거나 무효·타 사용자 토큰이면 삭제 생략, 200 유지)
- withdraw: 모든 기기 세션 삭제

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AT에 typ=access, RT에 typ=refresh claim 추가, 인증 필터는 typ=access만 인증
  (로그아웃·세션 제거 후에도 서명이 유효한 RT가 Bearer로 통과하던 문제 차단)
- JwtProvider.parseRefreshToken: typ·jti를 검증해 userId·sessionId 반환 (getSessionId·getUserId 대체)
- 회전 Lua 스크립트: 저장값 불일치 시 이미 회전된 RT의 재사용으로 보고 해당 세션만 원자적으로 폐기
- AuthService: 재사용 감지 시 WARN 로그 후 REFRESH_TOKEN_INVALID

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AuthController가 secure(true)를 하드코딩해 로컬(http)에서 refresh 후 쿠키가 Secure로
재발급되던 문제를 OAuth2SuccessHandler와 같이 app.cookie.secure 설정을 따르도록 수정

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@You-Hyuk You-Hyuk added Feat ✨ 새 기능 추가 Bug 🐛 버그 수정 labels Sep 27, 2026
@You-Hyuk You-Hyuk self-assigned this Sep 27, 2026
@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4ffa24d0-ddfe-4b98-a1d1-716dcf63ef70

📥 Commits

Reviewing files that changed from the base of the PR and between de0c63e and 2bc7e03.

📒 Files selected for processing (14)
  • src/main/java/com/Coming/Backend/auth/controller/AuthController.java
  • src/main/java/com/Coming/Backend/auth/dev/DevAuthService.java
  • src/main/java/com/Coming/Backend/auth/jwt/JwtAuthenticationFilter.java
  • src/main/java/com/Coming/Backend/auth/jwt/JwtProvider.java
  • src/main/java/com/Coming/Backend/auth/jwt/RefreshTokenClaims.java
  • src/main/java/com/Coming/Backend/auth/oauth2/OAuth2SuccessHandler.java
  • src/main/java/com/Coming/Backend/auth/repository/TokenRepository.java
  • src/main/java/com/Coming/Backend/auth/service/AuthService.java
  • src/test/java/com/Coming/Backend/auth/controller/AuthControllerTest.java
  • src/test/java/com/Coming/Backend/auth/jwt/JwtAuthenticationFilterTest.java
  • src/test/java/com/Coming/Backend/auth/jwt/JwtProviderTest.java
  • src/test/java/com/Coming/Backend/auth/oauth2/OAuth2SuccessHandlerTest.java
  • src/test/java/com/Coming/Backend/auth/repository/TokenRepositoryTest.java
  • src/test/java/com/Coming/Backend/auth/service/AuthServiceTest.java

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.

@You-Hyuk
You-Hyuk merged commit d2e4b42 into develop Sep 27, 2026
3 checks passed
You-Hyuk added a commit to Cominggg/Specification that referenced this pull request Sep 27, 2026
BE 구현 완료(Cominggg/Backend#131)에 따라 "BE 반영 전까지 사용자당 세션 1개" 안내를 제거한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@You-Hyuk You-Hyuk mentioned this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug 🐛 버그 수정 Feat ✨ 새 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 다중 기기 세션 관리 및 토큰 폐기 강화

1 participant