feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 统一认证服务
|
||||
# =============================================================================
|
||||
# 说明:统一认证核心逻辑,支持:
|
||||
# 1. Token 创建、验证、刷新、失效
|
||||
# 2. 登录日志记录
|
||||
# 3. 角色切换
|
||||
# 4. 登出功能(Token 黑名单)
|
||||
# 三端认证重构:取消OTP/账号密码登录,统一为企微扫码认证
|
||||
# =============================================================================
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.login_log import LoginLog
|
||||
from app.services.token_service import TokenService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Token TTL(8小时)
|
||||
TOKEN_TTL_SECONDS = 8 * 60 * 60
|
||||
|
||||
# Token 黑名单 Key 前缀
|
||||
TOKEN_BLACKLIST_PREFIX = "token:blacklist:"
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""统一认证服务。
|
||||
|
||||
管理用户认证的完整流程,包括 Token 管理和登录日志。
|
||||
"""
|
||||
|
||||
def __init__(self, redis_client: aioredis.Redis, db: Optional[AsyncSession] = None):
|
||||
"""初始化认证服务。
|
||||
|
||||
Args:
|
||||
redis_client: Redis 异步客户端
|
||||
db: 数据库会话(可选,用于记录登录日志)
|
||||
"""
|
||||
self.redis = redis_client
|
||||
self.db = db
|
||||
self.token_service = TokenService(redis_client)
|
||||
|
||||
async def create_token_and_login(
|
||||
self,
|
||||
employee_id: str,
|
||||
name: str,
|
||||
roles: List[str],
|
||||
corp_id: str,
|
||||
login_method: str,
|
||||
login_source: str,
|
||||
department: Optional[str] = None,
|
||||
avatar: Optional[str] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""创建 Token 并记录登录日志。
|
||||
|
||||
Args:
|
||||
employee_id: 企微 UserID
|
||||
name: 用户姓名
|
||||
roles: 角色列表
|
||||
corp_id: 企业ID
|
||||
login_method: 登录方式 (oauth/qrcode/bind)
|
||||
login_source: 登录来源 (h5/agent/admin)
|
||||
department: 部门(可选)
|
||||
avatar: 头像URL(可选)
|
||||
ip_address: 客户端IP(可选)
|
||||
user_agent: 客户端User-Agent(可选)
|
||||
|
||||
Returns:
|
||||
Dict: 包含 token 和用户信息的字典
|
||||
"""
|
||||
# 创建 Token
|
||||
token = await self.token_service.create_token(
|
||||
employee_id=employee_id,
|
||||
name=name,
|
||||
roles=roles,
|
||||
department=department,
|
||||
avatar=avatar,
|
||||
login_source=login_source,
|
||||
)
|
||||
|
||||
# 记录登录日志
|
||||
await self._record_login_log(
|
||||
employee_id=employee_id,
|
||||
corp_id=corp_id,
|
||||
login_method=login_method,
|
||||
login_source=login_source,
|
||||
status="success",
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
|
||||
# 获取当前角色
|
||||
current_role = self.token_service._get_default_role(roles)
|
||||
|
||||
logger.info(
|
||||
f"用户登录成功: employee_id={employee_id}, "
|
||||
f"login_method={login_method}, login_source={login_source}"
|
||||
)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"employee_id": employee_id,
|
||||
"name": name,
|
||||
"avatar": avatar or "",
|
||||
"department": department or "",
|
||||
"roles": roles,
|
||||
"current_role": current_role,
|
||||
"login_source": login_source,
|
||||
"expires_in": TOKEN_TTL_SECONDS,
|
||||
}
|
||||
|
||||
async def verify_token(self, token: str) -> Optional[Dict]:
|
||||
"""验证 Token 并返回用户信息。
|
||||
|
||||
Args:
|
||||
token: Token 字符串
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: 用户信息,如果 Token 无效返回 None
|
||||
"""
|
||||
# 检查是否在黑名单中
|
||||
is_blacklisted = await self._is_token_blacklisted(token)
|
||||
if is_blacklisted:
|
||||
logger.warning(f"Token 在黑名单中: {token[:10]}...")
|
||||
return None
|
||||
|
||||
# 验证 Token
|
||||
user_info = await self.token_service.get_user_info(token)
|
||||
return user_info
|
||||
|
||||
async def logout(self, token: str) -> bool:
|
||||
"""登出:将 Token 加入黑名单。
|
||||
|
||||
Args:
|
||||
token: 要失效的 Token 字符串
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
# 将 Token 加入黑名单
|
||||
await self._add_to_blacklist(token)
|
||||
|
||||
# 使 Redis 中的 Token 失效
|
||||
await self.token_service.invalidate_token(token)
|
||||
|
||||
logger.info(f"用户登出: token={token[:10]}...")
|
||||
return True
|
||||
|
||||
async def switch_role(self, token: str, new_role: str) -> bool:
|
||||
"""切换当前角色。
|
||||
|
||||
Args:
|
||||
token: Token 字符串
|
||||
new_role: 目标角色标识
|
||||
|
||||
Returns:
|
||||
bool: 是否切换成功
|
||||
"""
|
||||
return await self.token_service.switch_role(token, new_role)
|
||||
|
||||
async def get_current_user(self, token: str) -> Optional[Dict]:
|
||||
"""获取当前用户信息。
|
||||
|
||||
Args:
|
||||
token: Token 字符串
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: 用户信息
|
||||
"""
|
||||
return await self.verify_token(token)
|
||||
|
||||
async def record_failed_login(
|
||||
self,
|
||||
corp_id: str,
|
||||
login_method: str,
|
||||
login_source: str,
|
||||
fail_reason: str,
|
||||
ip_address: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
) -> None:
|
||||
"""记录失败的登录尝试。
|
||||
|
||||
Args:
|
||||
corp_id: 企业ID
|
||||
login_method: 登录方式
|
||||
login_source: 登录来源
|
||||
fail_reason: 失败原因
|
||||
ip_address: 客户端IP(可选)
|
||||
user_agent: 客户端User-Agent(可选)
|
||||
"""
|
||||
await self._record_login_log(
|
||||
employee_id=None,
|
||||
corp_id=corp_id,
|
||||
login_method=login_method,
|
||||
login_source=login_source,
|
||||
status="failed",
|
||||
fail_reason=fail_reason,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
f"登录失败: login_method={login_method}, "
|
||||
f"login_source={login_source}, reason={fail_reason}"
|
||||
)
|
||||
|
||||
async def _record_login_log(
|
||||
self,
|
||||
employee_id: Optional[str],
|
||||
corp_id: str,
|
||||
login_method: str,
|
||||
login_source: str,
|
||||
status: str,
|
||||
fail_reason: Optional[str] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
) -> None:
|
||||
"""记录登录日志到数据库。
|
||||
|
||||
Args:
|
||||
employee_id: 员工ID
|
||||
corp_id: 企业ID
|
||||
login_method: 登录方式
|
||||
login_source: 登录来源
|
||||
status: 登录状态
|
||||
fail_reason: 失败原因
|
||||
ip_address: 客户端IP
|
||||
user_agent: 客户端User-Agent
|
||||
"""
|
||||
if not self.db:
|
||||
logger.debug("未配置数据库会话,跳过登录日志记录")
|
||||
return
|
||||
|
||||
try:
|
||||
login_log = LoginLog(
|
||||
employee_id=employee_id,
|
||||
corp_id=corp_id,
|
||||
login_method=login_method,
|
||||
login_source=login_source,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
status=status,
|
||||
fail_reason=fail_reason,
|
||||
)
|
||||
self.db.add(login_log)
|
||||
await self.db.commit()
|
||||
logger.debug(f"登录日志已记录: employee_id={employee_id}, status={status}")
|
||||
except Exception as e:
|
||||
logger.error(f"记录登录日志失败: {e}")
|
||||
await self.db.rollback()
|
||||
|
||||
async def _add_to_blacklist(self, token: str) -> None:
|
||||
"""将 Token 加入黑名单。
|
||||
|
||||
Args:
|
||||
token: Token 字符串
|
||||
"""
|
||||
try:
|
||||
# 使用 Token 的 hash 作为 key,避免存储明文 Token
|
||||
import hashlib
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
key = f"{TOKEN_BLACKLIST_PREFIX}{token_hash}"
|
||||
|
||||
# 设置黑名单过期时间为 Token 原始过期时间
|
||||
await self.redis.setex(key, TOKEN_TTL_SECONDS, "1")
|
||||
logger.debug(f"Token 已加入黑名单: {token[:10]}...")
|
||||
except Exception as e:
|
||||
logger.error(f"添加 Token 到黑名单失败: {e}")
|
||||
|
||||
async def _is_token_blacklisted(self, token: str) -> bool:
|
||||
"""检查 Token 是否在黑名单中。
|
||||
|
||||
Args:
|
||||
token: Token 字符串
|
||||
|
||||
Returns:
|
||||
bool: 是否在黑名单中
|
||||
"""
|
||||
try:
|
||||
import hashlib
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
key = f"{TOKEN_BLACKLIST_PREFIX}{token_hash}"
|
||||
|
||||
result = await self.redis.get(key)
|
||||
return result is not None
|
||||
except Exception as e:
|
||||
logger.error(f"检查 Token 黑名单失败: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user