Files
LoveACE-EndF/loveace/router/endpoint/auth/login.py
Sibuxiangx bbc86b8330 ⚒️ 重大重构 LoveACE V2
引入了 mongodb
对数据库进行了一定程度的数据加密
性能改善
代码简化
统一错误模型和响应
使用 apifox 作为文档
2025-11-20 20:44:25 +08:00

223 lines
9.4 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.

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)