安全修复: - WebSocket 端点添加认证(cookie/token),防止未授权窃听日志 - SPA serve_spa 添加路径遍历防护(resolve + relative_to 检查) - Token 改用 httpOnly Cookie 存储,移除前端 localStorage token(防 XSS 窃取) - 添加安全响应头中间件(X-Content-Type-Options/X-Frame-Options/Referrer-Policy) - HTTP 请求日志脱敏请求体中的 password/secret/token 等敏感字段 - 权限检查统一使用 user_has_permission(考虑自定义权限,修复 has_permission 忽略 custom_permissions 的缺陷) 性能与稳定性: - cookies.py 列表接口修复 N+1 查询(改为批量查询 Account) - login_service.py run() 结束时关闭 DB Session(防止连接泄漏) - _active_batches/_active_tests 全局字典添加 threading.Lock(防止并发竞态) 配置优化: - CORS 源支持环境变量 CORS_ORIGINS 配置 - Uvicorn reload 支持环境变量 UVICORN_RELOAD 控制(生产环境默认关闭) - Cookie 安全标志支持环境变量 COOKIE_SECURE 配置(HTTPS 部署时启用) - logs.py 权限不足返回 HTTP 403(原来返回 200 + message)
81 lines
2.7 KiB
Python
81 lines
2.7 KiB
Python
"""FastAPI 依赖注入"""
|
|
|
|
from typing import Optional
|
|
from fastapi import Depends, HTTPException, Request, status, WebSocket
|
|
from fastapi.security import OAuth2PasswordBearer
|
|
from sqlalchemy.orm import Session
|
|
from jose import JWTError
|
|
|
|
from .database import get_db, SessionLocal
|
|
from .security import decode_access_token
|
|
from .models import User
|
|
from .permissions import get_user_permissions
|
|
|
|
# auto_error=False: 允许 token 为空(后续从 cookie 读取)
|
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
|
|
|
|
|
|
def get_current_user(
|
|
request: Request,
|
|
token: Optional[str] = Depends(oauth2_scheme),
|
|
db: Session = Depends(get_db),
|
|
) -> User:
|
|
credentials_exc = HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="无效的认证凭据",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
# 优先从 Authorization Bearer header 读取,回退到 httpOnly cookie
|
|
if not token:
|
|
token = request.cookies.get("access_token")
|
|
if not token:
|
|
raise credentials_exc
|
|
try:
|
|
payload = decode_access_token(token)
|
|
if payload is None:
|
|
raise credentials_exc
|
|
user_id: int = payload.get("sub")
|
|
if user_id is None:
|
|
raise credentials_exc
|
|
except JWTError:
|
|
raise credentials_exc
|
|
|
|
user = db.query(User).filter(User.id == int(user_id)).first()
|
|
if user is None or not user.is_active:
|
|
raise credentials_exc
|
|
return user
|
|
|
|
|
|
def require_permission(permission: str):
|
|
"""权限检查依赖工厂。用法: Depends(require_permission('user:create'))"""
|
|
def checker(current_user: User = Depends(get_current_user)) -> User:
|
|
if not current_user.is_active:
|
|
raise HTTPException(status_code=403, detail="账号已禁用")
|
|
perms = get_user_permissions(current_user)
|
|
if permission not in perms:
|
|
raise HTTPException(status_code=403, detail=f"无权限: {permission}")
|
|
return current_user
|
|
return checker
|
|
|
|
|
|
def authenticate_websocket(websocket: WebSocket) -> Optional[User]:
|
|
"""WebSocket 认证:从 cookie 或 query param token 中验证用户身份。
|
|
|
|
Returns:
|
|
User 如果认证成功,None 如果失败。
|
|
"""
|
|
token = websocket.cookies.get("access_token") or websocket.query_params.get("token")
|
|
if not token:
|
|
return None
|
|
payload = decode_access_token(token)
|
|
if not payload or not payload.get("sub"):
|
|
return None
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.query(User).filter(User.id == int(payload["sub"])).first()
|
|
if user and user.is_active:
|
|
return user
|
|
return None
|
|
finally:
|
|
db.close()
|