P0安全修复: WS token改subprotocol + nginx日志关闭 + 类型修复 + 降级验证 + 依赖
This commit is contained in:
@@ -21,7 +21,7 @@ from uuid import UUID
|
||||
import pyotp
|
||||
import qrcode
|
||||
import redis.asyncio as aioredis
|
||||
from passlib.hash import bcrypt
|
||||
import bcrypt # P1 修复: 直接使用 bcrypt 库替代 passlib
|
||||
from fastapi import APIRouter, Depends, Header, Query, Request
|
||||
from pydantic import BaseModel, Field
|
||||
from slowapi import Limiter
|
||||
@@ -217,13 +217,19 @@ async def agent_login(
|
||||
logger.warning(
|
||||
f"企微API不可达,已注册坐席降级放行: user_id={body.user_id}"
|
||||
)
|
||||
# P1 修复: 降级放行时,如果 agent 有 password_hash 则必须验证本地密码
|
||||
if existing_agent and existing_agent.password_hash:
|
||||
if not body.password:
|
||||
raise AppException(1011, "请输入本地密码")
|
||||
if not bcrypt.checkpw(body.password.encode('utf-8'), existing_agent.password_hash.encode('utf-8')):
|
||||
raise AppException(1011, "本地密码错误")
|
||||
|
||||
# P0-#5: 本地密码认证(企微验证失败时的备用认证)
|
||||
# 检查是否需要本地密码验证
|
||||
local_password_verified = False
|
||||
if body.password and agent and agent.password_hash:
|
||||
# 验证本地密码
|
||||
if bcrypt.verify(body.password, agent.password_hash):
|
||||
if bcrypt.checkpw(body.password.encode('utf-8'), agent.password_hash.encode('utf-8')):
|
||||
local_password_verified = True
|
||||
logger.info(f"本地密码验证通过: user_id={body.user_id}")
|
||||
else:
|
||||
@@ -566,11 +572,11 @@ async def update_agent_password(
|
||||
if agent.password_hash:
|
||||
if not body.old_password:
|
||||
raise AppException(1012, "请输入旧密码")
|
||||
if not bcrypt.verify(body.old_password, agent.password_hash):
|
||||
if not bcrypt.checkpw(body.old_password.encode('utf-8'), agent.password_hash.encode('utf-8')):
|
||||
raise AppException(1013, "旧密码错误")
|
||||
|
||||
# 设置新密码
|
||||
agent.password_hash = bcrypt.hash(body.new_password)
|
||||
agent.password_hash = bcrypt.hashpw(body.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
agent.updated_at = datetime.now()
|
||||
db.add(agent)
|
||||
await db.flush()
|
||||
|
||||
+28
-16
@@ -67,17 +67,24 @@ async def websocket_endpoint(
|
||||
request: Starlette Request(用于获取 header)
|
||||
"""
|
||||
# ======================================================================
|
||||
# WS-01: Token 认证(从 header 或 query 获取)
|
||||
# WS-01: Token 认证(从 subprotocol / header / query 获取)
|
||||
# ======================================================================
|
||||
|
||||
# 步骤1: 优先从 Authorization header 获取 token,其次从 query(向后兼容)
|
||||
# 格式: Authorization: Bearer {token}
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:] # 去掉 "Bearer " 前缀
|
||||
# 步骤1: 优先从 Sec-WebSocket-Protocol (subprotocol) 获取 token,其次从 Authorization header,最后从 query(向后兼容)
|
||||
# 格式: Sec-WebSocket-Protocol: bearer.{token}
|
||||
# 说明: 浏览器原生 WebSocket API 不支持 headers 参数,但支持 subprotocols (第2参数数组)
|
||||
# 前端用 new WebSocket(url, ["bearer.{token}"]) 传递,服务端从 sec-websocket-protocol 头读取
|
||||
subprotocol = request.headers.get("sec-websocket-protocol", "")
|
||||
if subprotocol.startswith("bearer."):
|
||||
token = subprotocol[7:] # 去掉 "bearer." 前缀
|
||||
else:
|
||||
# 向后兼容:从 query param 获取(即将废弃)
|
||||
token = request.query_params.get("token", "")
|
||||
# 其次从 Authorization header 获取
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:] # 去掉 "Bearer " 前缀
|
||||
else:
|
||||
# 向后兼容:从 query param 获取(即将废弃)
|
||||
token = request.query_params.get("token", "")
|
||||
|
||||
# 步骤2: 检查 token 是否为空
|
||||
if not token:
|
||||
@@ -222,17 +229,22 @@ async def h5_websocket_endpoint(
|
||||
request: Starlette Request(用于获取 header)
|
||||
"""
|
||||
# ======================================================================
|
||||
# Token 认证(从 header 或 query 获取)
|
||||
# Token 认证(从 subprotocol / header / query 获取)
|
||||
# ======================================================================
|
||||
|
||||
# 步骤1: 优先从 Authorization header 获取 token,其次从 query(向后兼容)
|
||||
# 格式: Authorization: Bearer {token}
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:] # 去掉 "Bearer " 前缀
|
||||
# 步骤1: 优先从 Sec-WebSocket-Protocol (subprotocol) 获取 token,其次从 Authorization header,最后从 query(向后兼容)
|
||||
# 格式: Sec-WebSocket-Protocol: bearer.{token}
|
||||
subprotocol = request.headers.get("sec-websocket-protocol", "")
|
||||
if subprotocol.startswith("bearer."):
|
||||
token = subprotocol[7:] # 去掉 "bearer." 前缀
|
||||
else:
|
||||
# 向后兼容:从 query param 获取(即将废弃)
|
||||
token = request.query_params.get("token", "")
|
||||
# 其次从 Authorization header 获取
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:] # 去掉 "Bearer " 前缀
|
||||
else:
|
||||
# 向后兼容:从 query param 获取(即将废弃)
|
||||
token = request.query_params.get("token", "")
|
||||
|
||||
# 步骤2: 检查 token 是否为空
|
||||
if not token:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import DateTime, Integer, JSON, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
@@ -141,7 +142,8 @@ class Agent(Base):
|
||||
# 本地密码哈希(可选,用于本地密码认证)
|
||||
# 使用 bcrypt 加密存储,不存储明文密码
|
||||
# 当企微验证不可用时,可作为备用认证方式
|
||||
password_hash: Mapped[str] = mapped_column(
|
||||
# P1 修复: Mapped[Optional[str]] 解决严格模式下 None 赋值报错
|
||||
password_hash: Mapped[Optional[str]] = mapped_column(
|
||||
String(128),
|
||||
nullable=True,
|
||||
default=None,
|
||||
|
||||
Reference in New Issue
Block a user