- JWT 검증 플로우: Gateway 내부 처리로 변경 - Username → UUID 변환 메커니즘 문서화 - UUID5 체계: Slack 사용자용 결정적 UUID 생성 - Gateway 프록시 패턴 상세 문서화 - 데이터베이스: gmail_tokens, robeing 스키마 추가 - 서비스 포트 매핑 및 역할 명확화 - auth_db → main_db 마이그레이션 반영
386 lines
9.8 KiB
Markdown
386 lines
9.8 KiB
Markdown
# UUID 변환 시스템 아키텍처
|
|
|
|
## 작성일: 2025-08-21
|
|
## 작성자: Claude (51123 서버 관리자)
|
|
|
|
---
|
|
|
|
## 1. 개요
|
|
|
|
RO-BEING 시스템의 UUID 변환 체계 문서입니다. 일반 사용자는 UUID4, Slack 사용자는 UUID5를 사용하여 일관된 사용자 식별 체계를 유지합니다.
|
|
|
|
---
|
|
|
|
## 2. UUID 타입별 용도
|
|
|
|
### 2.1 UUID 타입 구분
|
|
|
|
| 타입 | 생성 방식 | 용도 | 특징 |
|
|
|------|----------|------|------|
|
|
| UUID4 | 랜덤 생성 | Google OAuth 사용자 | 매번 다른 값 생성 |
|
|
| UUID5 | 네임스페이스 + 이름 해시 | Slack 사용자 | 같은 입력에 같은 출력 (결정적) |
|
|
|
|
### 2.2 네임스페이스
|
|
|
|
```python
|
|
# DNS 네임스페이스 (표준 UUID)
|
|
NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 사용자별 UUID 생성
|
|
|
|
### 3.1 Google OAuth 사용자 (UUID4)
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Google 로그인] --> B[이메일 확인]
|
|
B --> C{기존 사용자?}
|
|
C -->|Yes| D[기존 UUID 사용]
|
|
C -->|No| E[uuid4() 생성]
|
|
E --> F[DB 저장]
|
|
F --> G[JWT 생성]
|
|
```
|
|
|
|
#### 구현 코드
|
|
```python
|
|
import uuid
|
|
|
|
class GoogleAuthHandler:
|
|
async def create_or_get_user(self, email: str, name: str):
|
|
# 기존 사용자 확인
|
|
user = await self.db.fetchone(
|
|
"SELECT * FROM users WHERE email = $1",
|
|
email
|
|
)
|
|
|
|
if user:
|
|
return user['id']
|
|
|
|
# 신규 사용자 - UUID4 생성
|
|
new_user_id = str(uuid.uuid4())
|
|
|
|
await self.db.execute("""
|
|
INSERT INTO users (id, email, name, username, oauth_provider)
|
|
VALUES ($1, $2, $3, $4, 'google')
|
|
""", new_user_id, email, name, self.generate_username(email))
|
|
|
|
return new_user_id
|
|
```
|
|
|
|
### 3.2 Slack 사용자 (UUID5)
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Slack 이벤트] --> B[Slack User ID]
|
|
B --> C[UUID5 생성]
|
|
C --> D[네임스페이스 + Slack ID]
|
|
D --> E[SHA-1 해시]
|
|
E --> F[결정적 UUID]
|
|
F --> G{DB 확인}
|
|
G -->|없음| H[신규 등록]
|
|
G -->|있음| I[기존 사용]
|
|
```
|
|
|
|
#### 구현 코드
|
|
```python
|
|
import uuid
|
|
|
|
class SlackUserHandler:
|
|
NAMESPACE = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
|
|
|
|
def get_user_uuid(self, slack_user_id: str) -> str:
|
|
"""Slack User ID를 UUID5로 변환"""
|
|
return str(uuid.uuid5(self.NAMESPACE, slack_user_id))
|
|
|
|
async def get_or_create_user(self, slack_user_id: str, slack_user_info: dict):
|
|
# UUID5 생성 (항상 같은 결과)
|
|
user_uuid = self.get_user_uuid(slack_user_id)
|
|
|
|
# 기존 사용자 확인
|
|
user = await self.db.fetchone(
|
|
"SELECT * FROM users WHERE id = $1",
|
|
user_uuid
|
|
)
|
|
|
|
if user:
|
|
return user_uuid
|
|
|
|
# 신규 사용자 등록
|
|
username = f"slack_{slack_user_id[:8]}"
|
|
await self.db.execute("""
|
|
INSERT INTO users (id, username, email, name, oauth_provider)
|
|
VALUES ($1, $2, $3, $4, 'slack')
|
|
""", user_uuid, username,
|
|
slack_user_info.get('email', ''),
|
|
slack_user_info.get('real_name', ''))
|
|
|
|
return user_uuid
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Gateway에서의 UUID 변환
|
|
|
|
### 4.1 JWT Token → UUID 변환 플로우
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Client
|
|
participant Gateway
|
|
participant DB
|
|
participant Service
|
|
|
|
Client->>Gateway: Request + JWT (username)
|
|
Gateway->>Gateway: JWT 검증
|
|
Gateway->>Gateway: username 추출
|
|
|
|
Gateway->>DB: SELECT id FROM users WHERE username = ?
|
|
DB-->>Gateway: UUID
|
|
|
|
Gateway->>Service: Request + X-User-Id (UUID)
|
|
Service-->>Gateway: Response
|
|
Gateway-->>Client: Response
|
|
```
|
|
|
|
### 4.2 변환 구현
|
|
|
|
```python
|
|
class GatewayUUIDConverter:
|
|
def __init__(self, db_pool):
|
|
self.db_pool = db_pool
|
|
self.cache = {} # username -> uuid 캐시
|
|
|
|
async def username_to_uuid(self, username: str) -> str:
|
|
"""Username을 UUID로 변환 (캐싱 포함)"""
|
|
# 캐시 확인
|
|
if username in self.cache:
|
|
return self.cache[username]
|
|
|
|
# DB 조회
|
|
async with self.db_pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"SELECT id FROM users WHERE username = $1",
|
|
username
|
|
)
|
|
|
|
if not row:
|
|
raise UserNotFoundError(f"User {username} not found")
|
|
|
|
user_uuid = str(row['id'])
|
|
|
|
# 캐시 저장 (5분 TTL)
|
|
self.cache[username] = user_uuid
|
|
asyncio.create_task(self.clear_cache_after(username, 300))
|
|
|
|
return user_uuid
|
|
|
|
async def clear_cache_after(self, username: str, seconds: int):
|
|
"""일정 시간 후 캐시 제거"""
|
|
await asyncio.sleep(seconds)
|
|
self.cache.pop(username, None)
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 테스트 사용자 UUID
|
|
|
|
### 5.1 하드코딩된 테스트 UUID
|
|
|
|
```sql
|
|
-- 테스트 사용자 (고정 UUID)
|
|
INSERT INTO users (id, username, email, name) VALUES
|
|
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'happybell80', 'goeun2dc@gmail.com', '김종태'),
|
|
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'eagle0914', '0914eagle@gmail.com', '전희재'),
|
|
('cccccccc-cccc-cccc-cccc-cccccccccccc', 'test_user', 'test@example.com', 'Test User');
|
|
```
|
|
|
|
### 5.2 Slack 테스트 사용자 UUID5 예시
|
|
|
|
```python
|
|
# Slack User ID 예시
|
|
slack_ids = {
|
|
"U0925SXQFDK": "종태", # UUID5: 생성된 값
|
|
"U0123ABCDEF": "테스트" # UUID5: 생성된 값
|
|
}
|
|
|
|
# UUID5 생성 예시
|
|
for slack_id, name in slack_ids.items():
|
|
user_uuid = uuid.uuid5(NAMESPACE, slack_id)
|
|
print(f"{name} ({slack_id}): {user_uuid}")
|
|
```
|
|
|
|
---
|
|
|
|
## 6. UUID 마이그레이션
|
|
|
|
### 6.1 기존 VARCHAR user_id → UUID 마이그레이션
|
|
|
|
```sql
|
|
-- 1. 임시 컬럼 추가
|
|
ALTER TABLE gmail_tokens ADD COLUMN user_uuid UUID;
|
|
|
|
-- 2. UUID 매핑
|
|
UPDATE gmail_tokens gt
|
|
SET user_uuid = u.id
|
|
FROM users u
|
|
WHERE gt.user_id = u.username;
|
|
|
|
-- 3. 기존 컬럼 제거 및 이름 변경
|
|
ALTER TABLE gmail_tokens DROP COLUMN user_id;
|
|
ALTER TABLE gmail_tokens RENAME COLUMN user_uuid TO user_id;
|
|
|
|
-- 4. 외래키 제약 추가
|
|
ALTER TABLE gmail_tokens
|
|
ADD CONSTRAINT fk_user_id
|
|
FOREIGN KEY (user_id) REFERENCES users(id);
|
|
```
|
|
|
|
---
|
|
|
|
## 7. UUID 유효성 검증
|
|
|
|
### 7.1 검증 함수
|
|
|
|
```python
|
|
import re
|
|
|
|
def is_valid_uuid(uuid_string: str) -> bool:
|
|
"""UUID 형식 검증"""
|
|
uuid_pattern = re.compile(
|
|
r'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
|
|
re.IGNORECASE
|
|
)
|
|
return bool(uuid_pattern.match(uuid_string))
|
|
|
|
def get_uuid_version(uuid_string: str) -> int:
|
|
"""UUID 버전 확인"""
|
|
try:
|
|
u = uuid.UUID(uuid_string)
|
|
return u.version
|
|
except ValueError:
|
|
return None
|
|
|
|
# 사용 예시
|
|
user_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
|
if is_valid_uuid(user_id):
|
|
version = get_uuid_version(user_id)
|
|
print(f"Valid UUID v{version}")
|
|
```
|
|
|
|
---
|
|
|
|
## 8. UUID 관련 API 응답
|
|
|
|
### 8.1 사용자 정보 API
|
|
|
|
```python
|
|
@app.get("/api/users/me")
|
|
async def get_current_user(request: Request):
|
|
user_id = request.headers.get("X-User-Id")
|
|
username = request.headers.get("X-Username")
|
|
|
|
return {
|
|
"id": user_id, # UUID
|
|
"username": username,
|
|
"uuid_version": get_uuid_version(user_id),
|
|
"is_slack_user": get_uuid_version(user_id) == 5
|
|
}
|
|
```
|
|
|
|
### 8.2 UUID 디버깅 엔드포인트
|
|
|
|
```python
|
|
@app.get("/api/debug/uuid/{username}")
|
|
async def debug_uuid(username: str):
|
|
"""개발 환경에서만 사용"""
|
|
user = await db.fetchone(
|
|
"SELECT id, username, oauth_provider FROM users WHERE username = $1",
|
|
username
|
|
)
|
|
|
|
if not user:
|
|
return {"error": "User not found"}
|
|
|
|
return {
|
|
"username": username,
|
|
"uuid": str(user['id']),
|
|
"uuid_version": get_uuid_version(str(user['id'])),
|
|
"oauth_provider": user['oauth_provider'],
|
|
"is_deterministic": get_uuid_version(str(user['id'])) == 5
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. 트러블슈팅
|
|
|
|
### 9.1 일반적인 문제
|
|
|
|
| 문제 | 원인 | 해결 방법 |
|
|
|------|------|----------|
|
|
| UUID 불일치 | Slack ID 변경 | UUID5는 결정적이므로 같은 Slack ID는 항상 같은 UUID |
|
|
| 중복 UUID | UUID4 충돌 (매우 드물음) | 재생성 또는 UNIQUE 제약 확인 |
|
|
| 변환 실패 | username 없음 | users 테이블 username 필드 확인 |
|
|
| 캐시 불일치 | TTL 만료 전 DB 변경 | 캐시 무효화 또는 TTL 단축 |
|
|
|
|
### 9.2 디버깅 쿼리
|
|
|
|
```sql
|
|
-- UUID 버전별 사용자 수
|
|
SELECT
|
|
CASE
|
|
WHEN id::text LIKE '________-____-4___-____-____________' THEN 'UUID4 (Google)'
|
|
WHEN id::text LIKE '________-____-5___-____-____________' THEN 'UUID5 (Slack)'
|
|
ELSE 'Other'
|
|
END as uuid_type,
|
|
COUNT(*) as count
|
|
FROM users
|
|
GROUP BY uuid_type;
|
|
|
|
-- Username-UUID 매핑 확인
|
|
SELECT username, id, oauth_provider
|
|
FROM users
|
|
WHERE username = 'happybell80';
|
|
|
|
-- Slack 사용자 UUID5 검증
|
|
SELECT
|
|
username,
|
|
id,
|
|
id = uuid_generate_v5('6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid,
|
|
SUBSTRING(username FROM 7)) as is_valid_uuid5
|
|
FROM users
|
|
WHERE oauth_provider = 'slack';
|
|
```
|
|
|
|
---
|
|
|
|
## 10. 보안 고려사항
|
|
|
|
### 10.1 UUID 노출 최소화
|
|
|
|
```python
|
|
class SecureUUIDHandler:
|
|
@staticmethod
|
|
def mask_uuid(uuid_string: str) -> str:
|
|
"""UUID 일부 마스킹"""
|
|
# aaaaaaaa-****-****-****-aaaaaaaaaaaa
|
|
parts = uuid_string.split('-')
|
|
return f"{parts[0]}-****-****-****-{parts[4]}"
|
|
|
|
@staticmethod
|
|
def should_expose_uuid(user_role: str) -> bool:
|
|
"""역할별 UUID 노출 여부"""
|
|
return user_role in ['admin', 'developer']
|
|
```
|
|
|
|
### 10.2 UUID 추측 방지
|
|
|
|
- UUID4: 122비트 랜덤 엔트로피로 추측 불가능
|
|
- UUID5: Slack ID를 모르면 생성 불가능
|
|
- 네임스페이스 비공개 유지 (소스코드에서만 관리)
|
|
|
|
---
|
|
|
|
**문서 끝** |