⚒️ 重大重构 LoveACE V2

引入了 mongodb
对数据库进行了一定程度的数据加密
性能改善
代码简化
统一错误模型和响应
使用 apifox 作为文档
This commit is contained in:
2025-11-20 20:44:25 +08:00
parent 6b90c6d7bb
commit bbc86b8330
168 changed files with 14264 additions and 19152 deletions

View File

@@ -0,0 +1,10 @@
from fastapi import APIRouter
from loveace.router.endpoint.auth.authme import authme_router
from loveace.router.endpoint.auth.login import login_router
from loveace.router.endpoint.auth.register import register_router
auth_router = APIRouter(prefix="/auth", tags=["用户验证"])
auth_router.include_router(login_router)
auth_router.include_router(register_router)
auth_router.include_router(authme_router)

View File

@@ -0,0 +1,45 @@
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from loveace.router.endpoint.auth.model.authme import AuthMeResponse
from loveace.router.schemas.error import ProtectRouterErrorToCode
from loveace.router.schemas.uniresponse import UniResponseModel
from loveace.service.remote.aufe import AUFEConnection
from loveace.service.remote.aufe.depends import get_aufe_conn
authme_router = APIRouter(
prefix="/authme", responses=ProtectRouterErrorToCode.gen_code_table()
)
@authme_router.get(
"/token",
response_model=UniResponseModel[AuthMeResponse],
summary="Token 有效性验证",
)
async def auth_me(
conn: AUFEConnection = Depends(get_aufe_conn),
) -> UniResponseModel[AuthMeResponse] | JSONResponse:
"""
验证 Token 有效性并获取用户信息
✅ 功能特性:
- 验证 Authme Token 是否有效
- 返回当前认证用户的 ID
- 用于前端权限验证
💡 使用场景:
- 前端页面加载时验证登录状态
- Token 过期检测
- 获取当前登录用户信息
Returns:
AuthMeResponse: 包含验证结果和用户 ID
"""
user_id = conn.userid
return UniResponseModel[AuthMeResponse](
success=True,
data=AuthMeResponse(success=True, userid=user_id),
message="Token 验证成功",
error=None,
)

View File

@@ -0,0 +1,222 @@
import secrets
from datetime import datetime, timedelta
from uuid import uuid4
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from loveace.config.logger import LoggerMixin
from loveace.database.auth.login import LoginCoolDown
from loveace.database.auth.token import AuthMEToken
from loveace.database.auth.user import ACEUser
from loveace.database.creator import get_db_session
from loveace.router.dependencies.logger import no_user_logger_mixin
from loveace.router.endpoint.auth.model.login import (
LoginErrorToCode,
LoginRequest,
LoginResponse,
)
from loveace.router.schemas.uniresponse import UniResponseModel
from loveace.service.remote.aufe import AUFEService
from loveace.service.remote.aufe.depends import get_aufe_service
from loveace.utils.rsa import RSAUtils
login_router = APIRouter(prefix="/login", responses=LoginErrorToCode.gen_code_table())
rsa_util = RSAUtils.get_or_create_rsa_utils()
@login_router.post(
"/next",
response_model=UniResponseModel[LoginResponse],
summary="用户登录",
)
async def login(
login_request: LoginRequest,
db: AsyncSession = Depends(get_db_session),
aufe_service: AUFEService = Depends(get_aufe_service),
logger: LoggerMixin = Depends(no_user_logger_mixin),
) -> UniResponseModel[LoginResponse] | JSONResponse:
"""
用户登录,返回 Authme Token
✅ 功能特性:
- 通过 AUFE 服务验证 EC 密码和登录密码
- 限制用户总 Token 数为 5 个
- 登录失败后设置 1 分钟冷却时间
⚠️ 限制条件:
- 连续登录失败会触发冷却机制
- 冷却期间内拒绝该用户的登录请求
💡 使用场景:
- 用户首次登录
- 用户重新登录(更换设备)
- 用户忘记密码后重新设置并登录
Args:
login_request: 包含用户 ID、EC 密码、登录密码的登录请求
db: 数据库会话
aufe_service: AUFE 远程认证服务
logger: 日志记录器
Returns:
LoginResponse: 包含新生成的 Authme Token
"""
try:
async with db as session:
logger.info(f"用户登录: {login_request.userid}")
# 检查用户是否存在
query = select(ACEUser).where(ACEUser.userid == login_request.userid)
result = await session.execute(query)
user = result.scalars().first()
if user is None:
logger.info(f"用户不存在: {login_request.userid}")
return LoginErrorToCode().invalid_credentials.to_json_response(
logger.trace_id
)
# 检查是否在冷却时间内
query = select(LoginCoolDown).where(LoginCoolDown.userid == user.userid)
result = await session.execute(query)
cooldown = result.scalars().first()
if cooldown and cooldown.expire_date > datetime.now():
logger.info(f"用户 {login_request.userid} 在冷却时间内,拒绝登录")
return LoginErrorToCode().cooldown.to_json_response(logger.trace_id)
# 解密数据库中的 EC密码 登录密码 和 请求体中的 EC密码 登录密码
try:
db_ec_password = rsa_util.decrypt(user.ec_password)
db_password = rsa_util.decrypt(user.password)
ec_password = rsa_util.decrypt(login_request.ec_password)
password = rsa_util.decrypt(login_request.password)
except Exception as e:
logger.info(f"用户 {login_request.userid} 提供的密码解密失败: {e}")
return LoginErrorToCode().invalid_credentials.to_json_response(
logger.trace_id
)
# 尝试使用AUFE服务验证EC密码和登录密码
conn = await aufe_service.get_or_create_connection(
userid=login_request.userid,
ec_password=ec_password,
password=password,
)
if not await conn.health_check():
logger.info(f"用户 {login_request.userid} 的AUFE连接不可用")
# EC密码登录重试机制 (最多3次)
ec_login_status = None
for ec_retry in range(3):
ec_login_status = await conn.ec_login()
if ec_login_status.success:
break
# 如果是攻击防范或密码错误,直接退出重试
if (
ec_login_status.fail_maybe_attacked
or ec_login_status.fail_invalid_credentials
):
logger.info(
f"用户 {login_request.userid} EC登录失败 (攻击防范或密码错误),停止重试"
)
break
logger.info(
f"用户 {login_request.userid} EC登录重试第 {ec_retry + 1}"
)
if not ec_login_status or not ec_login_status.success:
logger.info(f"用户 {login_request.userid} 的EC密码错误")
# 设置冷却时间
cooldown_time = timedelta(minutes=1)
if cooldown:
cooldown.expire_date = datetime.now() + cooldown_time
else:
cooldown = LoginCoolDown(
userid=user.userid,
expire_date=datetime.now() + cooldown_time,
)
session.add(cooldown)
await session.commit()
return (
LoginErrorToCode().remote_invalid_credentials.to_json_response(
logger.trace_id
)
)
# UAAP密码登录重试机制 (最多3次)
uaap_login_status = None
for uaap_retry in range(3):
uaap_login_status = await conn.uaap_login()
if uaap_login_status.success:
break
# 如果是密码错误,直接退出重试
if uaap_login_status.fail_invalid_credentials:
logger.info(
f"用户 {login_request.userid} UAAP登录失败 (密码错误),停止重试"
)
break
logger.info(
f"用户 {login_request.userid} UAAP登录重试第 {uaap_retry + 1}"
)
if not uaap_login_status or not uaap_login_status.success:
logger.info(f"用户 {login_request.userid} 的登录密码错误")
# 设置冷却时间
cooldown_time = timedelta(minutes=1)
if cooldown:
cooldown.expire_date = datetime.now() + cooldown_time
else:
cooldown = LoginCoolDown(
userid=user.userid,
expire_date=datetime.now() + cooldown_time,
)
session.add(cooldown)
await session.commit()
return (
LoginErrorToCode().remote_invalid_credentials.to_json_response(
logger.trace_id
)
)
# 删除冷却时间
if cooldown:
await session.delete(cooldown)
await session.commit()
# 比对密码,如果新的密码与数据库中的密码不一致,则更新数据库中的密码
if db_ec_password != ec_password or db_password != password:
user.ec_password = rsa_util.encrypt(ec_password)
user.password = rsa_util.encrypt(password)
session.add(user)
await session.commit()
logger.info(f"用户 {login_request.userid} 的密码已更新")
# 创建新的Authme Token
new_token = AuthMEToken(
user_id=user.userid,
token=secrets.token_urlsafe(32),
device_id=uuid4().hex,
)
session.add(new_token)
await session.commit()
# 限制用户总 Token 数为5个删除最早的 Token
query = (
select(AuthMEToken)
.where(AuthMEToken.user_id == user.userid)
.order_by(AuthMEToken.create_date.asc())
)
result = await session.execute(query)
tokens = result.scalars().all()
if len(tokens) > 5:
for token in tokens[:-5]:
await session.delete(token)
await session.commit()
logger.info(f"用户 {login_request.userid} 登录成功返回Token")
return UniResponseModel[LoginResponse](
success=True,
data=LoginResponse(token=new_token.token),
message="登录成功",
error=None,
)
except Exception as e:
logger.error(f"用户 {login_request.userid} 登录时发生错误: {e}")
return LoginErrorToCode().server_error.to_json_response(logger.trace_id)

View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel, Field
class AuthMeResponse(BaseModel):
success: bool = Field(..., description="是否验证成功")
userid: str = Field(..., description="用户ID")

View File

@@ -0,0 +1,37 @@
from fastapi import status
from pydantic import BaseModel, Field
from loveace.router.schemas.base import ErrorToCode, ErrorToCodeNode
class LoginRequest(BaseModel):
userid: str = Field(..., description="用户ID")
ec_password: str = Field(..., description="用户EC密码rsa encrypt加密后的密文")
password: str = Field(..., description="用户登录密码rsa encrypt加密后的密文")
class LoginResponse(BaseModel):
token: str = Field(..., description="用户登录成功后返回的Authme Token")
class LoginErrorToCode(ErrorToCode):
invalid_credentials: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_403_FORBIDDEN,
code="CREDENTIALS_INVALID",
message="凭证无效",
)
remote_invalid_credentials: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_403_FORBIDDEN,
code="REMOTE_CREDENTIALS_INVALID",
message="远程凭证无效EC密码或登录密码错误需要进行密码重置",
)
cooldown: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_429_TOO_MANY_REQUESTS,
code="COOLDOWN",
message="操作过于频繁,请稍后再试",
)
server_error: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
code="SERVER_ERROR",
message="服务器错误",
)

View File

@@ -0,0 +1,99 @@
from fastapi import status
from pydantic import BaseModel, Field
from loveace.router.schemas import (
ErrorToCode,
ErrorToCodeNode,
)
##############################################################
# * 用户注册相关模型-邀请码 *#
class InviteCodeRequest(BaseModel):
invite_code: str = Field(..., description="邀请码")
class InviteCodeResponse(BaseModel):
token: str = Field(..., description="邀请码验证成功后返回的Token")
class InviteErrorToCode(ErrorToCode):
invalid_invite_code: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_403_FORBIDDEN,
code="INVITE_CODE_INVALID",
message="邀请码错误",
)
server_error: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
code="SERVER_ERROR",
message="服务器错误",
)
##############################################################
##############################################################
# * 用户注册相关模型-注册 *#
class RegisterRequest(BaseModel):
userid: str = Field(..., description="用户ID")
ec_password: str = Field(..., description="用户EC密码rsa encrypt加密后的密文")
password: str = Field(..., description="用户登录密码rsa encrypt加密后的密文")
token: str = Field(..., description="邀请码验证成功后返回的Token")
class RegisterResponse(BaseModel):
token: str = Field(..., description="用户登录成功后返回的Authme Token")
class RegisterErrorToCode(ErrorToCode):
invalid_token: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_403_FORBIDDEN,
code="TOKEN_INVALID",
message="Token无效",
)
userid_exists: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_409_CONFLICT,
code="USERID_EXISTS",
message="用户ID已存在",
)
decrypt_error: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_400_BAD_REQUEST,
code="DECRYPT_ERROR",
message="密码解密失败",
)
ec_server_error: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_400_BAD_REQUEST,
code="EC_SERVER_ERROR",
message="EC服务错误",
)
ec_password_error: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_400_BAD_REQUEST,
code="EC_PASSWORD_ERROR",
message="EC密码错误",
)
uaap_server_error: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_400_BAD_REQUEST,
code="UAAP_SERVER_ERROR",
message="UAAP服务错误",
)
uaap_password_error: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_400_BAD_REQUEST,
code="UAAP_PASSWORD_ERROR",
message="UAAP密码错误",
)
register_in_cooldown: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_429_TOO_MANY_REQUESTS,
code="REGISTER_IN_COOLDOWN",
message="注册请求过于频繁,请稍后再试",
)
server_error: ErrorToCodeNode = ErrorToCodeNode(
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
code="SERVER_ERROR",
message="服务器错误",
)
##############################################################

View File

@@ -0,0 +1,247 @@
import secrets
from datetime import datetime, timedelta
from uuid import uuid4
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from loveace.config.logger import LoggerMixin
from loveace.database.auth.register import InviteCode as InviteCodeDB
from loveace.database.auth.register import RegisterCoolDown
from loveace.database.auth.token import AuthMEToken
from loveace.database.auth.user import ACEUser
from loveace.database.creator import get_db_session
from loveace.router.dependencies.logger import no_user_logger_mixin
from loveace.router.endpoint.auth.model.register import (
InviteCodeRequest,
InviteCodeResponse,
InviteErrorToCode,
RegisterErrorToCode,
RegisterRequest,
RegisterResponse,
)
from loveace.router.schemas.uniresponse import UniResponseModel
from loveace.service.remote.aufe import AUFEService
from loveace.service.remote.aufe.depends import get_aufe_service
from loveace.utils.rsa import RSAUtils
register_router = APIRouter(prefix="/register")
temp_tokens = []
rsa_util = RSAUtils.get_or_create_rsa_utils()
@register_router.post(
"/invite",
response_model=UniResponseModel[InviteCodeResponse],
responses=InviteErrorToCode.gen_code_table(),
summary="邀请码验证",
)
async def register(
invite_code: InviteCodeRequest,
db: AsyncSession = Depends(get_db_session),
logger: LoggerMixin = Depends(no_user_logger_mixin),
) -> UniResponseModel[InviteCodeResponse] | JSONResponse:
"""
验证邀请码并返回临时 Token
✅ 功能特性:
- 验证邀请码的有效性
- 生成临时 Token 用于后续注册步骤
- 邀请码一次性使用
💡 使用场景:
- 用户注册流程的第一步
- 邀请制系统的验证
Args:
invite_code: 邀请码请求对象
db: 数据库会话
logger: 日志记录器
Returns:
InviteCodeResponse: 包含临时 Token
"""
try:
async with db as session:
logger.info(f"邀请码: {invite_code.invite_code}")
invite = select(InviteCodeDB).where(
InviteCodeDB.code == invite_code.invite_code
)
result = await session.execute(invite)
invite_data = result.scalars().first()
if invite_data is None:
logger.info(f"邀请码不存在: {invite_code.invite_code}")
return InviteErrorToCode().invalid_invite_code.to_json_response(
logger.trace_id
)
token = secrets.token_urlsafe(128)
temp_tokens.append(token)
return UniResponseModel[InviteCodeResponse](
success=True,
data=InviteCodeResponse(token=token),
message="邀请码验证成功",
error=None,
)
except Exception as e:
logger.error("邀请码验证失败:")
logger.exception(e)
return InviteErrorToCode().server_error.to_json_response(logger.trace_id)
@register_router.post(
"/next",
response_model=UniResponseModel[RegisterResponse],
responses=RegisterErrorToCode.gen_code_table(),
summary="用户注册",
)
async def register_user(
register_info: RegisterRequest,
db: AsyncSession = Depends(get_db_session),
logger: LoggerMixin = Depends(no_user_logger_mixin),
aufe_service: AUFEService = Depends(get_aufe_service),
) -> UniResponseModel[RegisterResponse] | JSONResponse:
"""
用户注册,验证身份并创建账户
✅ 功能特性:
- 通过 AUFE 服务验证 EC 密码和登录密码
- 验证身份信息的有效性
- 生成 Authme Token 用于登录
⚠️ 限制条件:
- EC 密码或登录密码错误会触发 5 分钟冷却时间
- 用户 ID 不能重复
- 必须提供有效的邀请 Token
💡 使用场景:
- 新用户注册
- 创建学号对应的账户
Args:
register_info: 包含用户 ID、EC 密码、登录密码和邀请 Token 的注册信息
db: 数据库会话
logger: 日志记录器
aufe_service: AUFE 远程认证服务
Returns:
RegisterResponse: 包含 Authme Token
"""
try:
async with db as session:
# COOLDOWN检查
query = select(RegisterCoolDown).where(
RegisterCoolDown.userid == register_info.userid
)
result = await session.execute(query)
cooldown = result.scalars().first()
if cooldown:
if cooldown.expire_date > datetime.now():
logger.info(f"用户ID注册冷却中: {register_info.userid}")
return RegisterErrorToCode().userid_exists.to_json_response(
logger.trace_id
)
else:
await session.delete(cooldown)
await session.commit()
if register_info.token not in temp_tokens:
logger.info(f"无效的注册Token: {register_info.token}")
return RegisterErrorToCode().invalid_token.to_json_response(
logger.trace_id
)
query = select(ACEUser).where(ACEUser.userid == register_info.userid)
result = await session.execute(query)
user = result.scalars().first()
if user is not None:
logger.info(f"用户ID已存在: {register_info.userid}")
return RegisterErrorToCode().userid_exists.to_json_response(
logger.trace_id
)
# 尝试使用AUFE服务验证EC密码
try:
ec_password = rsa_util.decrypt(register_info.ec_password)
password = rsa_util.decrypt(register_info.password)
except Exception as e:
logger.info(f"用户 {register_info.userid} 提供的密码解密失败: {e}")
return RegisterErrorToCode().decrypt_error.to_json_response(
logger.trace_id
)
conn = await aufe_service.get_or_create_connection(
userid=register_info.userid,
ec_password=ec_password,
password=password,
)
ec_login_status = await conn.ec_login()
if not ec_login_status.success:
cooldown_entry = RegisterCoolDown(
userid=register_info.userid,
expire_date=datetime.now() + timedelta(minutes=5),
)
session.add(cooldown_entry)
await session.commit()
if ec_login_status.fail_invalid_credentials:
logger.info(f"EC密码错误: {register_info.userid}")
return RegisterErrorToCode().ec_password_error.to_json_response(
logger.trace_id
)
else:
logger.error(f"AUFE服务异常: {ec_login_status}")
return RegisterErrorToCode().ec_server_error.to_json_response(
logger.trace_id
)
uaap_login_status = await conn.uaap_login()
if not uaap_login_status.success:
cooldown_entry = RegisterCoolDown(
userid=register_info.userid,
expire_date=datetime.now() + timedelta(minutes=5),
)
session.add(cooldown_entry)
await session.commit()
if uaap_login_status.fail_invalid_credentials:
logger.info(f"登录密码错误: {register_info.userid}")
return RegisterErrorToCode().uaap_password_error.to_json_response(
logger.trace_id
)
else:
logger.error(f"AUFE服务异常: {uaap_login_status}")
return RegisterErrorToCode().ec_server_error.to_json_response(
logger.trace_id
)
# 创建新用户
new_user = ACEUser(
userid=register_info.userid,
ec_password=register_info.ec_password,
password=register_info.password,
)
session.add(new_user)
await session.commit()
# 注册成功后删除临时Token
temp_tokens.remove(register_info.token)
# 生成Authme Token
authme_token = secrets.token_urlsafe(128)
new_token = AuthMEToken(
user_id=new_user.userid, token=authme_token, device_id=uuid4().hex
)
session.add(new_token)
await session.commit()
return UniResponseModel[RegisterResponse](
success=True,
data=RegisterResponse(token=authme_token),
message="注册成功",
error=None,
)
except ValueError as ve:
logger.error("用户注册失败: RSA解密错误")
logger.exception(ve)
return RegisterErrorToCode().server_error.to_json_response(
logger.trace_id, "RSA解密错误请检查授权密文"
)
except Exception as e:
logger.error("用户注册失败:")
logger.exception(e)
return RegisterErrorToCode().server_error.to_json_response(logger.trace_id)