토큰 관리 아키텍처
개요
DTA-WIDE 시스템의 토큰 관리 아키텍처는 사용자 토큰의 안전한 저장, 자동 갱신, 그리고 효율적인 분배를 담당합니다. 이 문서는 토큰 관리 시스템의 설계 원칙과 구현 세부사항을 설명합니다.
현재 구현 현황
구현 완료:
- dta-wide-mcp: User Token Delegation Pattern 완전 구현
TokenManagerService: 메모리 기반 토큰 캐싱 및 자동 갱신ApiClientService: 토큰 기반 HTTP 클라이언트 with 자동 재시도- CQRS 기반 토큰 갱신 (
RefreshTokenCommand연동)
계획 중:
- Redis 기반 분산 토큰 저장소 (수평 확장 지원)
- 토큰 암호화 강화 (AES-256-GCM)
- 고급 감사 로깅 및 이상 탐지
아키텍처 개요
핵심 컴포넌트
1. TokenManagerService
책임:
- 사용자별 토큰 저장 및 관리
- 토큰 만료 감지 및 자동 갱신
- 세션 생명주기 관리
주요 메서드:
interface TokenManagerService {
setupUserSession(userId: string, tokens: UserTokens): Promise<void>;
getValidToken(userId: string): Promise<string>;
refreshUserToken(userId: string): Promise<UserTokens>;
cleanupUserSession(userId: string): Promise<void>;
isTokenExpired(token: string): boolean;
}
구현 특징:
- 메모리 기반 토큰 캐싱으로 빠른 액세스
- 토큰 만료 5분 전 사전 갱신
- 401 에러 감지시 반응적 갱신
- 암호화된 토큰 저장
2. ApiClientService
책임:
- HTTP 클라이언트 래퍼 제공
- 자동 토큰 주입 및 갱신
- 재시도 로직 구현
주요 메서드:
interface ApiClientService {
get<T>(url: string, userId?: string): Promise<T>;
post<T>(url: string, data: any, userId?: string): Promise<T>;
put<T>(url: string, data: any, userId?: string): Promise<T>;
delete<T>(url: string, userId?: string): Promise<T>;
}
구현 특징:
- 자동 Bearer 토큰 헤더 추가
- 토큰 만료시 자동 갱신 후 재시도
- 환경별 설정 지원 (로컬/개발/운영)
- 포괄적인 오류 처리
3. Token Storage Strategy
메모리 기반 저장:
interface TokenCache {
[userId: string]: {
accessToken: string;
refreshToken: string;
expiresAt: number;
encryptedAt: number;
};
}
장점:
- 빠른 액세스 속도
- 네트워크 오버헤드 없음
- 단순한 구현
단점:
- 서버 재시작시 토큰 손실
- 메모리 사용량 증가
- 수평 확장시 세션 공유 불가
Redis 기반 저장 (향후 확장):
interface RedisTokenStorage {
setUserTokens(userId: string, tokens: UserTokens, ttl: number): Promise<void>;
getUserTokens(userId: string): Promise<UserTokens | null>;
deleteUserTokens(userId: string): Promise<void>;
isTokenValid(userId: string): Promise<boolean>;
}
토큰 생명주기 관리
1. 토큰 생성 및 저장
구현 세부사항:
async setupUserSession(userId: string, tokens: UserTokens): Promise<void> {
// 토큰 유효성 검증
if (!this.validateTokenFormat(tokens)) {
throw new Error('Invalid token format');
}
// 토큰 암호화
const encryptedTokens = await this.encryptTokens(tokens);
// 만료 시간 계산
const expiresAt = this.calculateExpirationTime(tokens.accessToken);
// 저장
this.tokenCache[userId] = {
...encryptedTokens,
expiresAt,
encryptedAt: Date.now()
};
this.logger.log(`User session setup completed for user: ${userId}`);
}
2. 토큰 유효성 검증
구현 세부사항:
async getValidToken(userId: string): Promise<string> {
const cachedTokens = this.tokenCache[userId];
if (!cachedTokens) {
throw new Error('No active session found');
}
// 토큰 만료 확인 (5분 여유)
const bufferTime = 5 * 60 * 1000; // 5분
if (Date.now() + bufferTime >= cachedTokens.expiresAt) {
await this.refreshUserToken(userId);
return this.tokenCache[userId].accessToken;
}
return cachedTokens.accessToken;
}
3. 자동 토큰 갱신
구현 세부사항:
async refreshUserToken(userId: string): Promise<UserTokens> {
const cachedTokens = this.tokenCache[userId];
if (!cachedTokens) {
throw new Error('No session to refresh');
}
try {
// CQRS 명령을 통한 토큰 갱신
const refreshCommand = new RefreshTokenCommand(cachedTokens.refreshToken);
const newTokens = await this.commandBus.execute(refreshCommand);
// 새 토큰으로 캐시 업데이트
await this.setupUserSession(userId, newTokens);
this.logger.log(`Token refreshed successfully for user: ${userId}`);
return newTokens;
} catch (error) {
// 갱신 실패시 세션 정리
await this.cleanupUserSession(userId);
throw new Error(`Token refresh failed: ${error.message}`);
}
}
보안 구현
1. 토큰 암호화
class TokenEncryption {
private readonly algorithm = 'aes-256-gcm';
private readonly keyLength = 32;
async encryptToken(token: string): Promise<EncryptedToken> {
const key = await this.getEncryptionKey();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.algorithm, key);
cipher.setAAD(Buffer.from('token-encryption'));
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encryptedData: encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
async decryptToken(encryptedToken: EncryptedToken): Promise<string> {
const key = await this.getEncryptionKey();
const decipher = crypto.createDecipher(this.algorithm, key);
decipher.setAuthTag(Buffer.from(encryptedToken.authTag, 'hex'));
decipher.setAAD(Buffer.from('token-encryption'));
let decrypted = decipher.update(encryptedToken.encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
2. 감사 로깅
interface TokenAuditLog {
userId: string;
action: 'CREATE' | 'REFRESH' | 'ACCESS' | 'DELETE';
timestamp: Date;
ipAddress?: string;
userAgent?: string;
success: boolean;
errorMessage?: string;
}
class TokenAuditLogger {
async logTokenAction(log: TokenAuditLog): Promise<void> {
const auditEntry = {
...log,
id: uuidv4(),
timestamp: new Date().toISOString()
};
// 구조화된 로깅
this.logger.log('Token action performed', auditEntry);
// 비정상적인 패턴 감지
await this.detectAnomalousPatterns(log);
}
private async detectAnomalousPatterns(log: TokenAuditLog): Promise<void> {
// 짧은 시간 내 과도한 토큰 갱신
const recentRefreshes = await this.getRecentRefreshCount(log.userId, 300000); // 5분
if (recentRefreshes > 10) {
this.logger.warn('Excessive token refresh detected', { userId: log.userId });
}
// 비정상적인 IP 주소 패턴
if (log.ipAddress && await this.isUnusualIpAddress(log.userId, log.ipAddress)) {
this.logger.warn('Unusual IP address detected', {
userId: log.userId,
ipAddress: log.ipAddress
});
}
}
}
3. 토큰 순환 정책
class TokenRotationPolicy {
private readonly maxTokenAge = 24 * 60 * 60 * 1000; // 24시간
private readonly forceRefreshThreshold = 12 * 60 * 60 * 1000; // 12시간
async enforceRotationPolicy(): Promise<void> {
const expiredSessions = await this.findExpiredSessions();
for (const session of expiredSessions) {
try {
if (this.shouldForceRefresh(session)) {
await this.forceTokenRefresh(session.userId);
} else {
await this.cleanupExpiredSession(session.userId);
}
} catch (error) {
this.logger.error('Token rotation failed', {
userId: session.userId,
error: error.message
});
}
}
}
private shouldForceRefresh(session: TokenSession): boolean {
const age = Date.now() - session.createdAt;
return age > this.forceRefreshThreshold && age < this.maxTokenAge;
}
}
성능 최적화
1. 토큰 캐싱 전략
class TokenCacheOptimizer {
private readonly cacheHitRatio = new Map<string, number>();
private readonly accessPatterns = new Map<string, number[]>();
async optimizeCacheStrategy(): Promise<void> {
// 캐시 히트율 분석
const lowHitRatioUsers = this.findLowHitRatioUsers();
// 접근 패턴 분석
const frequentUsers = this.findFrequentUsers();
// 캐시 전략 조정
await this.adjustCacheStrategy(lowHitRatioUsers, frequentUsers);
}
private findLowHitRatioUsers(): string[] {
return Array.from(this.cacheHitRatio.entries())
.filter(([_, ratio]) => ratio < 0.8)
.map(([userId]) => userId);
}
private async adjustCacheStrategy(
lowHitRatioUsers: string[],
frequentUsers: string[]
): Promise<void> {
// 자주 사용하는 사용자의 토큰은 더 오래 캐시
for (const userId of frequentUsers) {
await this.extendCacheTime(userId);
}
// 히트율이 낮은 사용자는 캐시 정책 재검토
for (const userId of lowHitRatioUsers) {
await this.reviewCachePolicy(userId);
}
}
}
2. 배치 토큰 갱신
class BatchTokenRefresh {
private readonly batchSize = 50;
private readonly refreshQueue: string[] = [];
async scheduleTokenRefresh(userId: string): Promise<void> {
this.refreshQueue.push(userId);
if (this.refreshQueue.length >= this.batchSize) {
await this.processBatch();
}
}
private async processBatch(): Promise<void> {
const batch = this.refreshQueue.splice(0, this.batchSize);
const refreshPromises = batch.map(userId =>
this.refreshUserTokenSafely(userId)
);
const results = await Promise.allSettled(refreshPromises);
// 실패한 갱신 처리
results.forEach((result, index) => {
if (result.status === 'rejected') {
this.logger.error('Batch token refresh failed', {
userId: batch[index],
error: result.reason
});
}
});
}
}
모니터링 및 메트릭
핵심 메트릭
-
토큰 갱신 성공률
- 목표: > 99%
- 알림 임계값: < 95%
-
평균 토큰 생명주기
- 목표: 24시간
- 모니터링: 비정상적으로 짧은 생명주기 감지
-
캐시 히트율
- 목표: > 90%
- 최적화 기준: < 80%
-
API 호출 지연 시간
- 목표: < 200ms (토큰 포함)
- 알림 임계값: > 500ms
대시보드 구성
interface TokenMetrics {
totalActiveSessions: number;
tokenRefreshRate: number;
cacheHitRatio: number;
averageTokenLifetime: number;
failedRefreshCount: number;
securityAlerts: number;
}
class TokenMetricsCollector {
async collectMetrics(): Promise<TokenMetrics> {
return {
totalActiveSessions: await this.countActiveSessions(),
tokenRefreshRate: await this.calculateRefreshRate(),
cacheHitRatio: await this.calculateCacheHitRatio(),
averageTokenLifetime: await this.calculateAverageLifetime(),
failedRefreshCount: await this.countFailedRefreshes(),
securityAlerts: await this.countSecurityAlerts()
};
}
}