DOCS/journey/troubleshooting/251117_admin_dashboard_jwt_verification_separation.md

9.1 KiB

Admin Dashboard JWT 검증 분리 구현

날짜: 2025-11-17
작업자: Claude (51123 서버 관리자)
관련 서버: 51123
관련 서비스: robeing-gateway, frontend-base, nginx


배경

관리자 대시보드(/admin) 접근 시 JWT 검증 문제 발생:

문제 상황

  • 브라우저가 /admin HTML 페이지를 요청
  • Gateway가 모든 요청에 JWT 검증 요구 → 401 반환
  • HTML 페이지 자체를 받지 못해서 JavaScript 실행 불가
  • JavaScript가 localStorage에서 JWT를 가져와서 API 요청에 포함해야 하는데, HTML이 로드되지 않아 실행 불가

근본 원인

Gateway의 /admin/{path:path} 라우트가 모든 요청에 JWT 검증을 요구하고 있었음:

  • HTML 페이지 요청도 JWT 검증 필요 → 잘못된 설계
  • HTML은 공개적으로 접근 가능해야 하고, JavaScript가 로드된 후 JWT 확인 필요

해결 방안

올바른 설계

  1. /admin (HTML) → JWT 검증 제외 → frontend-base
  2. /admin/api/* (데이터 API) → JWT 검증 필요 → Gateway → robeing-monitor

구현 전략

  • HTML/정적 파일: JWT 검증 없이 프록시 (JavaScript에서 JWT 체크)
  • API 요청: JWT 검증 필요 (보안 유지)

구현 내역

1. Gateway 라우팅 수정

파일: /home/admin/robeing-gateway/app/main.py

변경 사항:

  • /admin/{path:path} 라우트에서 조건부 JWT 검증 적용
  • path.startswith("api/")로 API 요청 구분
  • HTML/정적 파일은 JWT 검증 없이 프록시
  • API 요청만 JWT 검증 수행
@app.api_route("/admin/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def admin_proxy(
    path: str,
    request: Request,
    authorization: Optional[str] = Header(None)
):
    """
    Admin dashboard - proxy to frontend-base (51123:8000)
    
    - HTML/정적 파일: JWT 검증 없이 프록시 (JavaScript에서 JWT 체크)
    - API 요청 (/admin/api/*): JWT 검증 필요
    """
    frontend_base_url = "http://localhost:8000"
    target_url = f"{frontend_base_url}/admin/{path}" if path else f"{frontend_base_url}/admin"
    
    # API 요청인지 확인 (robeing-monitor API)
    is_api_request = path.startswith("api/") if path else False
    
    # API 요청은 JWT 검증 필요
    if is_api_request:
        try:
            user_uuid = get_verified_user(authorization)
            logger.info(f"Admin API request from user {user_uuid} to {target_url}")
        except HTTPException as e:
            logger.warning(f"Admin API request without valid JWT to {target_url}")
            raise e
    else:
        # HTML/정적 파일은 JWT 검증 없이 프록시
        logger.info(f"Admin static/HTML request to {target_url}")

    # Forward request
    async with httpx.AsyncClient() as client:
        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)
        )

2. Frontend JavaScript 수정

파일: /home/admin/frontend-base/admin-ui/index.html

변경 사항:

  1. 페이지 로드 시 localStorage에서 JWT 확인
  2. JWT가 없으면 auth.ro-being.com/login으로 리다이렉트
  3. JWT가 있으면 대시보드 표시
  4. API 호출 시 JWT 우선 사용 (fallback: adminToken)
// JWT token check (Gateway 인증)
// localStorage에서 JWT 확인: 'auth_token' 또는 'token' 키 사용
const jwtToken = localStorage.getItem('auth_token') || localStorage.getItem('token');
let authToken = localStorage.getItem('adminToken'); // frontend-base 자체 인증 (fallback)

// JWT가 없으면 로그인 페이지로 리다이렉트
if (!jwtToken) {
    // 현재 URL 저장 (로그인 후 돌아오기 위해)
    const currentUrl = window.location.href;
    localStorage.setItem('admin_redirect_url', currentUrl);
    
    // auth.ro-being.com으로 리다이렉트
    window.location.href = 'https://auth.ro-being.com/login?redirect=' + encodeURIComponent(currentUrl);
    // 리다이렉트 후 실행 중단
    throw new Error('Redirecting to login');
}

// JWT가 있으면 대시보드 표시
if (authToken) {
    verifyToken();
} else {
    // JWT만 있고 frontend-base 자체 인증이 없으면 대시보드 표시
    showDashboard();
}

API 호출 함수 수정:

// API helper
async function apiCall(endpoint) {
    // JWT 우선 사용, 없으면 adminToken 사용 (fallback)
    const token = jwtToken || authToken;
    
    const response = await fetch(endpoint, {
        headers: {
            'Authorization': `Bearer ${token}`
        }
    });
    
    if (!response.ok) {
        // 401 에러 시 JWT 만료 또는 무효, 로그인 페이지로 리다이렉트
        if (response.status === 401) {
            localStorage.removeItem('auth_token');
            localStorage.removeItem('token');
            window.location.href = 'https://auth.ro-being.com/login?redirect=' + encodeURIComponent(window.location.href);
            throw new Error('Unauthorized - redirecting to login');
        }
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return await response.json();
}

3. 테스트 코드 작성

파일: /home/admin/frontend-base/tests/test_admin_api.py

테스트 케이스:

  1. Gateway /admin HTML 요청 - JWT 없이도 200 OK (HTML 반환)
  2. Gateway /admin/api/* 요청 - JWT 없이 401 반환
  3. frontend-base 직접 접근 - 정상 작동 확인

실행 결과:

=== Test 1: Gateway /admin HTML without JWT (예상: 200 OK) ===
Status: 200
Content-Type: text/html; charset=utf-8
✅ HTML 페이지 JWT 검증 없이 접근 가능 확인

=== Test 3: Gateway /admin/api/* without JWT (예상: 401) ===
Status: 401
✅ API 엔드포인트 JWT 검증 작동 확인

아키텍처 플로우

Before (401 에러)

사용자 → nginx → Gateway → JWT 검증 실패 → 401
(HTML도 받지 못함)

After (정상 동작)

1. HTML 요청:
   사용자 → nginx → Gateway → JWT 검증 없이 → frontend-base → HTML 반환 ✅

2. JavaScript 실행:
   - localStorage에서 JWT 확인
   - 없으면 → auth.ro-being.com/login 리다이렉트
   - 있으면 → 대시보드 표시

3. API 요청:
   사용자 → Gateway → JWT 검증 → robeing-monitor (51124:9024) ✅

검증

1. Gateway 상태

cd /home/admin/robeing-gateway
docker compose down && docker compose up -d --build
docker ps --filter "name=robeing-gateway"
# 출력: Up X seconds (healthy)

2. 엔드포인트 테스트

# HTML 요청 - JWT 없이도 200 OK
curl http://localhost:8100/admin
# <!DOCTYPE html>...

# API 요청 - JWT 없이 401
curl http://localhost:8100/admin/api/test
# {"detail":"Missing or invalid authorization header"}

# API 요청 - JWT 포함 시 정상
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8100/admin/api/test
# 정상 응답

3. 브라우저 테스트

  • URL: https://ro-being.com/admin
  • JWT 없이 접근:
    1. HTML 페이지 로드
    2. JavaScript 실행 → localStorage JWT 확인
    3. JWT 없음 → auth.ro-being.com/login 리다이렉트
  • JWT 있이 접근:
    1. HTML 페이지 로드
    2. JavaScript 실행 → localStorage JWT 확인
    3. JWT 있음 → 대시보드 표시
    4. API 호출 정상 작동

설계 원칙

보안과 사용성 균형

  • HTML 페이지: 공개 접근 가능 (JavaScript에서 보안 처리)
  • API 엔드포인트: JWT 검증 필수 (서버 측 보안)

역할 분리

  • Gateway: 조건부 JWT 검증 (HTML vs API 구분)
  • Frontend JavaScript: 클라이언트 측 인증 체크 및 리다이렉트
  • Auth Server: 중앙화된 인증 제공

사용자 경험

  • HTML은 즉시 로드되어 빠른 응답
  • JavaScript에서 JWT 확인 후 필요시 리다이렉트
  • 로그인 후 원래 페이지로 돌아오기 (redirect URL 저장)

관련 문서

  • 이전 구현: /home/admin/DOCS/journey/troubleshooting/251117_admin_dashboard_routing_implementation.md
  • Gateway 아키텍처: /home/admin/DOCS/book/300_architecture/gateway_proxy_patterns.md
  • 전체 시스템 구조: /home/admin/DOCS/book/300_architecture/310_전체_시스템_구조_컨테이너와_마이크로서비스.md

교훈

설계 검증의 중요성

  • HTML 페이지와 API 엔드포인트의 보안 요구사항이 다름을 인식
  • 모든 요청에 동일한 보안 정책 적용은 사용성을 해침

단계별 접근

  1. HTML 로드 (공개)
  2. JavaScript 실행 (클라이언트 측 보안)
  3. API 호출 (서버 측 보안)

TDD 접근

  • 테스트 코드 먼저 작성하여 요구사항 명확화
  • 구현 → 테스트 → 검증 순서 준수

다음 단계

  1. Gateway 라우팅 수정 완료
  2. Frontend JavaScript 수정 완료
  3. 테스트 코드 작성 완료
  4. 실제 배포 및 브라우저 테스트
  5. 로그인 후 redirect URL 처리 확인