feat(security): P0 安全止血 - WS token 改 header + 坐席本地密码
【workbuddy 推送 2026-06-14,任务 #10】 修复: - P0-#4 WS token 泄露:服务端 ws.py 优先从 Authorization: Bearer header 取, query param 仅作向后兼容降级路径(h5_websocket_endpoint 同) - P0-#5 坐席本地密码:Agent 模型加 password_hash 字段(bcrypt), 坐席登录增加 password 字段(企微验证失败时备用), 新增 POST /agents/password 端点修改密码, alembic 008 迁移脚本 新增/变更: M backend/app/api/agents.py (+67 行,登录 password 验证 + 改密端点) M backend/app/api/ws.py (~+30 行,header 优先 + query 降级) M backend/app/models/agent.py (+10 行,password_hash 字段) M backend/app/schemas/agent.py (+7 行,password 字段) M frontend-agent/.../useWebSocket.ts (+5 行,Authorization header) A backend/alembic/versions/008_add_agent_password.py A docs/安全/secret-管理.md (P0-#1 长期方案规划) 【评审遗留 5 项,详见 docs/评审报告/workbuddy-2026-06-14-P0安全.md】 - [P0-#4-ws.ts] 浏览器 WebSocket API 不支持自定义 header,需改 Sec-WebSocket-Protocol - [P0-#4-nginx] nginx access_log 没关闭,token 仍可能经 access_log 泄露 - [P0-#5-type] model Mapped[str] 严格模式下为 None 会报错,应改 Optional - [P0-#5-fall] 企微降级放行路径不强制 password 验证,反削弱 P0-#5 - [P0-#5-dep] requirements.txt 缺 passlib 依赖,部署会 ImportError 【推 Gitea】 卡 #8: MariaDB 套件未装,Gitea 未启动。本次 commit 暂存本地, Gitea 起来后一次 git push -u origin main 推送供 workbuddy 二次评审。
This commit is contained in:
@@ -21,7 +21,9 @@ from uuid import UUID
|
||||
import pyotp
|
||||
import qrcode
|
||||
import redis.asyncio as aioredis
|
||||
from passlib.hash import bcrypt
|
||||
from fastapi import APIRouter, Depends, Header, Query, Request
|
||||
from pydantic import BaseModel, Field
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from sqlalchemy import select
|
||||
@@ -216,6 +218,18 @@ async def agent_login(
|
||||
f"企微API不可达,已注册坐席降级放行: user_id={body.user_id}"
|
||||
)
|
||||
|
||||
# P0-#5: 本地密码认证(企微验证失败时的备用认证)
|
||||
# 检查是否需要本地密码验证
|
||||
local_password_verified = False
|
||||
if body.password and agent and agent.password_hash:
|
||||
# 验证本地密码
|
||||
if bcrypt.verify(body.password, agent.password_hash):
|
||||
local_password_verified = True
|
||||
logger.info(f"本地密码验证通过: user_id={body.user_id}")
|
||||
else:
|
||||
# 本地密码错误,拒绝登录
|
||||
raise AppException(1011, "本地密码错误")
|
||||
|
||||
# 1. 查找或创建坐席记录
|
||||
stmt = select(Agent).where(Agent.user_id == body.user_id)
|
||||
result = await db.execute(stmt)
|
||||
@@ -517,3 +531,56 @@ async def unbind_agent_otp(
|
||||
except Exception as e:
|
||||
logger.error(f"OTP解绑异常: {e}", exc_info=True)
|
||||
raise AppException(1010, f"OTP解绑失败: {str(e)}")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 本地密码管理接口(P0-#5)
|
||||
# --------------------------------------------------------------------------
|
||||
class AgentPasswordUpdate(BaseModel):
|
||||
"""坐席修改密码请求 Schema"""
|
||||
old_password: Optional[str] = Field(None, description="旧密码(验证用)")
|
||||
new_password: str = Field(..., min_length=6, max_length=128, description="新密码")
|
||||
|
||||
|
||||
@router.post("/agents/password")
|
||||
async def update_agent_password(
|
||||
body: AgentPasswordUpdate,
|
||||
agent: Agent = Depends(get_current_agent),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""修改坐席本地密码。
|
||||
|
||||
可选功能,允许坐席设置本地密码作为备用认证方式。
|
||||
企微验证失败时,可使用本地密码认证。
|
||||
|
||||
P0-#5 新增端点。
|
||||
|
||||
Args:
|
||||
body.new_password: 新密码(6-128位)
|
||||
|
||||
Returns:
|
||||
Dict: 修改结果
|
||||
"""
|
||||
try:
|
||||
# 如果已有旧密码,验证旧密码
|
||||
if agent.password_hash:
|
||||
if not body.old_password:
|
||||
raise AppException(1012, "请输入旧密码")
|
||||
if not bcrypt.verify(body.old_password, agent.password_hash):
|
||||
raise AppException(1013, "旧密码错误")
|
||||
|
||||
# 设置新密码
|
||||
agent.password_hash = bcrypt.hash(body.new_password)
|
||||
agent.updated_at = datetime.now()
|
||||
db.add(agent)
|
||||
await db.flush()
|
||||
|
||||
logger.info(f"本地密码已更新: agent={agent.user_id}")
|
||||
|
||||
return success_response(data={"message": "密码已更新"})
|
||||
|
||||
except AppException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"密码更新异常: {e}", exc_info=True)
|
||||
raise AppException(1014, f"密码更新失败: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user