2025-10-22 20:13:56 +09:00

148 lines
5.3 KiB
Plaintext

@startuml user-로그인
!theme mono
title User Service - 로그인 내부 시퀀스 (UFR-USER-020)
actor Client
participant "UserController" as Controller <<API Layer>>
participant "UserService" as Service <<Business Layer>>
participant "AuthenticationService" as AuthService <<Business Layer>>
participant "UserRepository" as UserRepo <<Data Layer>>
participant "PasswordEncoder" as PwdEncoder <<Utility>>
participant "JwtTokenProvider" as JwtProvider <<Utility>>
participant "Redis\nCache" as Redis <<E>>
participant "User DB\n(PostgreSQL)" as UserDB <<E>>
note over Controller, UserDB
**UFR-USER-020: 로그인**
- 입력: 전화번호, 비밀번호
- 비밀번호 검증 (bcrypt compare)
- JWT 토큰 발급
- 세션 저장 (Redis)
- 최종 로그인 시각 업데이트
end note
Client -> Controller: POST /api/users/login\n(LoginRequest DTO)
activate Controller
Controller -> Controller: @Valid 어노테이션 검증\n(필수 필드 확인)
Controller -> AuthService: authenticate(phoneNumber, password)
activate AuthService
== 1단계: 사용자 조회 ==
AuthService -> Service: findByPhoneNumber(phoneNumber)
activate Service
Service -> UserRepo: findByPhoneNumber(phoneNumber)
activate UserRepo
UserRepo -> UserDB: 전화번호로 사용자 조회\n(사용자ID, 비밀번호해시, 역할,\n이름, 이메일 조회)
activate UserDB
UserDB --> UserRepo: 사용자 정보 또는 NULL
deactivate UserDB
UserRepo --> Service: Optional<User>
deactivate UserRepo
Service --> AuthService: Optional<User>
deactivate Service
alt 사용자 없음
AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요")
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"error": "전화번호 또는 비밀번호를\n확인해주세요"}
deactivate AuthService
deactivate Controller
else 사용자 존재
== 2단계: 비밀번호 검증 ==
AuthService -> PwdEncoder: matches(rawPassword, passwordHash)
activate PwdEncoder
PwdEncoder -> PwdEncoder: bcrypt compare\n(입력 비밀번호 vs 저장된 해시)
PwdEncoder --> AuthService: boolean (일치 여부)
deactivate PwdEncoder
alt 비밀번호 불일치
AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요")
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"error": "전화번호 또는 비밀번호를\n확인해주세요"}
deactivate AuthService
deactivate Controller
else 비밀번호 일치
== 3단계: JWT 토큰 생성 ==
AuthService -> JwtProvider: generateToken(userId, role)
activate JwtProvider
JwtProvider -> JwtProvider: JWT 토큰 생성\n(Claims: userId, role=OWNER,\nexp=7일)
JwtProvider --> AuthService: JWT 토큰
deactivate JwtProvider
== 4단계: 세션 저장 ==
AuthService -> Redis: SET user:session:{token}\n(userId, role, TTL 7일)
activate Redis
Redis --> AuthService: 세션 저장 완료
deactivate Redis
== 5단계: 최종 로그인 시각 업데이트 (비동기) ==
AuthService ->> Service: updateLastLoginAt(userId)
activate Service
note right of Service
**비동기 처리**
- @Async 어노테이션 사용
- 로그인 응답 지연 방지
- 별도 스레드풀에서 실행
end note
Service ->> UserRepo: updateLastLoginAt(userId)
activate UserRepo
UserRepo ->> UserDB: 최종 로그인 시각 업데이트\n(현재 시각으로 갱신)
activate UserDB
UserDB -->> UserRepo: 업데이트 완료
deactivate UserDB
UserRepo -->> Service: void
deactivate UserRepo
Service -->> AuthService: void (비동기 완료)
deactivate Service
note over AuthService, Service
**비동기 화살표 설명**
- `->>`: 비동기 호출 (호출 후 즉시 반환)
- `-->>`: 비동기 응답 (별도 스레드에서 완료)
end note
== 6단계: 응답 반환 ==
AuthService -> AuthService: 응답 DTO 생성\n(LoginResponse)
AuthService --> Controller: LoginResponse\n(token, userId, userName,\nrole, email)
deactivate AuthService
Controller --> Client: 200 OK\n{"token": "jwt_token",\n"userId": 123,\n"userName": "홍길동",\n"role": "OWNER",\n"email": "hong@example.com"}
deactivate Controller
end
end
note over Controller, UserDB
**보안 처리**
- 비밀번호: bcrypt compare (원본 노출 안 됨)
- 에러 메시지: 전화번호/비밀번호 구분 없이 동일 메시지 반환 (Timing Attack 방어)
- JWT 토큰: 7일 만료, 서버 세션과 동기화
**보안 강화 (향후 구현)**
- Rate Limiting: IP당 5분에 5회 로그인 실패 시 임시 차단 (15분)
- Account Lockout: 동일 계정 10회 실패 시 계정 잠금 (관리자 해제)
- MFA: 2단계 인증 추가 (SMS/TOTP)
- Anomaly Detection: 비정상 로그인 패턴 감지 (지역, 디바이스 변경)
**성능 최적화**
- 최종 로그인 시각 업데이트: 비동기 처리 (@Async)
- 평균 응답 시간: 0.5초 이내
- P95 응답 시간: 1.0초 이내
- Redis 세션 조회: 0.1초 이내
**에러 코드**
- AUTH_001: 인증 실패 (전화번호 또는 비밀번호 불일치)
end note
@enduml