feat(auth): 完成 AUTH-01~04 后端实现 - IP白名单中间件+mfa.py删除+agents.py重构+conftest修复

This commit is contained in:
Simon
2026-07-07 22:54:17 +08:00
parent fab75760e0
commit d56a9a6079
6 changed files with 153 additions and 587 deletions
+3 -172
View File
@@ -405,7 +405,7 @@ async def refresh_token(
)
logger.info(f"Token 刷新成功: employee_id={user_info.get('employee_id')}")
return success_response(data={"token": token, "expires_in": TOKEN_TTL_SECONDS})
return success_response(data={"token": token, "expires_in": TOKEN_TTL_SECONDS})
except json.JSONDecodeError:
pass
@@ -526,176 +526,7 @@ async def refresh_token_alias(
# --------------------------------------------------------------------------
# POST /api/auth_wecom/jsdk-login — 企微 JS-SDK 免认证登录 (v1.8 新增)
# 注意:/api/auth_wecom/jsdk-login 接口已按决策4删除
# 原功能为"企微免密登录",已按 PRD 要求移除
# --------------------------------------------------------------------------
# 流程:
# 1. 前端通过 wx.agentConfig 获取企微用户 userid
# 2. 前端调用本接口,传入 userid
# 3. 后端验证 userid 是否是坐席
# 4. 生成 token,返回给前端
# --------------------------------------------------------------------------
from pydantic import BaseModel, Field
class WecomJsdkLoginRequest(BaseModel):
"""企微 JS-SDK 免认证登录请求"""
userid: str = Field(..., description="企微用户 ID(从 wx.agentConfig 获取)")
login_source: str = Field(default="wecom_jsdk", description="登录来源标识")
@router.post("/jsdk-login")
async def wecom_jsdk_login(
body: WecomJsdkLoginRequest,
db: AsyncSession = Depends(get_db),
redis_client = Depends(get_redis),
):
"""企微 JS-SDK 免认证登录。
前端通过企微 JS-SDK (wx.agentConfig) 获取当前用户 ID
然后调用本接口进行免认证登录。
流程:
1. 调用 /wecom/check-role 接口验证 userid 是否是坐席
2. 查找或创建坐席记录
3. 生成 token,存入 Redis
4. 返回 token 和坐席信息
Args:
body: 包含企微 userid
db: 数据库会话
redis_client: Redis 客户端
Returns:
登录成功:{ code: 0, data: { token, employee_id, name, roles } }
"""
try:
# 1. 验证用户是否具有坐席或管理员角色
wecom_service = WecomService(redis_client)
userid = body.userid
# 从数据库查询用户角色(支持坐席和管理员)
from app.services.role_mapping_service import RoleMappingService
role_service = RoleMappingService(db)
user_roles = await role_service.get_user_roles(userid)
# 检查是否具有坐席或管理员角色
has_agent_role = "agent" in user_roles
has_admin_role = "admin" in user_roles
if not has_agent_role and not has_admin_role:
# 无坐席或管理员角色,尝试从企微标签检测(兼容旧逻辑)
tag_id = getattr(settings, "wecom_agent_tag_id", None)
if tag_id:
try:
access_token = await wecom_service.get_access_token()
url = f"https://qyapi.weixin.qq.com/cgi-bin/tag/get?access_token={access_token}&tagid={tag_id}"
import httpx
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(url)
result = resp.json()
if result.get("errcode", 0) == 0:
user_list = result.get("userlist", [])
user_ids = [
u if isinstance(u, str) else u.get("userid", "")
for u in user_list
]
if userid in user_ids:
has_agent_role = True
logger.info(f"企微 JS-SDK 免认证: userid={userid} 通过企微标签验证为坐席")
except Exception as e:
logger.warning(f"企微标签检测失败: {e}")
# 如果既没有坐席也没有管理员角色,拒绝登录
if not has_agent_role and not has_admin_role:
logger.warning(f"企微 JS-SDK 免认证失败: userid={userid} 没有坐席或管理员角色")
raise AppException(403, "您没有坐席或管理员权限,无法使用此方式登录")
# 确定用户角色
role_names = []
if has_admin_role:
role_names.append("admin")
if has_agent_role:
role_names.append("agent")
logger.info(f"企微 JS-SDK 免认证: userid={userid}, roles={role_names}")
# 3. 获取用户详细信息
try:
user_info = await wecom_service.get_user_info(userid)
user_name = user_info.get("name", userid)
# 同步头像到 employee 表 + 清缓存(要求 A;不阻塞登录)
try:
from app.services.avatar_service import sync_employee_avatar
await sync_employee_avatar(db, redis_client, userid, user_info.get("avatar", ""))
except Exception as av_err:
logger.warning(f"JS-SDK 同步头像失败(不阻塞): userid={userid}, error={av_err}")
except Exception as e:
logger.warning(f"获取企微用户信息失败: {e}")
user_name = userid
# 4. 查找或创建坐席记录
from app.models.agent import Agent
stmt = select(Agent).where(Agent.user_id == userid)
result = await db.execute(stmt)
agent = result.scalars().first()
if not agent:
# 首次登录,创建坐席记录
agent = Agent(
user_id=userid,
name=user_name,
status="online",
current_load=0,
max_load=5,
)
db.add(agent)
await db.flush()
logger.info(f"企微 JS-SDK 免认证创建坐席: user_id={userid}, name={user_name}")
else:
# 更新坐席状态
agent.name = user_name
agent.status = "online"
agent.updated_at = datetime.now()
db.add(agent)
await db.flush()
logger.info(f"企微 JS-SDK 免认证登录: user_id={userid}, name={user_name}")
# 5. 生成 token
token = secrets.token_urlsafe(32)
# 6. 存储 token 到 Redis
token_key = f"agent:token:{token}"
await redis_client.setex(token_key, TOKEN_TTL_SECONDS, userid)
# 7. 记录审计日志
await record_audit_log(
db=db,
employee_id=userid,
action="wecom_jsdk_login",
resource="auth",
resource_id=userid,
details={
"name": user_name,
"roles": role_names,
"login_method": "wecom_jsdk",
},
result="success",
)
await db.commit()
logger.info(f"企微 JS-SDK 免认证登录成功: user_id={userid}, roles={role_names}")
return success_response(data={
"token": token,
"employee_id": userid,
"name": user_name,
"roles": role_names,
})
except AppException:
raise
except Exception as e:
logger.error(f"企微 JS-SDK 免认证登录异常: {e}", exc_info=True)
raise AppException(500, f"免认证登录失败: {str(e)}")