diff --git a/journey/troubleshooting/251117_gateway_admin_routing_jwt_fix.md b/journey/troubleshooting/251117_gateway_admin_routing_jwt_fix.md new file mode 100644 index 0000000..f725f3c --- /dev/null +++ b/journey/troubleshooting/251117_gateway_admin_routing_jwt_fix.md @@ -0,0 +1,212 @@ +# Gateway Admin API 라우팅 및 JWT 검증 문제 해결 + +**날짜**: 2025-11-17 +**관련 이슈**: Admin Dashboard 표준 배포 방식 전환 + +## 문제 상황 + +Admin Dashboard를 표준 배포 방식으로 전환하면서 Gateway를 통한 API 프록시가 필요했으나, 다음 문제들이 발생: + +1. Gateway에서 admin API 요청이 404 오류 발생 +2. 로그인 후 JWT 토큰 검증 실패 (401 Unauthorized) +3. Docker 컨테이너 간 통신 문제 + +## 해결 과정 + +### 1. Gateway Admin Router 등록 + +**문제**: `/admin/api/*` 요청이 Gateway에서 처리되지 않음 + +**원인**: +- `robeing-gateway/app/routers/admin.py`의 router가 제대로 등록되지 않음 +- router prefix 설정 문제 + +**해결** (`robeing-gateway/app/main.py`): +```python +# Include routers +app.include_router(slack.router) +app.include_router(admin.router, prefix="") # prefix 명시 +``` + +**라우터 정의** (`robeing-gateway/app/routers/admin.py`): +```python +from fastapi import APIRouter, Request, HTTPException, Header +from typing import Optional +import httpx +import logging +from app.core.auth import get_verified_user + +logger = logging.getLogger(__name__) +router = APIRouter() + +ADMIN_BACKEND_URL = "http://admin-dashboard-backend:8000" + +@router.api_route("/admin/api/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) +async def admin_api_proxy( + request: Request, + path: str, + authorization: Optional[str] = Header(None) +): + """ + Admin API 프록시 - 로그인은 JWT 검증 생략, 나머지는 필수 + """ + # 로그인 API는 JWT 검증 생략 + if path == "login": + logger.info(f"Admin login request to /admin/api/{path}") + else: + # JWT 검증 (필수) + try: + user_uuid = get_verified_user(authorization) + logger.info(f"Admin API request from user {user_uuid} to /admin/api/{path}") + except HTTPException as e: + logger.warning(f"Admin API request without valid JWT to /admin/api/{path}") + raise e + + # Backend로 프록시 + target_url = f"{ADMIN_BACKEND_URL}/admin/{path}" + + async with httpx.AsyncClient(timeout=30.0) as client: + try: + response = await client.request( + method=request.method, + url=target_url, + headers=dict(request.headers), + content=await request.body() + ) + + return Response( + content=response.content, + status_code=response.status_code, + headers=dict(response.headers) + ) + except httpx.RequestError as e: + logger.error(f"Failed to proxy admin API request: {e}") + raise HTTPException( + status_code=503, + detail="Admin backend service unavailable" + ) +``` + +**검증**: +```bash +# Gateway 라우트 확인 +sudo docker exec robeing-gateway python -c \ + "from app.main import app; print([r.path for r in app.routes if 'admin' in r.path])" +# ['/admin/api/{path:path}'] +``` + +### 2. Docker 네트워크 내부 통신 문제 + +**문제**: Gateway 컨테이너에서 `localhost:8000`으로 접근 시 자기 자신을 가리킴 + +**원인**: +- Docker 컨테이너 내부에서 `localhost`는 컨테이너 자신을 의미 +- 다른 컨테이너에 접근하려면 컨테이너 이름 또는 네트워크 IP 사용 필요 + +**해결** (`robeing-gateway/app/routers/admin.py`): +```python +# Before: ADMIN_BACKEND_URL = "http://localhost:8000" +# After: +ADMIN_BACKEND_URL = "http://admin-dashboard-backend:8000" +``` + +**Docker 네트워크 확인**: +```bash +# 같은 네트워크에 있는 컨테이너 확인 +sudo docker network inspect appnet | grep -A 5 "admin-dashboard-backend" +# "Name": "admin-dashboard-backend", +# "IPv4Address": "172.21.0.4/16" +``` + +**교훈**: +- Docker 컨테이너 간 통신은 `localhost`가 아닌 컨테이너 이름 사용 +- 같은 Docker 네트워크(`appnet`)에 있는 컨테이너는 이름으로 자동 DNS 해석됨 +- 컨테이너 이름은 `docker-compose.yml`의 `container_name` 또는 서비스 이름 사용 + +### 3. JWT Secret Key 불일치 문제 + +**문제**: 로그인은 성공하지만 이후 API 호출 시 401 Unauthorized 오류 + +**원인**: +- Gateway의 JWT_SECRET_KEY와 admin-dashboard backend의 SECRET_KEY가 불일치 +- Gateway: `.env` 파일의 `JWT_SECRET_KEY=9cc562b6296b87b02dd89045a2e7e11c249713a59a5ac0160d852121f1289664` +- Backend: `admin_routes.py`의 `SECRET_KEY = "admin_secret_key_robeing_2025"` + +**에러 로그**: +``` +2025-11-17 19:36:23,945 - app.core.auth - ERROR - JWT verification failed: Not enough segments +{"detail":"Token validation failed: Signature verification failed."} +``` + +**해결** (`robeing-gateway/docker-compose.yml`): +```yaml +services: + robeing-gateway: + container_name: robeing-gateway + build: . + env_file: + - .env + environment: + - JWT_SECRET_KEY=admin_secret_key_robeing_2025 # Backend와 일치 + ports: + - "${CNTR_PORT:-8100}:8000" + networks: + - appnet +``` + +**재빌드**: +```bash +cd /home/admin/robeing-gateway +sudo docker compose down +sudo docker compose up -d --build +``` + +**검증**: +```bash +# JWT 토큰 발급 +TOKEN=$(curl -s -X POST http://localhost:8000/admin/login \ + -H "Content-Type: application/json" \ + -d '{"password":"19800508"}' | python3 -c \ + "import sys, json; print(json.load(sys.stdin)['access_token'])") + +# Gateway를 통한 API 호출 테스트 +curl -X GET http://localhost:8100/admin/api/system/overview \ + -H "Authorization: Bearer $TOKEN" +# {"timestamp":"2025-11-17T10:37:21.154596","cpu":{"percent":4.6,...},...} +``` + +**교훈**: +- JWT 검증을 위해서는 발급 서버와 검증 서버의 secret key가 반드시 일치해야 함 +- Docker Compose의 `environment` 섹션으로 `.env` 파일의 환경변수 오버라이드 가능 +- 운영 환경에서는 환경변수로 관리하여 보안 강화 필요 + +## 최종 구조 + +``` +[Browser] + → [Nginx] /admin/api/* + → [Gateway:8100] /admin/api/{path} + → [Admin Backend:8000] /admin/{path} +``` + +**요청 흐름**: +1. 브라우저: `POST /admin/api/login` (비밀번호 전송) +2. Nginx: Gateway로 프록시 (`http://localhost:8100/admin/api/login`) +3. Gateway: 로그인은 JWT 검증 생략, Backend로 프록시 (`http://admin-dashboard-backend:8000/admin/login`) +4. Backend: JWT 토큰 발급 후 반환 +5. 브라우저: 토큰 저장 후 `GET /admin/api/system/overview` (Authorization 헤더 포함) +6. Gateway: JWT 검증 후 Backend로 프록시 +7. Backend: 데이터 반환 + +## 관련 파일 + +- `robeing-gateway/app/routers/admin.py`: Admin API 프록시 라우터 +- `robeing-gateway/app/main.py`: Router 등록 +- `robeing-gateway/docker-compose.yml`: JWT_SECRET_KEY 환경변수 설정 +- `admin-dashboard/backend/admin_routes.py`: JWT 토큰 발급 (SECRET_KEY) + +## 참고 + +- [Admin Dashboard 표준 배포 전환 문서](./251117_admin_dashboard_standard_deployment_refactoring.md) +- Gateway는 nginx를 통해 프록시되므로 nginx 설정(`/admin/api/` → Gateway) 확인 필요 +