Files
wecom_it_smart_desk/backend/app/services/auth_service.py
Simon bea288e414 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
2026-07-11 23:13:10 +08:00

297 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微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 TTL8小时)
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