13 KiB
13 KiB
Gateway 프록시 패턴 아키텍처
작성일: 2025-08-21
작성자: Claude (51123 서버 관리자)
1. 개요
robeing-gateway (포트 8100)의 프록시 패턴 및 JWT 인증 처리 아키텍처 문서입니다. Gateway는 모든 API 요청의 진입점으로서 인증, 라우팅, UUID 변환을 담당합니다.
2. 핵심 역할
2.1 주요 기능
- JWT 토큰 검증: 내부적으로 토큰 유효성 검증
- Username → UUID 변환: JWT의 username을 UUID로 변환
- 요청 라우팅: 적절한 백엔드 서비스로 프록시
- 헤더 주입: X-User-Id, X-Username 헤더 추가
- 보안 게이트웨이: 인증되지 않은 요청 차단
2.3 Slack 웹훅 프록시 필수 규칙
- 원문 바디 보존: Slack Events/Interactive 프록시는
request.body()원문 바이트를 그대로 전달한다. - 재직렬화 금지: Slack 서명 검증 경로에서
json=기반 재직렬화 전달을 금지한다. - 권장 포워딩:
client.post(..., content=raw_body, headers=...)패턴을 기본으로 사용한다. - 헤더 전달:
X-Slack-Signature,X-Slack-Request-Timestamp,Content-Type를 함께 전달한다. - 운영 검증: 배포 후
/gateway/slack/events의 200/403/500 분포와upstream_status를 같은 시각 기준으로 확인한다.
2.2 서비스 위치
- 서버: 51123
- 포트: 8100
- 컨테이너: robeing-gateway
3. 인증 플로우
3.1 JWT 검증 프로세스
sequenceDiagram
participant Client as 클라이언트
participant Gateway as Gateway(8100)
participant DB as PostgreSQL
participant Service as 백엔드 서비스
Client->>Gateway: API 요청 + JWT Token
Gateway->>Gateway: JWT 서명 검증
Note over Gateway: HS256 알고리즘
alt 토큰 무효
Gateway-->>Client: 401 Unauthorized
else 토큰 유효
Gateway->>Gateway: username 추출
Note over Gateway: JWT payload에서
Gateway->>DB: SELECT id FROM user WHERE username = ?
DB-->>Gateway: UUID
Gateway->>Service: 요청 전달
Note over Service: Headers:<br/>X-User-Id: UUID<br/>X-Username: username
Service-->>Gateway: 응답
Gateway-->>Client: 응답 전달
end
3.2 JWT 구조
{
"header": {
"alg": "HS256",
"typ": "JWT"
},
"payload": {
"username": "happybell80",
"email": "goeun2dc@gmail.com",
"exp": 1724356800,
"iat": 1724270400
}
}
4. UUID 변환 메커니즘
4.1 변환 로직
class GatewayProxy:
async def convert_username_to_uuid(self, username: str) -> str:
"""Username을 UUID로 변환"""
query = "SELECT id FROM user WHERE username = $1"
result = await self.db.fetchone(query, username)
if not result:
raise UserNotFoundError(f"User {username} not found")
return str(result['id'])
async def process_request(self, request):
# 1. JWT에서 username 추출
token = request.headers.get('Authorization')
payload = self.verify_jwt(token)
username = payload['username']
# 2. UUID 변환
user_uuid = await self.convert_username_to_uuid(username)
# 3. 헤더 추가
request.headers['X-User-Id'] = user_uuid
request.headers['X-Username'] = username
# 4. 프록시
return await self.proxy_to_service(request)
4.2 캐싱 전략
from functools import lru_cache
from datetime import datetime, timedelta
class UserCache:
def __init__(self, ttl_seconds=300):
self.cache = {}
self.ttl = timedelta(seconds=ttl_seconds)
async def get_uuid(self, username: str) -> str:
# 캐시 확인
if username in self.cache:
entry = self.cache[username]
if datetime.now() < entry['expires']:
return entry['uuid']
# DB 조회
uuid = await self.fetch_from_db(username)
# 캐시 저장
self.cache[username] = {
'uuid': uuid,
'expires': datetime.now() + self.ttl
}
return uuid
5. 라우팅 규칙
5.1 서비스 매핑
routes:
# 인증 서비스
- path: /api/auth/*
service: auth-server
host: localhost
port: 9000
auth_required: false
# Gmail 아이템
- path: /api/items/gmail/*
service: robeing-monitor
host: localhost
port: 9024
auth_required: true
# 로빙 서비스 (51124 서버)
- path: /api/robeing/*
service: rb10508_micro
host: 192.168.219.52
port: 10508
auth_required: true
# 스킬 서비스
- path: /api/skills/email/*
service: skill-email
host: 192.168.219.52
port: 8501
auth_required: true
5.2 동적 라우팅
class DynamicRouter:
def __init__(self):
self.routes = self.load_routes()
async def route_request(self, path: str, headers: dict):
# 경로 매칭
for route in self.routes:
if self.match_path(path, route['path']):
# 인증 확인
if route['auth_required'] and not headers.get('X-User-Id'):
raise AuthenticationError()
# 프록시
return await self.proxy(
host=route['host'],
port=route['port'],
path=path,
headers=headers
)
raise NotFoundError(f"No route for {path}")
6. 에러 처리
6.1 에러 응답 포맷
{
"error": {
"code": "AUTHENTICATION_FAILED",
"message": "Invalid or expired token",
"timestamp": "2025-08-21T10:30:00Z",
"request_id": "req_123456"
}
}
6.2 에러 코드
| 코드 | HTTP Status | 설명 |
|---|---|---|
| INVALID_TOKEN | 401 | JWT 토큰 무효 |
| TOKEN_EXPIRED | 401 | JWT 토큰 만료 |
| USER_NOT_FOUND | 404 | Username에 해당하는 사용자 없음 |
| SERVICE_UNAVAILABLE | 503 | 백엔드 서비스 응답 없음 |
| RATE_LIMIT_EXCEEDED | 429 | 요청 제한 초과 |
7. 성능 최적화
7.1 Connection Pooling
class ConnectionPool:
def __init__(self):
self.pools = {}
async def get_connection(self, service: str):
if service not in self.pools:
self.pools[service] = await aiohttp.ClientSession(
connector=aiohttp.TCPConnector(
limit=100,
limit_per_host=30,
ttl_dns_cache=300
)
)
return self.pools[service]
7.2 Request/Response 압축
@app.middleware("http")
async def compression_middleware(request: Request, call_next):
response = await call_next(request)
# gzip 압축 적용
if "gzip" in request.headers.get("Accept-Encoding", ""):
response.headers["Content-Encoding"] = "gzip"
response.body = gzip.compress(response.body)
return response
8. 모니터링
8.1 메트릭 수집
metrics = {
"request_count": Counter("gateway_requests_total"),
"request_duration": Histogram("gateway_request_duration_seconds"),
"auth_failures": Counter("gateway_auth_failures_total"),
"proxy_errors": Counter("gateway_proxy_errors_total")
}
@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
start_time = time.time()
try:
response = await call_next(request)
metrics["request_count"].inc()
return response
except Exception as e:
metrics["proxy_errors"].inc()
raise
finally:
duration = time.time() - start_time
metrics["request_duration"].observe(duration)
8.2 로그 포맷
{
"timestamp": "2025-08-21T10:30:00Z",
"level": "INFO",
"service": "gateway",
"request_id": "req_123456",
"method": "POST",
"path": "/api/robeing/chat",
"username": "happybell80",
"user_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"backend_service": "rb10508_micro",
"response_time_ms": 150,
"status_code": 200
}
9. 보안 고려사항
9.1 Rate Limiting
from datetime import datetime, timedelta
class RateLimiter:
def __init__(self, requests_per_minute=60):
self.limits = {}
self.max_requests = requests_per_minute
async def check_limit(self, user_id: str) -> bool:
now = datetime.now()
minute_ago = now - timedelta(minutes=1)
# 사용자별 요청 기록
if user_id not in self.limits:
self.limits[user_id] = []
# 1분 이내 요청만 유지
self.limits[user_id] = [
ts for ts in self.limits[user_id]
if ts > minute_ago
]
# 제한 확인
if len(self.limits[user_id]) >= self.max_requests:
return False
self.limits[user_id].append(now)
return True
9.2 CORS 설정
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.ro-being.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
max_age=3600
)
10. 구현 예제
10.1 완전한 Gateway 클래스
from fastapi import FastAPI, Request, HTTPException
import httpx
import jwt
import asyncpg
class RobeingGateway:
def __init__(self):
self.app = FastAPI()
self.db_pool = None
self.http_client = httpx.AsyncClient()
self.setup_routes()
async def startup(self):
self.db_pool = await asyncpg.create_pool(
"postgresql://robeings:robeings@localhost/main_db"
)
def setup_routes(self):
@self.app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(request: Request, path: str):
# JWT 검증
token = request.headers.get("Authorization", "").replace("Bearer ", "")
if not token and self.requires_auth(path):
raise HTTPException(401, "Missing token")
# Username → UUID 변환
if token:
payload = self.verify_jwt(token)
username = payload['username']
user_uuid = await self.get_user_uuid(username)
# 헤더 추가
headers = dict(request.headers)
headers['X-User-Id'] = user_uuid
headers['X-Username'] = username
else:
headers = dict(request.headers)
# 백엔드 서비스로 프록시
backend_url = self.get_backend_url(path)
response = await self.http_client.request(
method=request.method,
url=backend_url,
headers=headers,
content=await request.body()
)
return response.json()
def verify_jwt(self, token: str) -> dict:
try:
return jwt.decode(
token,
"your-secret-key",
algorithms=["HS256"]
)
except jwt.InvalidTokenError:
raise HTTPException(401, "Invalid token")
async def get_user_uuid(self, username: str) -> str:
async with self.db_pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT id FROM user WHERE username = $1",
username
)
if not row:
raise HTTPException(404, f"User {username} not found")
return str(row['id'])
11. 테스트 전략
11.1 단위 테스트
import pytest
from unittest.mock import Mock, patch
class TestGateway:
@pytest.mark.asyncio
async def test_username_to_uuid_conversion(self):
gateway = RobeingGateway()
# Mock DB response
with patch.object(gateway, 'db_pool') as mock_pool:
mock_pool.acquire.return_value.__aenter__.return_value.fetchrow.return_value = {
'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
}
uuid = await gateway.get_user_uuid('happybell80')
assert uuid == 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
def test_jwt_verification(self):
gateway = RobeingGateway()
# Valid token
token = jwt.encode(
{'username': 'test', 'exp': datetime.now() + timedelta(hours=1)},
'your-secret-key',
algorithm='HS256'
)
payload = gateway.verify_jwt(token)
assert payload['username'] == 'test'
문서 끝