Files
Nexus/scripts/unlock_login_lockout.py
T

115 lines
3.6 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
清除登录防暴破锁定(删除 login_attempts 中近期失败记录)。
锁定规则见 server/application/services/auth_service.py
MAX_LOGIN_FAILURES=5LOCKOUT_MINUTES=15(按 username + ip_address 统计)
用法:
python scripts/unlock_login_lockout.py
python scripts/unlock_login_lockout.py --username admin
python scripts/unlock_login_lockout.py --all-failures # 删除该用户全部失败记录
需在仓库根目录有 .env(含 NEXUS_DATABASE_URL 或 DATABASE_URL),或在生产机部署目录执行。
"""
from __future__ import annotations
import argparse
import asyncio
import os
import re
import sys
from pathlib import Path
from urllib.parse import unquote, urlparse
ROOT = Path(__file__).resolve().parents[1]
def _load_dotenv() -> None:
env_path = ROOT / ".env"
if not env_path.is_file():
return
for line in env_path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key, value = key.strip(), value.strip().strip("\"'")
if key and key not in os.environ:
os.environ[key] = value
def _parse_database_url(url: str) -> dict:
"""Parse mysql+aiomysql://user:pass@host:port/db"""
u = urlparse(url.replace("mysql+aiomysql://", "mysql://", 1))
return {
"host": u.hostname or "127.0.0.1",
"port": u.port or 3306,
"user": unquote(u.username or ""),
"password": unquote(u.password or ""),
"db": (u.path or "/").lstrip("/") or "nexus",
}
async def _unlock(username: str, all_failures: bool) -> int:
import aiomysql
raw = (
os.environ.get("NEXUS_DATABASE_URL")
or os.environ.get("DATABASE_URL")
or ""
)
if not raw:
print("ERROR: 未找到 NEXUS_DATABASE_URL / DATABASE_URL,请在 .env 中配置或在生产目录执行", file=sys.stderr)
return 1
cfg = _parse_database_url(raw)
conn = await aiomysql.connect(
host=cfg["host"],
port=int(cfg["port"]),
user=cfg["user"],
password=cfg["password"],
db=cfg["db"],
connect_timeout=20,
)
try:
async with conn.cursor() as cur:
if all_failures:
await cur.execute(
"DELETE FROM login_attempts WHERE username = %s AND success = 0",
(username,),
)
else:
await cur.execute(
"""
DELETE FROM login_attempts
WHERE username = %s AND success = 0
AND attempted_at >= UTC_TIMESTAMP() - INTERVAL 20 MINUTE
""",
(username,),
)
deleted = cur.rowcount
await conn.commit()
print(f"OK: 已删除用户「{username}」失败登录记录 {deleted} 条,锁定已解除。")
return 0
finally:
conn.close()
def main() -> int:
parser = argparse.ArgumentParser(description="解除 Nexus 登录防暴破锁定")
parser.add_argument("--username", default="admin", help="管理员用户名(默认 admin")
parser.add_argument(
"--all-failures",
action="store_true",
help="删除该用户全部历史失败记录(默认仅近 20 分钟)",
)
args = parser.parse_args()
_load_dotenv()
return asyncio.run(_unlock(args.username, args.all_failures))
if __name__ == "__main__":
sys.exit(main())