2026-03-06 17:57:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
认证服务 - 处理用户认证和 Token 管理
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
from fastapi import Request
|
2026-04-26 20:44:18 +08:00
|
|
|
|
import bcrypt
|
2026-03-06 17:57:44 +08:00
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
|
|
from ..model.user import User
|
|
|
|
|
|
from ..core.config import get_config
|
2026-04-13 15:55:12 +08:00
|
|
|
|
from .user import UserService
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
|
from plugins.jwt.jwt_cache import get_jwt_cache, TokenResponse, TokenData
|
|
|
|
|
|
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
class AuthService:
|
|
|
|
|
|
"""认证服务"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.jwt_cache = get_jwt_cache()
|
|
|
|
|
|
self.settings = get_config()
|
|
|
|
|
|
|
|
|
|
|
|
async def authenticate_user(
|
|
|
|
|
|
self, email: str, password: str, session: AsyncSession
|
|
|
|
|
|
) -> Optional[User]:
|
2026-04-26 21:57:17 +08:00
|
|
|
|
"""
|
|
|
|
|
|
验证用户凭据
|
|
|
|
|
|
|
|
|
|
|
|
通过邮箱和密码验证用户身份,使用 bcrypt 进行密码哈希比对。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
email: 用户邮箱地址
|
|
|
|
|
|
password: 用户明文密码
|
|
|
|
|
|
session: 数据库异步会话对象
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
验证成功返回 User 对象,失败返回 None
|
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
|
user = await UserService.get_user_by_email(email, session)
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
return None
|
2026-04-26 20:44:18 +08:00
|
|
|
|
if not bcrypt.checkpw(
|
2026-04-26 21:57:17 +08:00
|
|
|
|
password.encode("utf-8"), user.hashed_password.encode("utf-8")
|
2026-04-26 20:44:18 +08:00
|
|
|
|
):
|
2026-03-06 17:57:44 +08:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
async def create_tokens(
|
|
|
|
|
|
self,
|
|
|
|
|
|
user: User,
|
|
|
|
|
|
request: Optional[Request] = None,
|
|
|
|
|
|
device_id: Optional[str] = None,
|
|
|
|
|
|
) -> TokenResponse:
|
2026-04-26 21:57:17 +08:00
|
|
|
|
"""
|
|
|
|
|
|
为用户创建访问和刷新令牌
|
|
|
|
|
|
|
|
|
|
|
|
根据用户信息和请求上下文生成 JWT access token 和 refresh token,
|
|
|
|
|
|
同时记录设备信息用于安全审计。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user: 已验证的用户对象
|
|
|
|
|
|
request: FastAPI 请求对象,用于提取客户端信息(可选)
|
|
|
|
|
|
device_id: 设备唯一标识符(可选)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
TokenResponse 对象,包含 access_token、refresh_token 及过期时间
|
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 获取用户数据
|
|
|
|
|
|
user_data = {
|
|
|
|
|
|
"username": user.username,
|
|
|
|
|
|
"email": user.email,
|
|
|
|
|
|
"is_superuser": user.is_superuser,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 获取设备信息
|
|
|
|
|
|
user_agent = None
|
|
|
|
|
|
ip_address = None
|
|
|
|
|
|
if request:
|
|
|
|
|
|
user_agent = request.headers.get("user-agent")
|
|
|
|
|
|
ip_address = request.client.host if request.client else None
|
|
|
|
|
|
|
|
|
|
|
|
# 创建访问令牌
|
|
|
|
|
|
access_token = self.jwt_cache.create_token(
|
|
|
|
|
|
user_id=str(user.id),
|
|
|
|
|
|
user_data=user_data,
|
|
|
|
|
|
token_type="access",
|
|
|
|
|
|
device_id=device_id,
|
|
|
|
|
|
user_agent=user_agent,
|
|
|
|
|
|
ip_address=ip_address,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建刷新令牌
|
|
|
|
|
|
refresh_token = self.jwt_cache.create_token(
|
|
|
|
|
|
user_id=str(user.id),
|
|
|
|
|
|
user_data=user_data,
|
|
|
|
|
|
token_type="refresh",
|
|
|
|
|
|
device_id=device_id,
|
|
|
|
|
|
user_agent=user_agent,
|
|
|
|
|
|
ip_address=ip_address,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return TokenResponse(
|
|
|
|
|
|
access_token=access_token,
|
|
|
|
|
|
refresh_token=refresh_token,
|
|
|
|
|
|
expires_in=self.settings["security"]["access_token_expire_minutes"] * 60,
|
|
|
|
|
|
refresh_expires_in=getattr(self.settings, "refresh_token_expire_days", 7)
|
|
|
|
|
|
* 24
|
|
|
|
|
|
* 60
|
|
|
|
|
|
* 60,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def validate_access_token(self, token: str) -> Optional[TokenData]:
|
2026-04-26 21:57:17 +08:00
|
|
|
|
"""
|
|
|
|
|
|
验证访问令牌
|
|
|
|
|
|
|
|
|
|
|
|
校验 JWT access token 的有效性和完整性,提取用户身份信息。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
token: JWT access token 字符串
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
验证成功返回 TokenData 对象(包含 user_id、username、email),失败返回 None
|
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
|
validation = self.jwt_cache.validate_token(token, token_type="access")
|
|
|
|
|
|
if not validation:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return TokenData(
|
|
|
|
|
|
user_id=validation["user_id"],
|
|
|
|
|
|
username=validation["payload"]["data"].get("username"),
|
|
|
|
|
|
email=validation["payload"]["data"].get("email"),
|
|
|
|
|
|
scopes=[], # 可以根据需要添加权限范围
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def refresh_access_token(
|
|
|
|
|
|
self,
|
|
|
|
|
|
refresh_token: str,
|
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
|
request: Optional[Request] = None,
|
|
|
|
|
|
) -> Optional[str]:
|
2026-04-26 21:57:17 +08:00
|
|
|
|
"""
|
|
|
|
|
|
使用刷新令牌获取新的访问令牌
|
|
|
|
|
|
|
|
|
|
|
|
验证 refresh token 的有效性,确认用户仍然存在,然后生成新的 access token。
|
|
|
|
|
|
保持原有的设备信息以确保会话连续性。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
refresh_token: JWT refresh token 字符串
|
|
|
|
|
|
session: 数据库异步会话对象
|
|
|
|
|
|
request: FastAPI 请求对象,用于提取客户端信息(可选)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
验证成功返回新的 access token 字符串,失败返回 None
|
|
|
|
|
|
"""
|
2026-04-13 15:55:12 +08:00
|
|
|
|
validation = self.jwt_cache.validate_token(refresh_token, token_type="refresh")
|
2026-03-06 17:57:44 +08:00
|
|
|
|
if not validation:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
user_id = validation["user_id"]
|
|
|
|
|
|
|
|
|
|
|
|
# 获取用户
|
2026-04-20 17:11:31 +08:00
|
|
|
|
user = await UserService.get_user_by_id(user_id, session)
|
2026-03-06 17:57:44 +08:00
|
|
|
|
if not user:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 创建新的访问令牌
|
|
|
|
|
|
user_data = {
|
|
|
|
|
|
"username": user.username,
|
|
|
|
|
|
"email": user.email,
|
|
|
|
|
|
"is_superuser": user.is_superuser,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
device_id = validation.get("device_id")
|
|
|
|
|
|
user_agent = None
|
|
|
|
|
|
ip_address = None
|
|
|
|
|
|
if request:
|
|
|
|
|
|
user_agent = request.headers.get("user-agent")
|
|
|
|
|
|
ip_address = request.client.host if request.client else None
|
|
|
|
|
|
|
|
|
|
|
|
new_access_token = self.jwt_cache.create_token(
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
user_data=user_data,
|
|
|
|
|
|
token_type="access",
|
|
|
|
|
|
device_id=device_id,
|
|
|
|
|
|
user_agent=user_agent,
|
|
|
|
|
|
ip_address=ip_address,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return new_access_token
|
|
|
|
|
|
|
|
|
|
|
|
async def logout(self, token: str):
|
2026-04-26 21:57:17 +08:00
|
|
|
|
"""
|
|
|
|
|
|
用户登出 - 使令牌失效
|
|
|
|
|
|
|
|
|
|
|
|
将指定的 token 加入黑名单,使其立即失效。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
token: 需要失效的 JWT token 字符串
|
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
|
self.jwt_cache.invalidate_token(token)
|
|
|
|
|
|
|
|
|
|
|
|
async def logout_all(self, user_id: str):
|
2026-04-26 21:57:17 +08:00
|
|
|
|
"""
|
|
|
|
|
|
使用户的所有令牌失效
|
|
|
|
|
|
|
|
|
|
|
|
强制注销用户的所有活跃会话,适用于密码修改或安全事件场景。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_id: 用户 ID 字符串
|
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
|
self.jwt_cache.invalidate_user_tokens(str(user_id))
|
|
|
|
|
|
|
|
|
|
|
|
async def get_user_sessions(self, user_id: str):
|
2026-04-26 21:57:17 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取用户的活跃会话
|
|
|
|
|
|
|
|
|
|
|
|
查询指定用户当前所有有效的 token 会话信息。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
user_id: 用户 ID 字符串
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
用户活跃会话列表
|
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
|
return self.jwt_cache.get_user_sessions(str(user_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 全局认证服务实例
|
|
|
|
|
|
_auth_service = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_auth_service() -> AuthService:
|
2026-04-26 21:57:17 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取认证服务实例(单例模式)
|
|
|
|
|
|
|
|
|
|
|
|
确保整个应用中只有一个 AuthService 实例,避免重复初始化。
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
AuthService 单例实例
|
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
|
global _auth_service
|
|
|
|
|
|
if _auth_service is None:
|
|
|
|
|
|
_auth_service = AuthService()
|
|
|
|
|
|
return _auth_service
|