feat(phase4): 全部业务模块迁移到 modules/(结构迁移)
- 21 个模块目录(auth/system/dictionary/configuration/cms/member/region/link/notice/job/log/upload/index) - 每个模块 router.py(合并模块导出多个 router)/schemas.py/service.py/repository.py/models.py - routers/v1.py 与 routers/__init__.py 改为直接引用 modules,前缀与权限串原样保留 - core/middleware.py 懒加载指向 modules.system.menu / modules.log;core/redis_client.py 指向 modules.job.scheduler - apps/views|services|forms 全部改为 re-export 垫片 - 迁移后 grep 守门通过:modules/core 无 apps/extensions 残留引用 - 实测登录、27+ 模块接口、写操作 commit、操作日志落库均正常 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import asyncio
|
||||
|
||||
from fastapi.logger import logger
|
||||
from sqlalchemy import and_
|
||||
|
||||
from modules.auth.schemas import LoginForm
|
||||
from modules.system.user.models import User
|
||||
from modules.log import service as login_log
|
||||
from core.database import get_db_session
|
||||
from core import R
|
||||
from core import password
|
||||
from core.jwt import create_token
|
||||
from utils.captcha import verify_captcha
|
||||
from utils.ip import get_client_ip
|
||||
from config import LOGIN_FAIL_MAX, LOGIN_LOCK_SECONDS
|
||||
|
||||
# Redis Key 前缀
|
||||
_LOGIN_FAIL_PREFIX = 'login:fail:'
|
||||
_LOGIN_LOCK_PREFIX = 'login:lock:'
|
||||
|
||||
|
||||
def _fail_key(ip: str, username: str) -> str:
|
||||
"""登录失败计数Key:按IP+用户名维度"""
|
||||
return f'{_LOGIN_FAIL_PREFIX}{ip}:{username}'
|
||||
|
||||
|
||||
def _lock_key(ip: str, username: str) -> str:
|
||||
"""登录锁定Key"""
|
||||
return f'{_LOGIN_LOCK_PREFIX}{ip}:{username}'
|
||||
|
||||
|
||||
async def _is_locked(redis, ip: str, username: str) -> tuple[bool, int]:
|
||||
"""检查是否处于锁定状态,返回 (是否锁定, 剩余秒数)"""
|
||||
key = _lock_key(ip, username)
|
||||
ttl = await redis.ttl(key)
|
||||
if ttl and ttl > 0:
|
||||
return True, ttl
|
||||
return False, 0
|
||||
|
||||
|
||||
async def _record_fail(redis, ip: str, username: str):
|
||||
"""记录一次登录失败,达到上限则锁定"""
|
||||
key = _fail_key(ip, username)
|
||||
count = await redis.incr(key)
|
||||
# 首次失败时设置过期时间(与锁定时间一致,避免残留)
|
||||
if count == 1:
|
||||
await redis.expire(key, LOGIN_LOCK_SECONDS)
|
||||
# 达到上限,设置锁定
|
||||
if count >= LOGIN_FAIL_MAX:
|
||||
lock_k = _lock_key(ip, username)
|
||||
await redis.set(lock_k, '1', ex=LOGIN_LOCK_SECONDS)
|
||||
logger.warning(f"登录锁定:IP={ip}, 用户={username}, 连续失败{count}次")
|
||||
|
||||
|
||||
async def _clear_fail(redis, ip: str, username: str):
|
||||
"""登录成功后清除失败记录"""
|
||||
fail_k = _fail_key(ip, username)
|
||||
lock_k = _lock_key(ip, username)
|
||||
await redis.delete(fail_k, lock_k)
|
||||
|
||||
|
||||
# 系统登录
|
||||
|
||||
async def login(request, data: LoginForm):
|
||||
# 获取客户端IP
|
||||
ip = get_client_ip(request)
|
||||
|
||||
# 检查是否被锁定
|
||||
from core.redis_client import get_redis
|
||||
redis = get_redis()
|
||||
locked, remaining = await _is_locked(redis, ip, data.username)
|
||||
if locked:
|
||||
return R.failed(f"登录失败次数过多,请{remaining}秒后再试")
|
||||
|
||||
# 验证码校验(验证成功后自动删除,防止重放攻击)
|
||||
if not await verify_captcha(data.idKey, data.captcha):
|
||||
return R.failed("验证码不正确")
|
||||
|
||||
# 根据用户名查询用户信息(状态为1-正常,2-禁用),同步DB查询放入线程池
|
||||
def _query_user():
|
||||
db = get_db_session()
|
||||
return db.query(User).filter(and_(User.username == data.username, User.is_delete == 0, User.status == 1)).first()
|
||||
|
||||
user = await asyncio.to_thread(_query_user)
|
||||
if not user:
|
||||
# 记录失败但不透露具体原因(用户不存在、已禁用、密码错误统一提示)
|
||||
await _record_fail(redis, ip, data.username)
|
||||
await asyncio.to_thread(login_log.record_login_log, request, data.username, 1, 0, 1, error="用户名或密码不正确")
|
||||
return R.failed("用户名或密码不正确")
|
||||
|
||||
# 比对密码是否相同
|
||||
if not password.verify_password(data.password, user.salt or '', user.password or ''):
|
||||
await _record_fail(redis, ip, data.username)
|
||||
await asyncio.to_thread(login_log.record_login_log, request, data.username, 1, 0, 1, error="用户名或密码不正确")
|
||||
return R.failed("用户名或密码不正确")
|
||||
|
||||
# 登录成功,清除失败记录
|
||||
await _clear_fail(redis, ip, data.username)
|
||||
|
||||
# 生成TOKEN
|
||||
access_token = create_token({"userId": user.id, "username": user.username, "realname": user.realname or ''})
|
||||
# 记录登录成功日志
|
||||
await asyncio.to_thread(login_log.record_login_log, request, data.username, 1, 0, 0, result="登录成功")
|
||||
# 返回结果
|
||||
return R.ok(msg="登录成功", data={"access_token": access_token})
|
||||
Reference in New Issue
Block a user