fix: harden authentication system (JWT, cookies, rate limiting, password policy)

- Replace hardcoded JWT secret with randomly generated key persisted to file
- Replace hardcoded default password with random password shown in logs
- Migrate token storage from localStorage to HttpOnly SameSite=strict cookie
- Add IP-based login rate limiter (5 attempts / 15 min, 429 on lockout)
- Add token_version for JWT revocation on password change
- Add password strength validation (min 6 chars, 3+ unique characters)
- Inject decoded user payload into request.state.user in auth middleware
- Add /api/auth/me and /api/auth/logout endpoints
- Narrow auth middleware exception handling (JWTError only, not all Exception)
- Update updated_at timestamp on password change
- Remove localStorage token management from frontend (axios, router, store)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
祀梦
2026-05-17 15:54:45 +08:00
parent 1047bcece9
commit 9d4d869d57
13 changed files with 249 additions and 75 deletions

View File

@@ -7,10 +7,12 @@ import time
import json
from app.config import CORS_ORIGINS, WEBUI_PATH, HOST, PORT
from app.database import init_db
from app.database import init_db, SessionLocal
from app.models.user_settings import UserSettings
from app.routers import api_router
from app.utils.logger import logger
from app.utils.auth import decode_access_token
from jose import JWTError
@asynccontextmanager
@@ -90,25 +92,35 @@ async def log_requests(request: Request, call_next):
return response
# 认证中间件(保护所有 /api/* 路由,除了 /api/auth/* 和 /health
# 认证中间件(保护 /api/*,仅放行 /health 和 /api/auth/login、/api/auth/logout
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
path = request.url.path
# 不拦截健康检查、静态文件、auth 路由
if path == "/health" or not path.startswith("/api/") or path.startswith("/api/auth/"):
# 不拦截:健康检查、静态文件、公开的 auth 端点
public_paths = {"/health", "/api/auth/login", "/api/auth/logout"}
if path in public_paths or not path.startswith("/api/"):
return await call_next(request)
auth_header = request.headers.get("Authorization", "")
token = auth_header.replace("Bearer ", "")
token = request.cookies.get("access_token", "")
if not token:
return JSONResponse(status_code=401, content={"detail": "未登录"})
try:
decode_access_token(token)
except Exception:
payload = decode_access_token(token)
except JWTError:
return JSONResponse(status_code=401, content={"detail": "登录已过期,请重新登录"})
db = SessionLocal()
try:
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
if settings and payload.get("tv") != settings.token_version:
return JSONResponse(status_code=401, content={"detail": "密码已修改,请重新登录"})
finally:
db.close()
request.state.user = payload
return await call_next(request)