From cf7440963a639515d6375b4df7d1f8e5e7aac182 Mon Sep 17 00:00:00 2001 From: happybell80 Date: Mon, 17 Nov 2025 15:31:15 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=82=A0=EC=A7=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(2025-01-17=20=E2=86=92=202025-11-17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...n_dashboard_jwt_verification_separation.md | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 journey/troubleshooting/250117_admin_dashboard_jwt_verification_separation.md diff --git a/journey/troubleshooting/250117_admin_dashboard_jwt_verification_separation.md b/journey/troubleshooting/250117_admin_dashboard_jwt_verification_separation.md new file mode 100644 index 0000000..120d470 --- /dev/null +++ b/journey/troubleshooting/250117_admin_dashboard_jwt_verification_separation.md @@ -0,0 +1,299 @@ +# 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 검증 수행 + +```python +@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) + +```javascript +// 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 호출 함수 수정**: +```javascript +// 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 상태 +```bash +cd /home/admin/robeing-gateway +docker compose down && docker compose up -d --build +docker ps --filter "name=robeing-gateway" +# 출력: Up X seconds (healthy) +``` + +### 2. 엔드포인트 테스트 +```bash +# HTML 요청 - JWT 없이도 200 OK +curl http://localhost:8100/admin +# ... + +# 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 처리 확인 +