Files
chongming_lite/src/chongming/app/service/auth.py
T
2026-04-26 21:57:17 +08:00

248 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
认证服务 - 处理用户认证和 Token 管理
"""
from typing import Optional
from fastapi import Request
import bcrypt
from sqlalchemy.ext.asyncio import AsyncSession
from ..model.user import User
from ..core.config import get_config
from .user import UserService
from plugins.jwt.jwt_cache import get_jwt_cache, TokenResponse, TokenData
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]:
"""
验证用户凭据
通过邮箱和密码验证用户身份,使用 bcrypt 进行密码哈希比对。
Args:
email: 用户邮箱地址
password: 用户明文密码
session: 数据库异步会话对象
Returns:
验证成功返回 User 对象,失败返回 None
"""
user = await UserService.get_user_by_email(email, session)
if not user:
return None
if not bcrypt.checkpw(
password.encode("utf-8"), user.hashed_password.encode("utf-8")
):
return None
return user
async def create_tokens(
self,
user: User,
request: Optional[Request] = None,
device_id: Optional[str] = None,
) -> TokenResponse:
"""
为用户创建访问和刷新令牌
根据用户信息和请求上下文生成 JWT access token 和 refresh token,
同时记录设备信息用于安全审计。
Args:
user: 已验证的用户对象
request: FastAPI 请求对象,用于提取客户端信息(可选)
device_id: 设备唯一标识符(可选)
Returns:
TokenResponse 对象,包含 access_token、refresh_token 及过期时间
"""
# 获取用户数据
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]:
"""
验证访问令牌
校验 JWT access token 的有效性和完整性,提取用户身份信息。
Args:
token: JWT access token 字符串
Returns:
验证成功返回 TokenData 对象(包含 user_id、username、email),失败返回 None
"""
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]:
"""
使用刷新令牌获取新的访问令牌
验证 refresh token 的有效性,确认用户仍然存在,然后生成新的 access token。
保持原有的设备信息以确保会话连续性。
Args:
refresh_token: JWT refresh token 字符串
session: 数据库异步会话对象
request: FastAPI 请求对象,用于提取客户端信息(可选)
Returns:
验证成功返回新的 access token 字符串,失败返回 None
"""
validation = self.jwt_cache.validate_token(refresh_token, token_type="refresh")
if not validation:
return None
user_id = validation["user_id"]
# 获取用户
user = await UserService.get_user_by_id(user_id, session)
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):
"""
用户登出 - 使令牌失效
将指定的 token 加入黑名单,使其立即失效。
Args:
token: 需要失效的 JWT token 字符串
"""
self.jwt_cache.invalidate_token(token)
async def logout_all(self, user_id: str):
"""
使用户的所有令牌失效
强制注销用户的所有活跃会话,适用于密码修改或安全事件场景。
Args:
user_id: 用户 ID 字符串
"""
self.jwt_cache.invalidate_user_tokens(str(user_id))
async def get_user_sessions(self, user_id: str):
"""
获取用户的活跃会话
查询指定用户当前所有有效的 token 会话信息。
Args:
user_id: 用户 ID 字符串
Returns:
用户活跃会话列表
"""
return self.jwt_cache.get_user_sessions(str(user_id))
# 全局认证服务实例
_auth_service = None
def get_auth_service() -> AuthService:
"""
获取认证服务实例(单例模式)
确保整个应用中只有一个 AuthService 实例,避免重复初始化。
Returns:
AuthService 单例实例
"""
global _auth_service
if _auth_service is None:
_auth_service = AuthService()
return _auth_service