297 lines
9.0 KiB
Python
297 lines
9.0 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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
|