27 KiB
Nexus 宝塔一键登录长期掉线修复方案与代码改进
生成时间:2026-07-07(Asia/Shanghai)
项目路径:C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus
生产 API:https://api.synaglobal.vip
排查机器:42.193.182.190
Nexus server id:395
本文档不包含任何密码、token、私钥、API key。所有敏感凭据均已省略。
1. 问题背景
用户反馈:
- Nexus 一键登录宝塔本身能成功。
- 同时打开多个宝塔面板后,约 1~2 小时刷新会陆续掉线。
- 目标机器示例:
42.193.182.190。
一开始怀疑点:
- Nexus 生成的宝塔临时登录 token TTL 太短。
- Nexus 一键登录 URL 缓存导致返回已消费的一次性 token。
- 多 worker / 多请求并发生成宝塔临时登录,互相覆盖或失效。
- 宝塔自身 session 清理策略导致登录态被清掉。
最终确认:根因是第 4 点,宝塔自身 BT-Task 定期清理 session 文件时硬编码 3600 秒。
2. 排查结论
2.1 Nexus 生产生成的一键登录 token 已经是 24 小时
通过生产 Nexus 调用:
POST /api/btpanel/servers/395/login-url
返回:
method=api
url=https://42.193.182.190:31701/login?tmp_token=***
随后在目标机宝塔 DB 查到最新 temp_login:
state=0
ttl_seconds≈86399
remain_seconds≈86398
因此 Nexus 当前生产生成的宝塔临时登录不是 2/3 小时,而是约 24 小时。
2.2 目标机器宝塔配置也是 24 小时
目标机器:42.193.182.190
/www/server/panel/data/session_timeout.pl = 86400
宝塔版本:11.0.0
宝塔端口:31701
2.3 真正根因:宝塔 task.py 硬编码清理 1 小时 session
宝塔后台任务文件:
/www/server/panel/task.py
原始逻辑片段:
if f_time > 3600:
os.remove(filename)
continue
宝塔每 10 分钟跑一次 session 清理,超过 3600 秒的 session 文件会被删除。
临时登录刷新时又依赖 session 文件存在。相关逻辑在:
/www/server/panel/class/common.py
核心逻辑:
if 'tmp_login_expire' in session:
s_file = 'data/session/{}'.format(session['tmp_login_id'])
if session['tmp_login_expire'] < time.time():
session.clear()
if os.path.exists(s_file):
os.remove(s_file)
return ...
if not os.path.exists(s_file):
session.clear()
return ...
也就是说:
Nexus 一键登录成功
→ 宝塔生成 tmp_login session 文件
→ BT-Task 每 10 分钟清理 session
→ task.py 硬编码超过 3600 秒删除
→ 用户 1~2 小时后刷新
→ tmp_login session 文件不存在
→ 宝塔清空 session
→ 用户被踢出
3. 总体修复方案
3.1 Nexus 侧改进
Nexus 后端做四类改进:
-
不缓存成功的一键登录 URL
- 宝塔
tmp_token是一次性链接。 - 如果缓存成功 URL,用户再次拿到的可能是已消费 token。
- 宝塔
-
一键登录生成加 Redis 分布式锁
- 同一台宝塔服务器同时只允许一个 Nexus worker 生成登录 URL。
- 避免 10 个页面同时一键登录时互相覆盖临时登录 token。
-
宝塔临时登录 TTL 固定为 86400 秒
set_temp_login传入 24 小时过期时间。
-
Nexus bootstrap/repair 自动修复宝塔 session 清理硬编码
- 检查
/www/server/panel/task.py。 - 把
if f_time > 3600:改为if f_time > session_cleanup_timeout:。 session_cleanup_timeout从宝塔public.get_session_timeout()读取。- 修改前备份。
- 修改前做 Python 语法校验。
- 保留原文件权限。
- 检查
3.2 目标机器热修复
已对 42.193.182.190 执行热修复:
- 备份:
/www/server/panel/task.py.nexus-session-ttl.bak - 修改:
/www/server/panel/task.py - reload:
/etc/init.d/bt reload - 状态:
Bt-Panel与Bt-Task均 running
4. 调用链图:Nexus 一键登录 API / 服务 / 数据库 / 宝塔调用链
flowchart TD
A["前端点击:宝塔一键登录"] --> B["POST /api/btpanel/servers/{server_id}/login-url"]
B --> C["server/api/btpanel.py:create_bt_login_url"]
C --> D["BtPanelService.create_login_url"]
D --> E["Redis 分布式锁 login_url_lock(server_id)"]
E --> F["_create_login_url_fresh"]
F --> G{"bt_panel credentials 是否已配置"}
G -- "否" --> H["bootstrap_server(force=True)"]
H --> I["ssh_bootstrap_panel"]
I --> J["远程修改宝塔 api.json / 白名单 / session cleanup TTL"]
J --> K["更新 Server.extra_attrs.bt_panel"]
G -- "是" --> L["read_bt_panel_credentials"]
K --> L
L --> M["BtPanelClient.post /config?action=set_temp_login"]
M --> N["宝塔 default.db.temp_login 写入临时登录记录"]
N --> O["resolve_temp_login_url / normalize_temp_login_url"]
O --> P["写 AuditLog: bt_panel_login_url"]
P --> Q["返回 login URL 给前端"]
5. 本地 Nexus 代码改进清单
5.1 新增 Redis 分布式锁
文件:
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\login_url_lock.py
完整新增代码:
"""Distributed per-server lock for BT Panel one-click login URL generation.
BT Panel temporary login tokens are effectively single-use and creating a new token can
invalidate a previous unused token. A process-local ``asyncio.Lock`` is not sufficient
when Nexus runs with multiple workers/containers, so this lock is backed by Redis.
"""
from __future__ import annotations
import asyncio
import logging
import secrets
from contextlib import asynccontextmanager
from typing import AsyncIterator
from server.infrastructure.redis.client import get_redis
logger = logging.getLogger("nexus.btpanel.login_url_lock")
_LOCK_KEY_PREFIX = "btpanel:login_url:lock"
_LOCK_TTL_SECONDS = 600
_LOCK_WAIT_TIMEOUT_SECONDS = 120.0
_LOCK_POLL_SECONDS = 0.2
_RELEASE_SCRIPT = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
"""
class LoginUrlLockTimeout(RuntimeError):
"""Raised when another worker is already generating a login URL too long."""
def _lock_key(server_id: int) -> str:
return f"{_LOCK_KEY_PREFIX}:{server_id}"
@asynccontextmanager
async def login_url_lock(server_id: int) -> AsyncIterator[None]:
"""Serialize login URL generation across all Nexus workers.
Redis is mandatory in Nexus, so lock backend errors are treated as hard failures
rather than silently falling back to an unsafe process-local lock.
"""
redis = get_redis()
key = _lock_key(server_id)
token = secrets.token_urlsafe(24)
deadline = asyncio.get_running_loop().time() + _LOCK_WAIT_TIMEOUT_SECONDS
while True:
acquired = bool(await redis.set(key, token, ex=_LOCK_TTL_SECONDS, nx=True))
if acquired:
break
if asyncio.get_running_loop().time() >= deadline:
raise LoginUrlLockTimeout("BT Panel login URL is already being generated; please retry shortly")
await asyncio.sleep(_LOCK_POLL_SECONDS)
try:
yield
finally:
try:
await redis.eval(_RELEASE_SCRIPT, 1, key, token)
except Exception: # noqa: BLE001 - lock expires automatically; log release failures for ops visibility
logger.warning("failed to release bt panel login url lock server=%s", server_id, exc_info=True)
设计点:
- 锁 key:
btpanel:login_url:lock:{server_id}。 - 锁 TTL:600 秒,避免 worker 崩溃后死锁。
- 等待超时:120 秒。
- Lua 释放锁,避免误删其它请求持有的新锁。
5.2 登录 URL 不再缓存,始终 fresh 生成
文件:
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\application\services\btpanel_service.py
核心代码:
async def create_login_url(
self,
server_id: int,
*,
admin_username: str,
ip_address: str | None,
) -> dict[str, str | bool]:
# BT Panel tmp_token is a one-time login link. Do not cache successful
# URLs: a repeated request after the user opens the link could receive
# an already-consumed token. Redis lock still prevents concurrent
# workers from creating tokens at the same time and invalidating each other.
try:
async with login_url_lock(server_id):
return await self._create_login_url_fresh(
server_id,
admin_username=admin_username,
ip_address=ip_address,
)
except Exception as exc:
await self._audit_login_url_failure(
server_id,
admin_username=admin_username,
ip_address=ip_address,
error=exc.__class__.__name__,
)
raise
关键变化:
- 成功 URL 不缓存。
- 所有一键登录请求进入 Redis 分布式锁。
- 异常统一写 audit,便于排查。
5.3 临时登录 token TTL 固定为 24 小时
文件:
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\constants.py
代码:
"""Baota panel integration constants."""
# Temporary login token/session upper bound in seconds; passed to BT Panel set_temp_login expire_time.
BT_TEMP_LOGIN_TTL_SECONDS = 86400
# Login URL success results are intentionally not cached: BT Panel tmp_token is one-time.
# Concurrent generation is guarded by the Redis distributed lock in login_url_lock.py.
调用点:
expire_time = int(time.time()) + BT_TEMP_LOGIN_TTL_SECONDS
client = BtPanelClient(creds, server.id)
data = await client.post(
"/config?action=set_temp_login",
{"expire_time": expire_time},
)
效果:
- Nexus 调宝塔
set_temp_login时传入 24 小时绝对过期时间。 - 生产实测
temp_login记录 TTL 约 86399 秒。
5.4 API 层把锁超时映射为 409
文件:
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\api\btpanel.py
代码:
def _http_error(exc: Exception) -> HTTPException:
if isinstance(exc, ValueError):
return HTTPException(status_code=400, detail=str(exc))
if isinstance(exc, BtPanelApiError):
return HTTPException(status_code=502, detail=str(exc))
if isinstance(exc, LoginUrlLockTimeout):
return HTTPException(status_code=409, detail=str(exc))
if isinstance(exc, RuntimeError):
return HTTPException(status_code=502, detail=str(exc))
logger.exception("btpanel error")
return HTTPException(status_code=500, detail="宝塔操作失败")
设计点:
- 同一服务器一键登录并发过多时,返回
409 Conflict。 - 前端可以提示“正在生成登录链接,请稍后重试”。
- 不返回 500,避免误判成系统异常。
5.5 Nexus bootstrap 自动修复宝塔 session 清理 TTL
文件:
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\server\infrastructure\btpanel\ssh_bootstrap.py
新增函数:
def ensure_session_cleanup_ttl():
# Make Baota session cleanup honor session_timeout.pl instead of a hard-coded 1h.
task_path = os.path.join(PANEL_ROOT, "task.py")
if not os.path.isfile(task_path):
return False
with open(task_path, "r", encoding="utf-8") as f:
source = f.read()
marker = "session_cleanup_timeout = int(public.get_session_timeout() or 86400)"
patched_condition = " if f_time > session_cleanup_timeout:\n"
if marker in source:
if patched_condition not in source:
raise RuntimeError("task.py session cleanup patch incomplete")
return False
anchor = (
" s_time = time.time()\n"
" f_list = os.listdir(sess_path)\n"
" f_num = len(f_list)\n"
)
replacement = (
" s_time = time.time()\n"
" session_cleanup_timeout = 86400\n"
" try:\n"
" session_cleanup_timeout = int(public.get_session_timeout() or 86400)\n"
" except Exception:\n"
" pass\n"
" f_list = os.listdir(sess_path)\n"
" f_num = len(f_list)\n"
)
hard_coded = " if f_time > 3600:\n"
anchor_index = source.find(anchor)
if anchor_index < 0:
raise RuntimeError("task.py session cleanup anchor not found")
patched = source[:anchor_index] + replacement + source[anchor_index + len(anchor):]
condition_index = patched.find(hard_coded, anchor_index + len(replacement))
if condition_index < 0:
raise RuntimeError("task.py session cleanup threshold not found")
patched = patched[:condition_index] + patched_condition + patched[condition_index + len(hard_coded):]
compile(patched, task_path, "exec")
original_mode = os.stat(task_path).st_mode & 0o7777
backup = task_path + ".nexus-session-ttl.bak"
if not os.path.exists(backup):
with open(backup, "w", encoding="utf-8") as f:
f.write(source)
os.chmod(backup, 0o600)
tmp = task_path + ".nexus.tmp"
with open(tmp, "w", encoding="utf-8") as f:
f.write(patched)
os.chmod(tmp, original_mode)
os.replace(tmp, task_path)
return True
接入 bootstrap 主流程:
actions = []
warnings = []
try:
if ensure_session_cleanup_ttl():
actions.append("patched_session_cleanup_ttl")
except Exception as exc:
warnings.append(f"session_cleanup_ttl_patch_failed:{exc.__class__.__name__}")
返回结果新增 warnings:
emit({
"ok": True,
"base_url": base,
"api_key": token,
"actions": actions,
"warnings": warnings,
"reloaded": bool(actions),
"verify_ssl": False,
})
设计点:
- 精确匹配:只匹配
sess_expire中s_time/f_list/f_num附近的固定片段。 - 幂等:已经包含
session_cleanup_timeout时不重复 patch。 - 半补丁检测:有 marker 但没有新判断时报警。
- 备份:首次修改前备份到
task.py.nexus-session-ttl.bak。 - 语法校验:写入前
compile(patched, task_path, "exec")。 - 权限保留:新文件保留原
task.pymode。 - 不阻断 bootstrap:patch 失败只进
warnings,不影响 API token/bootstrap 主流程。
6. 测试改进
文件:
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\tests\test_btpanel_ssh_bootstrap.py
新增测试:
def test_remote_bootstrap_patches_session_cleanup_ttl():
from server.infrastructure.btpanel import ssh_bootstrap as mod
script = mod._REMOTE_SCRIPT
assert "patched_session_cleanup_ttl" in script
assert "session_cleanup_timeout = int(public.get_session_timeout() or 86400)" in script
assert "if f_time > session_cleanup_timeout" in script
assert "hard_coded = \" if f_time > 3600:\\n\"" in script
assert "compile(patched, task_path, \"exec\")" in script
assert "original_mode = os.stat(task_path).st_mode & 0o7777" in script
assert "os.chmod(tmp, original_mode)" in script
assert ".nexus-session-ttl.bak" in script
已做静态验证:
compile ok server\infrastructure\btpanel\ssh_bootstrap.py
remote script compile ok
compile ok tests\test_btpanel_ssh_bootstrap.py
完整 pytest 暂未跑完,原因见第 11 节。
7. 生产热修复:42.193.182.190
7.1 已执行动作
通过生产 Nexus API 走 SOCKS5:
SOCKS5: 127.0.0.1:10808
Nexus API: https://api.synaglobal.vip
server_id: 395
对目标机执行:
- 检查
/www/server/panel/task.py当前逻辑。 - 备份为
/www/server/panel/task.py.nexus-session-ttl.bak。 - 修改 session 清理阈值。
- reload 宝塔。
- 回查修改结果和宝塔状态。
7.2 生产执行结果
执行结果:
exec_status HTTP/1.1 200 OK execution_id 5917
status completed exit 0
修复前:
998: if f_time > 3600:
修复后:
992: session_cleanup_timeout = 86400
994: session_cleanup_timeout = int(public.get_session_timeout() or 86400)
1003: if f_time > session_cleanup_timeout:
备份:
backup 600 root:root 61810 2026-07-07 05:08:05 +0800 /www/server/panel/task.py.nexus-session-ttl.bak
宝塔状态:
Bt-Panel already running
Bt-Task already running
8. 生产远程修复脚本模板
注意:以下脚本不包含任何 Nexus 登录凭据。实际执行应通过 Nexus 脚本执行功能或 SSH,在目标机 root/sudo 环境运行。
set -e
P=/www/server/panel/pyenv/bin/python3
[ -x "$P" ] || P=/www/server/panel/pyenv/bin/python
[ -x "$P" ] || P=python3
RESULT_FILE=/tmp/nexus_bt_session_ttl_patch.result
rm -f "$RESULT_FILE"
cat /www/server/panel/data/session_timeout.pl 2>/dev/null | sed 's/^/session_timeout=/' || true
sudo grep -n 'session_cleanup_timeout\|if f_time > 3600\|if f_time > session_cleanup_timeout' /www/server/panel/task.py || true
sudo "$P" - <<'PY'
import os
import py_compile
import stat
PANEL_ROOT = "/www/server/panel"
task_path = os.path.join(PANEL_ROOT, "task.py")
result_file = "/tmp/nexus_bt_session_ttl_patch.result"
with open(task_path, "r", encoding="utf-8") as f:
source = f.read()
marker = "session_cleanup_timeout = int(public.get_session_timeout() or 86400)"
patched_condition = " if f_time > session_cleanup_timeout:\n"
if marker in source:
if patched_condition not in source:
raise SystemExit("patch_incomplete")
result = "already_patched"
else:
anchor = (
" s_time = time.time()\n"
" f_list = os.listdir(sess_path)\n"
" f_num = len(f_list)\n"
)
replacement = (
" s_time = time.time()\n"
" session_cleanup_timeout = 86400\n"
" try:\n"
" session_cleanup_timeout = int(public.get_session_timeout() or 86400)\n"
" except Exception:\n"
" pass\n"
" f_list = os.listdir(sess_path)\n"
" f_num = len(f_list)\n"
)
hard_coded = " if f_time > 3600:\n"
anchor_index = source.find(anchor)
if anchor_index < 0:
raise SystemExit("anchor_not_found")
patched = source[:anchor_index] + replacement + source[anchor_index + len(anchor):]
condition_index = patched.find(hard_coded, anchor_index + len(replacement))
if condition_index < 0:
raise SystemExit("threshold_not_found")
patched = patched[:condition_index] + patched_condition + patched[condition_index + len(hard_coded):]
backup = task_path + ".nexus-session-ttl.bak"
if not os.path.exists(backup):
with open(backup, "w", encoding="utf-8") as f:
f.write(source)
os.chmod(backup, 0o600)
st = os.stat(task_path)
tmp = task_path + ".nexus.tmp"
with open(tmp, "w", encoding="utf-8") as f:
f.write(patched)
os.chmod(tmp, stat.S_IMODE(st.st_mode))
py_compile.compile(tmp, doraise=True)
os.replace(tmp, task_path)
result = "patched"
with open(result_file, "w", encoding="utf-8") as f:
f.write(result)
print(result)
PY
RESULT=$(cat "$RESULT_FILE")
echo "patch_result=$RESULT"
if [ "$RESULT" = "patched" ]; then
sudo /etc/init.d/bt reload || true
fi
sudo grep -n 'session_cleanup_timeout\|if f_time > 3600\|if f_time > session_cleanup_timeout' /www/server/panel/task.py || true
sudo stat -c 'backup %a %U:%G %s %y %n' /www/server/panel/task.py.nexus-session-ttl.bak 2>/dev/null || true
sudo /etc/init.d/bt status 2>/dev/null || true
9. 部署方案
9.1 本地代码部署到 Nexus 生产
建议流程:
- 备份当前生产 Nexus 代码/镜像。
- 合入以下文件变更:
server/infrastructure/btpanel/login_url_lock.py
server/infrastructure/btpanel/constants.py
server/application/services/btpanel_service.py
server/api/btpanel.py
server/infrastructure/btpanel/ssh_bootstrap.py
tests/test_btpanel_ssh_bootstrap.py
- 构建后端镜像。
- 发布到生产。
- 重启后端服务。
- 回归测试:
POST /api/btpanel/servers/{server_id}/login-url
- 检查 audit:
action=bt_panel_login_url
method=api 或 ssh
lock=redis
- 对一台测试机器触发 bootstrap/repair,确认返回:
{
"actions": ["patched_session_cleanup_ttl"],
"warnings": [],
"reloaded": true
}
或者已修复时:
{
"actions": [],
"warnings": [],
"reloaded": false
}
9.2 对已有机器批量修复
建议分批执行,不要一次修全部:
- 先选 1 台验证。
- 再选 5~10 台。
- 最后批量。
每台机器执行前检查:
cat /www/server/panel/data/session_timeout.pl
sudo grep -n 'if f_time > 3600\|session_cleanup_timeout' /www/server/panel/task.py
执行后检查:
sudo grep -n 'session_cleanup_timeout\|if f_time > session_cleanup_timeout' /www/server/panel/task.py
sudo stat /www/server/panel/task.py.nexus-session-ttl.bak
sudo /etc/init.d/bt status
10. 回滚方案
10.1 单机宝塔回滚
如果某台宝塔修复后异常,可以回滚:
sudo cp -a /www/server/panel/task.py /www/server/panel/task.py.nexus-session-ttl.rollback-copy
sudo cp -a /www/server/panel/task.py.nexus-session-ttl.bak /www/server/panel/task.py
sudo /etc/init.d/bt reload
sudo /etc/init.d/bt status
回滚后会恢复宝塔原始 3600 秒 session 清理行为。
10.2 Nexus 代码回滚
回滚以下逻辑即可:
ssh_bootstrap.py中ensure_session_cleanup_ttl()及调用。login_url_lock.py及create_login_url()内 Redis 锁使用。constants.py中 86400 TTL 如需恢复原值。btpanel.py对LoginUrlLockTimeout的 409 映射。
不建议回滚“不缓存 login URL”的逻辑,因为宝塔 tmp_token 是一次性链接,缓存成功 URL 本身有风险。
11. 依赖安装与测试现状
11.1 已创建虚拟环境
路径:
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv
Python / pip:
pip 26.0.1
python 3.14
11.2 安装依赖尝试
用户要求安装:
pytest/sqlalchemy
项目依赖文件:
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\requirements.txt
C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus\requirements-dev.txt
尝试 SOCKS5 安装:
python -m pip install --proxy socks5://127.0.0.1:10808 -r requirements.txt -r requirements-dev.txt
失败原因:
ERROR: Could not install packages due to an OSError: Missing dependencies for SOCKS support.
含义:当前 venv 的 pip/requests 缺 PySocks,所以不能直接识别 socks5://。
尝试直连安装:
python -m pip install -r requirements.txt -r requirements-dev.txt
失败原因:
SSLError(SSLEOFError: UNEXPECTED_EOF_WHILE_READING)
No matching distribution found for fastapi==0.115.6
含义:直连 PyPI SSL 被中断或网络不可用。
11.3 推荐安装方案
方案 A:如果本机有 HTTP 代理端口,优先用 HTTP 代理安装:
$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv"
$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus"
& "$venv\Scripts\python.exe" -m pip install --proxy http://127.0.0.1:端口 -r "$proj\requirements.txt" -r "$proj\requirements-dev.txt"
方案 B:先离线/手动安装 PySocks,再走 SOCKS5:
$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv"
& "$venv\Scripts\python.exe" -m pip install PySocks
& "$venv\Scripts\python.exe" -m pip install --proxy socks5://127.0.0.1:10808 -r requirements.txt -r requirements-dev.txt
但 B 的第一步仍需要能联网,所以如果直连不可用,仍建议使用 HTTP 代理或镜像源。
方案 C:使用国内镜像源:
$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv"
$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus"
& "$venv\Scripts\python.exe" -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r "$proj\requirements.txt" -r "$proj\requirements-dev.txt"
11.4 依赖安装成功后运行测试
$venv = "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus-pytest-venv"
$proj = "C:\Users\uzuma\Desktop\1111\nexus-transfer-20260706\Nexus"
cd $proj
$env:PYTHONDONTWRITEBYTECODE = "1"
& "$venv\Scripts\python.exe" -m pytest tests/test_btpanel_ssh_bootstrap.py tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_login_url.py -q
12. 代码审查结果
12.1 已解决风险
| 风险 | 原状态 | 改进后 |
|---|---|---|
| 宝塔 tmp_token 一次性 URL 被缓存 | 可能返回已消费 token | 不缓存成功 login URL |
| 多 worker 并发生成 token | 可能互相覆盖/失效 | Redis per-server lock |
| token TTL 太短 | 已确认不是短 TTL,但需固定 | 统一 86400 秒 |
| 宝塔 session 文件 3600 秒硬清 | 1~2 小时刷新掉线 | 跟随 session_timeout.pl |
| 宝塔 task.py 误改风险 | 直接替换可能误伤 | 精确 anchor + 语法校验 |
| 远程修复不可回滚 | 无备份风险 | .nexus-session-ttl.bak 备份 |
| 文件权限变化 | 可能破坏宝塔运行 | 保留原 task.py mode |
| bootstrap patch 失败影响主流程 | 可能导致 API 配置失败 | warning,不阻断主 bootstrap |
12.2 剩余注意事项
-
宝塔升级可能覆盖
task.py- 升级后需要重新触发 Nexus bootstrap/repair 或批量修复。
-
当前不是永久登录
- 当前按
session_timeout.pl=86400,约 24 小时。 - 如果想更久,需要调大宝塔
session_timeout.pl,并接受安全风险。
- 当前按
-
reload 宝塔可能让当前面板短暂重连
- 单台修复时影响较小。
- 批量修复建议低峰执行。
-
Redis 必须可用
login_url_lock不做不安全 fallback。- Redis 故障时一键登录会失败,避免并发隐患静默放大。
13. 验证清单
13.1 立即验证
对 42.193.182.190:
sudo grep -n 'session_cleanup_timeout\|if f_time > session_cleanup_timeout' /www/server/panel/task.py
sudo stat /www/server/panel/task.py.nexus-session-ttl.bak
sudo /etc/init.d/bt status
期望:
session_cleanup_timeout = 86400
session_cleanup_timeout = int(public.get_session_timeout() or 86400)
if f_time > session_cleanup_timeout
Bt-Panel running
Bt-Task running
13.2 业务验证
- Nexus 一键登录
42.193.182.190。 - 同时打开 10 个宝塔页面。
- 等待 2 小时。
- 刷新页面。
- 期望:不再因为 3600 秒 session 清理导致掉线。
13.3 24 小时边界验证
如果需要验证 24 小时边界:
- 24 小时内刷新应保持登录。
- 超过
session_timeout.pl后刷新可能要求重新登录,这是预期行为。
14. 最终结论
本次修复后,42.193.182.190 的“宝塔一键登录后 1~2 小时刷新掉线”问题已针对根因修复:
宝塔 task.py 不再硬编码 3600 秒清理 session,改为跟随 session_timeout.pl。
Nexus 后端也已补上长期改进:
不缓存一次性登录 URL
+ Redis 分布式锁串行化同一服务器登录 URL 生成
+ 24 小时 temp_login TTL
+ bootstrap 自动修复宝塔 session 清理 TTL
+ audit/warnings 可观测
建议下一步:
- 安装 Python 测试依赖并跑相关 pytest。
- 把本地 Nexus 改动发布到生产。
- 对其它宝塔机器分批执行同类修复。
- 加一个周期性巡检:检测
task.py是否被宝塔升级覆盖。