From 8c54a38f558d5ee3ce45b1cc1feffdcf6bbaa02c Mon Sep 17 00:00:00 2001 From: duxingchen Date: Wed, 12 Aug 2026 12:03:12 +0800 Subject: [PATCH] =?UTF-8?q?security:=20API=E9=89=B4=E6=9D=83=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=20+=20SQL=E6=8B=BC=E6=8E=A5=E9=9A=90=E6=82=A3?= =?UTF-8?q?=E6=B6=88=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. materials.py - get_material_groups和get_material_items补全Depends(get_current_user) - 移除TYPE_FILTER="1=1"死代码及4处f-string SQL拼接 - 全部SQL改为纯参数化text()查询 2. notifications.py - list_notifications废弃user_id查询参数(越权漏洞) - user_id强制从JWT Token解析,防止篡改参数偷看他人通知 - mark_notification_read补全鉴权 --- backend/app/api/v1/endpoints/materials.py | 36 +++++++------------ backend/app/api/v1/endpoints/notifications.py | 13 +++++-- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/backend/app/api/v1/endpoints/materials.py b/backend/app/api/v1/endpoints/materials.py index 2c62731..17971c6 100644 --- a/backend/app/api/v1/endpoints/materials.py +++ b/backend/app/api/v1/endpoints/materials.py @@ -1,14 +1,12 @@ """物料选择器 — 读 MOM material_base,按成品/半成品 category 手风琴分组""" -from fastapi import APIRouter, Query, HTTPException, status +from fastapi import APIRouter, Query, HTTPException, status, Depends from pydantic import BaseModel from app.core.mom_database import MomSessionLocal +from app.services.auth_service import get_current_user from sqlalchemy import text router = APIRouter(prefix="/materials", tags=["物料选择"]) -# 全量展示全部物料类别 -TYPE_FILTER = "1=1" - # ============================================================ # 响应模型 @@ -36,6 +34,7 @@ class MaterialItem(BaseModel): @router.get("/groups", response_model=list[MaterialGroup]) def get_material_groups( keyword: str = Query("", description="搜索(按名称/规格)"), + current_user: dict = Depends(get_current_user), ): """ 按 category 分组汇总,前端渲染手风琴外层。 @@ -44,29 +43,23 @@ def get_material_groups( db = MomSessionLocal() try: if keyword.strip(): - sql = text( - f""" + sql = text(""" SELECT category, COUNT(*) AS count FROM material_base WHERE is_enabled = TRUE - AND ({TYPE_FILTER}) AND (name ILIKE :kw OR spec_model ILIKE :kw) GROUP BY category ORDER BY category - """ - ) + """) result = db.execute(sql, {"kw": f"%{keyword.strip()}%"}) else: - sql = text( - f""" + sql = text(""" SELECT category, COUNT(*) AS count FROM material_base WHERE is_enabled = TRUE - AND ({TYPE_FILTER}) GROUP BY category ORDER BY category - """ - ) + """) result = db.execute(sql) rows = result.fetchall() @@ -85,6 +78,7 @@ def get_material_items( category: str = Query(..., description="物料分类"), keyword: str = Query("", description="分组内搜索"), limit: int = Query(500, ge=1, le=9999), + current_user: dict = Depends(get_current_user), ): """ 获取指定 category 下的物料条目,前端展开手风琴时懒加载。 @@ -92,35 +86,29 @@ def get_material_items( db = MomSessionLocal() try: if keyword.strip(): - sql = text( - f""" + sql = text(""" SELECT id, name, spec_model AS spec, category, material_type AS type, COALESCE(unit, '') AS unit, is_enabled FROM material_base WHERE is_enabled = TRUE - AND ({TYPE_FILTER}) AND category = :cat AND (name ILIKE :kw OR spec_model ILIKE :kw) ORDER BY name LIMIT :lim - """ - ) + """) result = db.execute( sql, {"cat": category, "kw": f"%{keyword.strip()}%", "lim": limit} ) else: - sql = text( - f""" + sql = text(""" SELECT id, name, spec_model AS spec, category, material_type AS type, COALESCE(unit, '') AS unit, is_enabled FROM material_base WHERE is_enabled = TRUE - AND ({TYPE_FILTER}) AND category = :cat ORDER BY name LIMIT :lim - """ - ) + """) result = db.execute(sql, {"cat": category, "lim": limit}) rows = result.fetchall() diff --git a/backend/app/api/v1/endpoints/notifications.py b/backend/app/api/v1/endpoints/notifications.py index f405e02..3a96ae7 100644 --- a/backend/app/api/v1/endpoints/notifications.py +++ b/backend/app/api/v1/endpoints/notifications.py @@ -11,18 +11,26 @@ from app.models.notification import Notification from app.models.task import Task from app.models.product import Product from app.schemas.notification import NotificationResponse, NotificationListResponse +from app.services.auth_service import get_current_user router = APIRouter(prefix="/notifications", tags=["消息通知"]) @router.get("/", response_model=NotificationListResponse) async def list_notifications( - user_id: str = Query(..., description="当前用户ID"), skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), + current_user: dict = Depends(get_current_user), ): - """获取当前用户的通知列表(按时间倒序)""" + """ + 获取当前用户的通知列表(按时间倒序)。 + + 安全:user_id 强制从 JWT Token 解析,不接受查询参数, + 杜绝通过篡改 user_id 参数越权查看他人通知。 + """ + user_id: str = current_user.get("username", "") or current_user.get("sub", "") + # 总数 count_stmt = select(func.count()).select_from(Notification).where( Notification.user_id == user_id @@ -80,6 +88,7 @@ async def list_notifications( async def mark_notification_read( notification_id: str, db: AsyncSession = Depends(get_db), + current_user: dict = Depends(get_current_user), ): """标记单条通知为已读""" nid = uuid.UUID(notification_id)