인증/인가 서비스 - JWT 발급, 토큰 검증, 리프레시 토큰 관리
| 항목 | 내용 |
|---|---|
| 포트 | 8086 |
| 데이터베이스 | auth_db (PostgreSQL) |
| 주요 역할 | 인증 정보 관리, JWT 토큰 발급/검증 |
Auth Server가 인증 정보를 직접 보유 (User Service에서 Feign 동기 호출)
┌─────────────────────────────────────────────────────────────────┐
│ 인증 아키텍처 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User Service (프로필) Auth Server (인증) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ - name │ │ - email │ │
│ │ - phoneNumber │ userId │ - password │ │
│ │ - birthDate │ ◀────────▶│ - role │ │
│ │ - status │ (참조) │ - status │ │
│ └─────────────────┘ │ - loginAttempts │ │
│ │ │ - lockedUntil │ │
│ │ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ user_db auth_db │
│ │
└─────────────────────────────────────────────────────────────────┘
분리 이유:
- 인증 독립성: User Service 장애 시에도 로그인 가능
- 빠른 응답: 로그인 시 Auth Server만 조회
- 보안: 비밀번호는 Auth Server에서만 관리
- 확장성: 인증 방식 변경이 User Service에 영향 없음
- Access Token: 짧은 만료 시간 (30분), 요청마다 검증
- Refresh Token: 긴 만료 시간 (7일), Access Token 갱신용
- 토큰 구조: Header.Payload.Signature
┌─────────────────────────────────────────────────────────────┐
│ JWT 토큰 흐름 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Client Gateway Auth Server │
│ │ │ │ │
│ │ 1. 로그인 요청 │ │ │
│ │ ─────────────────────────────────────────> │ │
│ │ │ │ │
│ │ 2. Access + Refresh Token 발급 │ │
│ │ <───────────────────────────────────────── │ │
│ │ │ │ │
│ │ 3. API 요청 (Bearer Token) │ │
│ │ ──────────────────> │ │ │
│ │ │ 4. 토큰 검증 │ │
│ │ │ ──────────────────> │ │
│ │ │ 5. 검증 결과 │ │
│ │ │ <────────────────── │ │
│ │ │ │ │
│ │ │ 6. X-User-Id 헤더 주입 │
│ │ │ ─────────> 서비스들 │
│ │ │ │ │
└─────────────────────────────────────────────────────────────┘
- 최대 시도 횟수: 5회
- 잠금 시간: 30분
- 자동 해제: 잠금 시간 경과 시
- 리프레시 토큰 사용 시 새 토큰 발급
- 이전 토큰 무효화 (토큰 탈취 방지)
domain/
├── auth/ # 인증 사용자 도메인 (8개)
│ └── domain/
│ ├── exception/
│ │ ├── AuthErrorCode.java # 인증 에러 코드 (AUTH_xxx)
│ │ └── AuthException.java # 인증 예외
│ └── model/
│ ├── AuthUser.java # Aggregate Root (잠금 정책)
│ ├── AuthUserStatus.java # ACTIVE/LOCKED/DISABLED
│ ├── UserRole.java # USER/ADMIN
│ └── vo/
│ ├── AuthUserId.java # AUT-xxxxxxxx
│ ├── Email.java # 이메일 (검증, 마스킹)
│ └── Password.java # 비밀번호 (정책 검증)
│
├── token/ # 토큰 도메인 (4개)
│ └── domain/
│ ├── exception/
│ │ ├── TokenErrorCode.java # 토큰 에러 코드 (TKN_xxx)
│ │ └── TokenException.java # 토큰 예외
│ └── model/
│ ├── RefreshToken.java # 리프레시 토큰 관리
│ └── vo/
│ └── RefreshTokenId.java # RTK-xxxxxxxx
│
└── history/ # 로그인 이력 도메인 (4개)
└── domain/
├── exception/
│ ├── HistoryErrorCode.java # 이력 에러 코드 (LGH_xxx)
│ └── HistoryException.java # 이력 예외
└── model/
├── LoginHistory.java # Append-only 이력
└── vo/
└── LoginHistoryId.java # LGH-xxxxxxxx
| 도메인 | 책임 | 특성 |
|---|---|---|
| auth | 인증 사용자 관리, 잠금 정책 | 상태 변경 가능, Soft Delete |
| token | 리프레시 토큰 생명주기 | revoke만 가능, Stateless Access Token은 제외 |
| history | 로그인 시도 기록 | Append-only, 감사/보안 목적 |
┌─────────────────────────────────────────────────────────────┐
│ AuthUser │
├─────────────────────────────────────────────────────────────┤
│ 【핵심 필드】 │
│ authUserId: AuthUserId (PK, AUT-xxxxxxxx) │
│ userId: String (User Service의 USR-xxx 참조) │
│ email: Email (로그인 ID) │
│ password: Password (BCrypt 암호화) │
│ role: UserRole (USER/ADMIN) │
│ status: AuthUserStatus (ACTIVE/LOCKED/DISABLED) │
│ failedLoginAttempts: int (로그인 실패 횟수) │
│ lockedUntil: LocalDateTime (잠금 해제 시간) │
│ lastLoginAt: LocalDateTime │
├─────────────────────────────────────────────────────────────┤
│ 【감사 필드 - BaseEntity】 │
│ createdAt, updatedAt, createdBy, updatedBy │
│ deletedAt, deletedBy, isDeleted (Soft Delete) │
├─────────────────────────────────────────────────────────────┤
│ 【비즈니스 메서드】 │
│ + canLogin(): boolean // 로그인 가능 여부 (잠금 시간 포함)
│ + isLocked(): boolean // 실제 잠금 상태 (시간 경과 확인)
│ + getRemainingLockMinutes(): long │
│ + changePassword(Password): void │
│ + recordLoginSuccess(): void // 실패 횟수 초기화, 잠금 해제 │
│ + recordLoginFailure(): void // 실패 횟수 증가, 잠금 처리 │
│ + recordLoginFailure(maxAttempts, lockMinutes): void │
│ + unlock(): void // 수동 잠금 해제 │
│ + disable(): void // 계정 비활성화 │
│ + enable(): void // 계정 활성화 │
│ + changeRole(UserRole): void │
├─────────────────────────────────────────────────────────────┤
│ 【상수】 │
│ DEFAULT_MAX_ATTEMPTS = 5 // 기본 최대 시도 횟수 │
│ DEFAULT_LOCK_MINUTES = 30 // 기본 잠금 시간 (분) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ RefreshToken │
├─────────────────────────────────────────────────────────────┤
│ 【핵심 필드】 │
│ refreshTokenId: RefreshTokenId (RTK-xxxxxxxx) │
│ userId: String (토큰 소유자) │
│ token: String (JWT 토큰 값) │
│ expiresAt: LocalDateTime │
│ isRevoked: boolean (폐기 여부) │
│ deviceInfo: String (User-Agent) │
│ ipAddress: String (접속 IP) │
│ createdAt: LocalDateTime │
├─────────────────────────────────────────────────────────────┤
│ 【비즈니스 메서드】 │
│ + isExpired(): boolean │
│ + isValid(): boolean // !revoked && !expired │
│ + validateForUse(): void // 사용 전 검증, 예외 발생 │
│ + revoke(): void // 토큰 폐기 │
│ + matchesContext(deviceInfo, ip): boolean │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ LoginHistory │
├─────────────────────────────────────────────────────────────┤
│ 【핵심 필드】 │
│ loginHistoryId: LoginHistoryId (LGH-xxxxxxxx) │
│ userId: String (없을 수 있음) │
│ email: String (로그인 시도 이메일) │
│ loginAt: LocalDateTime │
│ ipAddress: String │
│ userAgent: String │
│ success: boolean │
│ failReason: String (실패 시: INVALID_PASSWORD 등) │
├─────────────────────────────────────────────────────────────┤
│ 【팩토리 메서드】 │
│ + success(userId, email, ip, userAgent): LoginHistory │
│ + failure(userId, email, ip, userAgent, reason): LoginHistory│
├─────────────────────────────────────────────────────────────┤
│ ※ Append-only: INSERT만 허용, UPDATE/DELETE 금지 │
└─────────────────────────────────────────────────────────────┘
public enum UserRole {
USER("일반 사용자", level=1),
ADMIN("관리자", level=100);
public boolean isAdmin();
public boolean hasAuthority(UserRole role); // ADMIN은 USER 권한 포함
public Set<UserRole> getIncludedRoles();
public String toAuthority(); // "ROLE_USER", "ROLE_ADMIN"
}public enum AuthUserStatus {
ACTIVE("정상", canAuthenticate=true),
LOCKED("잠금", canAuthenticate=false), // 일시적 (시간 경과 시 해제)
DISABLED("비활성화", canAuthenticate=false); // 영구적 (관리자만 해제)
public boolean canTransitionTo(AuthUserStatus target);
public Set<AuthUserStatus> getAllowedTransitions();
}상태 전이 규칙:
ACTIVE → LOCKED (로그인 실패), DISABLED (관리자)
LOCKED → ACTIVE (시간 경과/수동 해제), DISABLED (관리자)
DISABLED → ACTIVE (관리자)
public record Password(String encodedValue) {
// 평문 비밀번호 정책 검증 (Application Layer에서 호출)
public static void validateRawPassword(String raw);
public static boolean isValidRawPassword(String raw);
// 정책: 8자 이상, 영문자, 숫자, 특수문자(@$!%*?&) 포함
@Override
public String toString() { return "[PROTECTED]"; } // 보안
}public enum AuthErrorCode implements ErrorCode {
// 인증 실패 (401)
INVALID_CREDENTIALS("AUTH_001", "이메일 또는 비밀번호가 올바르지 않습니다", 401),
PASSWORD_MISMATCH("AUTH_002", "비밀번호가 일치하지 않습니다", 401),
// 토큰 (401)
INVALID_TOKEN("AUTH_010", "유효하지 않은 토큰입니다", 401),
TOKEN_EXPIRED("AUTH_011", "토큰이 만료되었습니다", 401),
TOKEN_REVOKED("AUTH_012", "폐기된 토큰입니다", 401),
// 계정 상태 (403)
ACCOUNT_LOCKED("AUTH_020", "계정이 잠겨있습니다", 403),
ACCOUNT_DISABLED("AUTH_021", "비활성화된 계정입니다", 403),
// 조회 (404)
AUTH_USER_NOT_FOUND("AUTH_025", "인증 사용자를 찾을 수 없습니다", 404),
// 유효성 (400)
INVALID_PASSWORD_FORMAT("AUTH_031", "비밀번호 정책 위반", 400);
}public class AuthException extends BusinessException {
public static AuthException invalidCredentials();
public static AuthException accountLocked(long remainingMinutes);
public static AuthException tokenExpired();
public static AuthException tokenRevoked();
public static AuthException authUserNotFound(String identifier);
// ...
}POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePassword123!"
}처리 흐름:
- AuthUser 조회 (email)
authUser.canLogin()확인 (잠금/비활성화 체크)- 비밀번호 검증
- 성공 시:
authUser.recordLoginSuccess(), 토큰 발급 - 실패 시:
authUser.recordLoginFailure(), LoginHistory 기록
Response (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 1800,
"user": {
"userId": "USR-a1b2c3d4",
"email": "user@example.com",
"role": "USER"
}
}실패 응답:
- 401:
INVALID_CREDENTIALS(이메일/비밀번호 오류) - 403:
ACCOUNT_LOCKED(계정 잠금, 남은 시간 포함) - 403:
ACCOUNT_DISABLED(계정 비활성화)
POST /api/v1/auth/refresh
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}처리 흐름:
- RefreshToken 조회
refreshToken.validateForUse()(만료/폐기 검증)- 기존 토큰 폐기:
refreshToken.revoke() - 새 Access + Refresh Token 발급
POST /api/v1/auth/validate
Content-Type: application/json
{
"accessToken": "eyJhbGciOiJIUzI1NiIs..."
}Response (200 OK)
{
"valid": true,
"userId": "USR-a1b2c3d4",
"email": "user@example.com",
"role": "USER",
"expiresAt": "2024-01-15T11:00:00"
}POST /api/v1/auth/logout
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}처리: refreshToken.revoke() 호출
POST /api/v1/auth/register
Content-Type: application/json
{
"userId": "USR-a1b2c3d4",
"email": "user@example.com",
"password": "SecurePassword123!"
}처리 흐름:
Password.validateRawPassword(password)정책 검증- 비밀번호 암호화 (BCrypt)
- AuthUser 생성 및 저장
com.jun_bank.auth_server
├── AuthServerApplication.java
├── global/ # 전역 설정 레이어
│ ├── config/
│ │ ├── JpaConfig.java
│ │ ├── QueryDslConfig.java
│ │ ├── KafkaProducerConfig.java
│ │ ├── KafkaConsumerConfig.java
│ │ ├── SecurityConfig.java # PasswordEncoder, 인증 제외 경로
│ │ ├── FeignConfig.java
│ │ ├── SwaggerConfig.java
│ │ └── AsyncConfig.java
│ ├── infrastructure/
│ │ ├── entity/
│ │ │ └── BaseEntity.java
│ │ └── jpa/
│ │ └── AuditorAwareImpl.java
│ ├── security/
│ │ └── SecurityContextUtil.java
│ ├── feign/
│ │ ├── FeignErrorDecoder.java
│ │ └── FeignRequestInterceptor.java
│ └── aop/
│ └── LoggingAspect.java
└── domain/
├── auth/ # 인증 사용자 Bounded Context ★
│ ├── domain/ # 순수 도메인 ✅
│ │ ├── exception/
│ │ │ ├── AuthErrorCode.java # AUTH_xxx 에러 코드
│ │ │ └── AuthException.java
│ │ └── model/
│ │ ├── AuthUser.java # Aggregate Root
│ │ ├── UserRole.java
│ │ ├── AuthUserStatus.java
│ │ └── vo/
│ │ ├── AuthUserId.java
│ │ ├── Email.java
│ │ └── Password.java
│ ├── application/ # 유스케이스 (TODO)
│ ├── infrastructure/ # Adapter Out (TODO)
│ └── presentation/ # Adapter In (TODO)
│
├── token/ # 토큰 Bounded Context ★
│ ├── domain/ # 순수 도메인 ✅
│ │ ├── exception/
│ │ │ ├── TokenErrorCode.java # TKN_xxx 에러 코드
│ │ │ └── TokenException.java
│ │ └── model/
│ │ ├── RefreshToken.java # Aggregate Root
│ │ └── vo/
│ │ └── RefreshTokenId.java
│ ├── application/ # (TODO)
│ ├── infrastructure/
│ │ └── jwt/ # JWT Provider
│ │ ├── JwtTokenProvider.java
│ │ └── JwtProperties.java
│ └── presentation/ # (TODO)
│
└── history/ # 로그인 이력 Bounded Context ★
├── domain/ # 순수 도메인 ✅
│ ├── exception/
│ │ ├── HistoryErrorCode.java # LGH_xxx 에러 코드
│ │ └── HistoryException.java
│ └── model/
│ ├── LoginHistory.java # Aggregate Root (Append-only)
│ └── vo/
│ └── LoginHistoryId.java
├── application/ # (TODO)
├── infrastructure/ # (TODO)
└── presentation/ # (TODO)
{
"sub": "USR-a1b2c3d4",
"email": "user@example.com",
"role": "USER",
"iat": 1705302600,
"exp": 1705304400,
"iss": "jun-bank-auth-server"
}{
"sub": "USR-a1b2c3d4",
"type": "refresh",
"jti": "RTK-x1y2z3w4",
"iat": 1705302600,
"exp": 1705907400,
"iss": "jun-bank-auth-server"
}| 엔드포인트 | 용도 |
|---|---|
| POST /api/v1/auth/register | 회원가입 시 인증 정보 생성 |
| 이벤트 | 토픽 | 수신 서비스 |
|---|---|---|
| LOGIN_SUCCESS | auth.login.success | Ledger |
| LOGIN_FAILED | auth.login.failed | Ledger |
| USER_DELETED | user.deleted | Auth Server (토큰 무효화) |
- AuthErrorCode (AUTH_xxx 에러 코드)
- AuthException (팩토리 메서드 패턴)
- UserRole (권한 정책)
- AuthUserStatus (상태 정책)
- AuthUserId VO
- Email VO
- Password VO (정책 검증)
- AuthUser (잠금 정책 포함)
- TokenErrorCode (TKN_xxx 에러 코드)
- TokenException (팩토리 메서드 패턴)
- RefreshTokenId VO
- RefreshToken
- HistoryErrorCode (LGH_xxx 에러 코드)
- HistoryException (팩토리 메서드 패턴)
- LoginHistoryId VO
- LoginHistory (Append-only)
- LoginUseCase
- RefreshTokenUseCase
- ValidateTokenUseCase
- LogoutUseCase
- RegisterUseCase
- AuthUserPort
- RefreshTokenPort
- LoginHistoryPort
- DTO 정의
- AuthUserEntity
- RefreshTokenEntity
- LoginHistoryEntity
- JPA Repository
- Repository Adapter
- JwtTokenProvider
- JwtProperties
- Kafka Producer/Consumer
- AuthController
- Request/Response DTO
- Swagger 문서화
- 도메인 단위 테스트 (잠금 정책 등)
- Application 단위 테스트
- JWT 통합 테스트
- API 통합 테스트