# ============================================================================= # 企微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