Files

297 lines
9.0 KiB
Python
Raw Permalink Normal View History

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