mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-12 04:28:28 +08:00
feat: 添加访问令牌管理功能并升级至 0.2.4
- 新增 Management Token(访问令牌)功能,支持创建、更新、删除和管理 - 前端添加访问令牌管理页面,支持普通用户和管理员 - 后端实现完整的令牌生命周期管理 API - 添加数据库迁移脚本创建 management_tokens 表 - Nginx 配置添加 gzip 压缩,优化响应传输 - Dialog 组件添加 persistent 属性,防止意外关闭 - 为管理后台 API 添加详细的中文文档注释 - 简化多处类型注解,统一代码风格
This commit is contained in:
@@ -2,12 +2,14 @@
|
||||
认证服务
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import HTTPException, status
|
||||
@@ -18,6 +20,9 @@ from sqlalchemy.orm import Session, joinedload
|
||||
from src.config import config
|
||||
from src.core.logger import logger
|
||||
from src.core.enums import AuthSource
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.database import ManagementToken
|
||||
from src.models.database import ApiKey, User, UserRole
|
||||
from src.services.auth.jwt_blacklist import JWTBlacklistService
|
||||
from src.services.auth.ldap import LDAPService
|
||||
@@ -478,3 +483,137 @@ class AuthService:
|
||||
except Exception as e:
|
||||
logger.error(f"撤销 Token 失败: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def authenticate_management_token(
|
||||
db: Session, raw_token: str, client_ip: str
|
||||
) -> Optional[tuple[User, "ManagementToken"]]:
|
||||
"""Management Token 认证
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
raw_token: Management Token 字符串
|
||||
client_ip: 客户端 IP
|
||||
|
||||
Returns:
|
||||
(User, ManagementToken) 元组,认证失败返回 None
|
||||
|
||||
Raises:
|
||||
RateLimitException: 超过速率限制时抛出(用于返回 429)
|
||||
"""
|
||||
from src.core.exceptions import RateLimitException
|
||||
from src.models.database import AuditEventType, ManagementToken
|
||||
from src.services.rate_limit.ip_limiter import IPRateLimiter
|
||||
from src.services.system.audit import AuditService
|
||||
|
||||
# 速率限制检查(防止暴力破解)
|
||||
allowed, remaining, ttl = await IPRateLimiter.check_limit(
|
||||
client_ip,
|
||||
endpoint_type="management_token",
|
||||
limit=config.management_token_rate_limit,
|
||||
)
|
||||
if not allowed:
|
||||
logger.warning(f"Management Token 认证 - IP {client_ip} 超过速率限制")
|
||||
raise RateLimitException(limit=config.management_token_rate_limit, window="分钟")
|
||||
|
||||
# 检查 Token 格式
|
||||
if not raw_token.startswith(ManagementToken.TOKEN_PREFIX):
|
||||
logger.warning("Management Token 认证失败 - 格式错误")
|
||||
return None
|
||||
|
||||
# 哈希查找
|
||||
token_hash = ManagementToken.hash_token(raw_token)
|
||||
token_record = (
|
||||
db.query(ManagementToken)
|
||||
.options(joinedload(ManagementToken.user))
|
||||
.filter(ManagementToken.token_hash == token_hash)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not token_record:
|
||||
logger.warning("Management Token 认证失败 - Token 不存在")
|
||||
return None
|
||||
|
||||
# 注意:数据库查询已通过 token_hash 索引匹配,此处不再需要额外的常量时间比较
|
||||
# Token 的 62^40 熵(约 238 位)加上速率限制已足够防止暴力破解
|
||||
|
||||
# 检查状态
|
||||
if not token_record.is_active:
|
||||
logger.warning(f"Management Token 认证失败 - Token 已禁用: {token_record.id}")
|
||||
return None
|
||||
|
||||
# 检查过期(使用属性方法,确保时区安全)
|
||||
if token_record.is_expired:
|
||||
logger.warning(f"Management Token 认证失败 - Token 已过期: {token_record.id}")
|
||||
AuditService.log_event(
|
||||
db=db,
|
||||
event_type=AuditEventType.MANAGEMENT_TOKEN_EXPIRED,
|
||||
description=f"Management Token 已过期: {token_record.name}",
|
||||
user_id=token_record.user_id,
|
||||
ip_address=client_ip,
|
||||
metadata={
|
||||
"token_id": token_record.id,
|
||||
"token_name": token_record.name,
|
||||
"expired_at": (
|
||||
token_record.expires_at.isoformat() if token_record.expires_at else None
|
||||
),
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
# 检查 IP 白名单
|
||||
if not token_record.is_ip_allowed(client_ip):
|
||||
logger.warning(
|
||||
f"Management Token IP 限制 - Token: {token_record.id}, IP: {client_ip}"
|
||||
)
|
||||
AuditService.log_event(
|
||||
db=db,
|
||||
event_type=AuditEventType.MANAGEMENT_TOKEN_IP_BLOCKED,
|
||||
description=f"Management Token IP 被拒绝: {token_record.name}",
|
||||
user_id=token_record.user_id,
|
||||
ip_address=client_ip,
|
||||
metadata={
|
||||
"token_id": token_record.id,
|
||||
"token_name": token_record.name,
|
||||
"blocked_ip": client_ip,
|
||||
# 不记录 allowed_ips 以防信息泄露
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
# 获取用户
|
||||
user = token_record.user
|
||||
if not user or not user.is_active:
|
||||
logger.warning("Management Token 认证失败 - 用户不存在或已禁用")
|
||||
return None
|
||||
|
||||
# 使用 SQL 原子操作更新使用统计
|
||||
from sqlalchemy import func
|
||||
|
||||
db.query(ManagementToken).filter(ManagementToken.id == token_record.id).update(
|
||||
{
|
||||
ManagementToken.last_used_at: func.now(), # 使用数据库时间确保一致性
|
||||
ManagementToken.last_used_ip: client_ip,
|
||||
ManagementToken.usage_count: ManagementToken.usage_count + 1,
|
||||
ManagementToken.updated_at: func.now(), # 显式更新,因为原子 SQL 绕过 ORM
|
||||
},
|
||||
synchronize_session=False,
|
||||
)
|
||||
|
||||
# 记录 Token 使用审计日志
|
||||
AuditService.log_event(
|
||||
db=db,
|
||||
event_type=AuditEventType.MANAGEMENT_TOKEN_USED,
|
||||
description=f"Management Token 认证成功: {token_record.name}",
|
||||
user_id=user.id,
|
||||
ip_address=client_ip,
|
||||
metadata={
|
||||
"token_id": token_record.id,
|
||||
"token_name": token_record.name,
|
||||
},
|
||||
)
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.debug(f"Management Token 认证成功: user={user.email}, token={token_record.id}")
|
||||
return user, token_record
|
||||
|
||||
Reference in New Issue
Block a user