From 51084de7e1d62022f95df4ba3ea7e68b8c566c5c Mon Sep 17 00:00:00 2001 From: Nixevol Date: Wed, 22 Apr 2026 15:54:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0JWT=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E5=B9=B6=E5=85=B3=E9=97=AD=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 62 +++++++++++++++++++++++++++++++++++++++-- docs/project_context.md | 16 +++++++++++ static/index.html | 15 ++++++++++ static/js/app.js | 57 +++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 docs/project_context.md diff --git a/app/main.py b/app/main.py index 592c526..c956bcb 100644 --- a/app/main.py +++ b/app/main.py @@ -15,10 +15,39 @@ from datetime import datetime from typing import Any, Dict, List, Optional from threading import Thread -from fastapi import FastAPI, File, UploadFile, HTTPException, Query, Body +from fastapi import FastAPI, File, UploadFile, HTTPException, Query, Body, Request from fastapi.staticfiles import StaticFiles -from fastapi.responses import HTMLResponse, FileResponse +from fastapi.responses import HTMLResponse, FileResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware +import base64 +import hmac +import hashlib +import time + +SECRET_KEY = "CapaReportSecretKey2026" +ADMIN_PASSWORD = "admin" # 默认管理密码 + +def create_jwt_token(data: dict, expires_in: int = 86400 * 30) -> str: + header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()).decode().rstrip("=") + payload_data = data.copy() + payload_data["exp"] = int(time.time()) + expires_in + payload = base64.urlsafe_b64encode(json.dumps(payload_data).encode()).decode().rstrip("=") + signature = base64.urlsafe_b64encode(hmac.new(SECRET_KEY.encode(), f"{header}.{payload}".encode(), hashlib.sha256).digest()).decode().rstrip("=") + return f"{header}.{payload}.{signature}" + +def verify_jwt_token(token: str) -> dict: + try: + header, payload, signature = token.split(".") + expected_sig = base64.urlsafe_b64encode(hmac.new(SECRET_KEY.encode(), f"{header}.{payload}".encode(), hashlib.sha256).digest()).decode().rstrip("=") + if not hmac.compare_digest(signature, expected_sig): + return None + payload_padded = payload + "=" * ((4 - len(payload) % 4) % 4) + data = json.loads(base64.urlsafe_b64decode(payload_padded).decode()) + if "exp" in data and data["exp"] < int(time.time()): + return None + return data + except Exception: + return None from app.config import AppConfig, CACHE_DIR, BASE_DIR from app.database import DatabaseManager @@ -30,7 +59,10 @@ from app.history import HistoryManager app = FastAPI( title="CapacityReport", description="容量报表数据处理系统", - version="2.0.1" + version="2.0.1", + docs_url=None, + redoc_url=None, + openapi_url=None ) # CORS 配置 @@ -61,6 +93,30 @@ global_task_lock: Dict[str, Any] = { } +# ==================== 认证中间件 ==================== + +@app.middleware("http") +async def jwt_middleware(request: Request, call_next): + path = request.url.path + if path.startswith("/api/") and path != "/api/login": + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + return JSONResponse(status_code=401, content={"detail": "未授权,请提供有效的Token"}) + token = auth_header.split(" ")[1] + payload = verify_jwt_token(token) + if not payload: + return JSONResponse(status_code=401, content={"detail": "Token无效或已过期"}) + return await call_next(request) + + +@app.post("/api/login") +async def login(password: str = Body(..., embed=True)): + if password != ADMIN_PASSWORD: + return JSONResponse(status_code=401, content={"detail": "密码错误"}) + token = create_jwt_token({"user": "admin"}) + return {"success": True, "token": token} + + # ==================== 健康检查 ==================== @app.get("/health") diff --git a/docs/project_context.md b/docs/project_context.md new file mode 100644 index 0000000..4bb1694 --- /dev/null +++ b/docs/project_context.md @@ -0,0 +1,16 @@ +# 项目上下文记忆 (Project Context) + +## 最近更新记录 + +### 2026-04-22: 增加 JWT 鉴权和接口安全控制 +- **问题背景**: 网管部门通报安全问题,扫描到项目存在暴露的 API 文档(/docs, /redoc, /openapi.json),并且 API 接口没有使用授权控制,要求快速增加鉴权。 +- **架构变更**: + - FastAPI 初始化时关闭 `docs_url`、`redoc_url` 和 `openapi_url`。 + - 由于依赖环境限制(`uv` 虚拟环境被破坏),为实现快速且无额外依赖的 JWT 方案,在 `app/main.py` 中自行使用 Python 标准库 `hmac`、`hashlib`、`base64` 实现了原生的 JWT `create_jwt_token` 和 `verify_jwt_token`。 + - 在 `app/main.py` 增加了一个登录接口 `/api/login`,密码验证通过后下发 token。当前密码硬编码为 `admin`。 + - 添加了全局路由中间件 `@app.middleware("http") jwt_middleware`,拦截所有 `/api/` 路由(除登录外),验证请求头 `Authorization: Bearer ` 是否合法或过期。 +- **前端适配**: + - `static/index.html` 增加了 `loginModal`(系统登录弹出框)。 + - `static/js/app.js` 的 `api` 统一调用封装修改,支持在请求头附带 token;同时增加对 401 状态码的拦截,一旦失效将移除 token 并在页面弹出 `showLoginModal`。 + - `static/js/app.js` 在处理 XHR 文件上传时,一并增加了 token 的拼装及 401 处理。 +- **经验教训**: 在部署内网或暴露环境前,快速关闭 Swagger OpenAPI 的自带页面十分重要。为了绕过环境管理工具或网络导致库安装失败,通过原生标准库提供足够安全的 JWT 校验能极大提升应急处理效率。 diff --git a/static/index.html b/static/index.html index 722305d..e75a667 100644 --- a/static/index.html +++ b/static/index.html @@ -490,6 +490,21 @@ + + +