Skip to content

feat: 중복 로그인 차단 정책 추가#78

Merged
kon28289 merged 2 commits intodevfrom
feat/block-multi-device-login
Mar 8, 2026
Merged

feat: 중복 로그인 차단 정책 추가#78
kon28289 merged 2 commits intodevfrom
feat/block-multi-device-login

Conversation

@kon28289
Copy link
Copy Markdown
Contributor

@kon28289 kon28289 commented Mar 8, 2026

🚀 1. 개요

  • 중복 로그인 차단 정책을 추가합니다.

📝 2. 주요 변경 사항

  • Optional<User> findByIdForUpdate(@Param("userId") Long userId);에 비관적 락을 추가해 issueTokenIfNoActiveSession 메서드 동시 호출을 차단합니다.
  • 중복 로그인 시 {state에서 복원된 redirectUri}?error=already_logged_in 경로로 리다이렉트가 진행됩니다/

📸 3. 스크린샷 (API 테스트 결과)

Summary by CodeRabbit

리팩토링

  • OAuth 로그인 프로세스
    • OAuth 로그인 시 세션 관리 로직을 개선했습니다. 이미 활성 세션이 있는 상태에서 재로그인을 시도하면 오류 안내를 받으며, 만료된 토큰은 자동으로 갱신됩니다.

@kon28289 kon28289 self-assigned this Mar 8, 2026
@kon28289 kon28289 requested a review from Juhye0k March 8, 2026 08:50
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 8, 2026

Walkthrough

OAuth 로그인 성공 핸들러가 JwtHandler 직접 사용에서 OAuthLoginPolicyService로 변경되었습니다. 새 서비스는 활성 세션 존재 여부를 검증하고 조건부로 토큰을 발급하며, 중복 로그인을 방지합니다.

Changes

Cohort / File(s) Summary
OAuth2 로그인 정책 서비스 도입
src/main/java/com/gpt/geumpumtabackend/global/oauth/service/OAuthLoginPolicyService.java
활성 세션 검증 및 조건부 토큰 발급 로직을 수행하는 새로운 서비스 추가. 사용자 존재 확인(FOR UPDATE), 기존 리프레시 토큰 확인, 만료된 토큰 삭제 후 새 토큰 쌍 발급.
OAuth2 인증 핸들러 통합
src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java
JwtHandler 의존성을 OAuthLoginPolicyService로 변경. 활성 세션 존재 시 "already_logged_in" 에러 파라미터와 함께 리다이렉트하는 로직 추가.
사용자 저장소 확장
src/main/java/com/gpt/geumpumtabackend/user/repository/UserRepository.java
비관적 쓰기 잠금(PESSIMISTIC_WRITE)을 적용한 findByIdForUpdate 메서드 추가. 동시성 제어 강화.
서비스 단위 테스트
src/test/java/com/gpt/geumpumtabackend/unit/oauth/service/OAuthLoginPolicyServiceTest.java
활성 세션 감지 시 토큰 발급 차단, 만료된 토큰 갱신, 사용자 미발견 시 예외 처리에 대한 3가지 테스트 케이스 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as OAuth2AuthenticationSuccessHandler
    participant Service as OAuthLoginPolicyService
    participant UserRepo as UserRepository
    participant TokenRepo as RefreshTokenRepository
    participant JwtHandler

    Client->>Handler: OAuth2 인증 성공
    Handler->>Service: issueTokenIfNoActiveSession(jwtUserClaim)
    Service->>UserRepo: findByIdForUpdate(userId)<br/>(비관적 잠금)
    UserRepo-->>Service: User (잠금 획득)
    Service->>TokenRepo: findByUserId(userId)
    TokenRepo-->>Service: RefreshToken (또는 null)
    
    alt 활성 세션 존재 (토큰 미만료)
        Service-->>Handler: Optional.empty()
        Handler-->>Client: 리다이렉트 (error=already_logged_in)
    else 활성 세션 없음 또는 만료
        Service->>TokenRepo: delete(oldToken)
        Service->>JwtHandler: issueToken(accessToken, refreshToken)
        JwtHandler-->>Service: Token
        Service-->>Handler: Optional.of(Token)
        Handler-->>Client: 리다이렉트 (accessToken, refreshToken)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

enhancement

Suggested reviewers

  • Juhye0k

Poem

🐰 새로운 정책으로 중복 로그인을 막고,
활성 세션을 확인하는 마법을 펼쳤네요!
잠금과 함께 안전하게, ✨
토큰은 필요할 때만 나누어주고,
사용자의 편의와 보안이 춤을 춘답니다! 🎭

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'feat: 중복 로그인 차단 정책 추가'는 변경사항의 핵심을 명확하게 설명하며, 코드 변경 사항과 일치합니다.
Description check ✅ Passed PR 설명이 요구되는 템플릿 구조(개요, 주요 변경 사항, 스크린샷)를 따르고 있으며, 주요 변경 사항을 명확하게 설명합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/block-multi-device-login

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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/com/gpt/geumpumtabackend/global/oauth/service/OAuthLoginPolicyService.java`:
- Around line 19-30: Add `@Transactional`(readOnly = true) at the class level of
OAuthLoginPolicyService to make read-only the default transaction behavior, and
keep the existing `@Transactional` on the write method issueTokenIfNoActiveSession
so it overrides the class-level readOnly setting for write operations; this
ensures future read-only methods follow the correct default while the
token-issuing method still executes a writable transaction.

In `@src/main/java/com/gpt/geumpumtabackend/user/repository/UserRepository.java`:
- Around line 32-34: The current findByIdForUpdate method locks rows for any
user ID but does not exclude soft-deleted users (deletedAt), allowing
masked/withdrawn users to obtain tokens; modify the method/query to filter out
soft-deleted records (e.g., add "AND u.deletedAt IS NULL" to the JPQL) or rely
on the BaseEntity soft-delete setup (ensure User extends BaseEntity annotated
with `@SQLDelete` and `@Where`(clause = "deleted_at IS NULL") so the
PESSIMISTIC_WRITE query respects soft deletes), and update the repository method
findByIdForUpdate to use the non-deleted constraint so masked users are not
returned.

In
`@src/test/java/com/gpt/geumpumtabackend/unit/oauth/service/OAuthLoginPolicyServiceTest.java`:
- Around line 25-26: The test class OAuthLoginPolicyServiceTest currently uses
`@ExtendWith`(MockitoExtension.class) which bypasses shared test setup; change the
class to extend BaseUnitTest instead (remove the explicit `@ExtendWith`
annotation) so it inherits common test configuration and helpers from
BaseUnitTest; ensure imports and any Mockito/AssertJ usage remain compatible
after switching the class signature to extend BaseUnitTest.
- Around line 44-101: Add a test in OAuthLoginPolicyServiceTest to cover the "no
existing refresh token" branch: mock userRepository.findByIdForUpdate(userId) to
return Optional.of(mock(User.class)) and
refreshTokenRepository.findByUserId(userId) to return Optional.empty(), then
call oAuthLoginPolicyService.issueTokenIfNoActiveSession(claim) and assert the
returned Optional contains the Token produced by jwtHandler.createTokens(claim);
also verify jwtHandler.createTokens(claim) was called and
refreshTokenRepository.deleteByUserId(userId) was never called. This ensures the
issueTokenIfNoActiveSession path for new logins is tested.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96bbe7a8-4fbc-48be-9198-200232bd5e9f

📥 Commits

Reviewing files that changed from the base of the PR and between 8a2f1fb and 0cc29d8.

📒 Files selected for processing (4)
  • src/main/java/com/gpt/geumpumtabackend/global/oauth/handler/OAuth2AuthenticationSuccessHandler.java
  • src/main/java/com/gpt/geumpumtabackend/global/oauth/service/OAuthLoginPolicyService.java
  • src/main/java/com/gpt/geumpumtabackend/user/repository/UserRepository.java
  • src/test/java/com/gpt/geumpumtabackend/unit/oauth/service/OAuthLoginPolicyServiceTest.java

Copy link
Copy Markdown
Contributor

@Juhye0k Juhye0k left a comment

Choose a reason for hiding this comment

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

고생하셨습니다

@kon28289 kon28289 merged commit 7013fd7 into dev Mar 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants