DOCS/journey/troubleshooting/251117_gateway_admin_routing_jwt_fix.md

7.0 KiB

Gateway Admin API 라우팅 및 JWT 검증 문제 해결

날짜: 2025-11-17
작성자: admin
관련 이슈: 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):

# Include routers
app.include_router(slack.router)
app.include_router(admin.router, prefix="")  # prefix 명시

라우터 정의 (robeing-gateway/app/routers/admin.py):

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"
            )

검증:

# 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):

# Before: ADMIN_BACKEND_URL = "http://localhost:8000"
# After:
ADMIN_BACKEND_URL = "http://admin-dashboard-backend:8000"

Docker 네트워크 확인:

# 같은 네트워크에 있는 컨테이너 확인
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.ymlcontainer_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.pySECRET_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):

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

재빌드:

cd /home/admin/robeing-gateway
sudo docker compose down
sudo docker compose up -d --build

검증:

# 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)

참고