fix(install): auto-resolve 1Panel Redis auth (:pass@ / default)
Stop prefilling root; probe multiple Redis URL formats on credential check and persist the URL that works for init-db. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -122,7 +122,7 @@ Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安
|
||||
1. **应用商店** → 安装 **MySQL**、**Redis**
|
||||
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
|
||||
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`)
|
||||
4. 填写 MySQL 密码;Redis:**用户名 `root`** + 应用参数中的密码(见 1Panel 文档)
|
||||
4. 填写 MySQL 密码;Redis:**用户名通常留空** + 应用参数密码(自动尝试 `:pass@` / `default`)
|
||||
|
||||
### 常见错误 1045(Access denied for 'nexus'@'172.18.x.x')
|
||||
|
||||
|
||||
@@ -75,11 +75,16 @@ main() {
|
||||
|
||||
echo ""
|
||||
if [[ "$any" -eq 1 ]]; then
|
||||
ok "至少一种格式可用 — 将对应 URL 写入向导步骤 3 或 /app/.env 的 NEXUS_REDIS_URL"
|
||||
ok "至少一种格式可用"
|
||||
echo ""
|
||||
info "安装向导步骤 3 建议:Redis 用户名留空,只填密码(后端自动用 :pass@ 或 default)"
|
||||
info "已有 .env 时写入 NEXUS_REDIS_URL,例如:"
|
||||
info " redis://:${pass}@${redis_host}:6379/0"
|
||||
info " 或 redis://default:${pass}@${redis_host}:6379/0"
|
||||
exit 0
|
||||
fi
|
||||
fail "全部失败。请在 1Panel → Redis → 参数 核对密码,或检查 Nexus 是否在 1panel-network"
|
||||
info "本机 Redis 容器内: docker exec -it ${redis_host} redis-cli -u 'redis://root:密码@127.0.0.1:6379' ping"
|
||||
info "本机: docker exec -it ${redis_host} redis-cli -u 'redis://:密码@127.0.0.1:6379' ping"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# 2026-06-06 — 安装向导 Redis 自动探测 :pass@ / default
|
||||
|
||||
## 摘要
|
||||
|
||||
实测 1Panel Redis(`1Panel-redis-1g6K`)支持 `redis://:pass@` 与 `default:pass`,**不支持 root**。向导取消预填 root;`test-credentials` / `init-db` 自动依次尝试多种 URL 并写入可用项。
|
||||
|
||||
## 动机
|
||||
|
||||
用户 `test-1panel-redis.sh` 显示 root 失败、`:pass@` 与 default 成功;旧版预填 root 导致步骤 3 检测失败。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/api/install.py` — `_redis_url_candidates`、`_resolve_redis_url`
|
||||
- `web/app/install.html` — 用户名留空提示、清除 stale root
|
||||
- `deploy/test-1panel-redis.sh`、`docs/project/nexus-1panel-operations-knowledge.md`
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
pytest tests/test_security_unit.py -k "redis_url or resolve_redis" -q
|
||||
bash deploy/sync-install-wizard-to-container.sh
|
||||
```
|
||||
|
||||
步骤 3:Redis 用户名留空 + 密码 → 检测应显示「仅密码 (:pass@)」通过。
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
`sync-install-wizard-to-container.sh` 或 `nx update`。已有错误 `.env` 手动改为 `redis://:pass@1Panel-redis-xxx:6379/0`。
|
||||
@@ -122,7 +122,7 @@ entrypoint EXIT trap
|
||||
| 主机 | `1Panel-mysql-xxxx` | `1Panel-redis-xxxx` |
|
||||
| 端口 | `3306` | `6379` |
|
||||
| 库名/用户 | `nexus` / `nexus` | 库号 `0` |
|
||||
| 用户名 | — | **`root`**(1Panel Redis ACL,[官方文档](https://docs.1panel.pro/v2/user_manual/databases/redis/)) |
|
||||
| 用户名 | — | **通常留空**(自动 `redis://:pass@`);部分实例填 `default`。**勿填 root** |
|
||||
| 密码 | 1Panel「数据库」里建的 nexus 密码 | 1Panel Redis **应用参数**中的随机密码 |
|
||||
|
||||
### 3.3 Docker 预填来源
|
||||
@@ -147,8 +147,8 @@ mysql+aiomysql://nexus:密码@1Panel-mysql-xxxx:3306/nexus
|
||||
|
||||
| 场景 | URL 格式 |
|
||||
|------|----------|
|
||||
| 1Panel(推荐) | `redis://root:密码@1Panel-redis-xxxx:6379/0` |
|
||||
| 仅 requirepass、无用户名 | `redis://:密码@1Panel-redis-xxxx:6379/0` |
|
||||
| 1Panel(常见,推荐) | `redis://:密码@1Panel-redis-xxxx:6379/0`(向导用户名留空) |
|
||||
| 1Panel ACL default | `redis://default:密码@1Panel-redis-xxxx:6379/0` |
|
||||
| 无密码 | `redis://1Panel-redis-xxxx:6379/0` |
|
||||
|
||||
**历史 bug(807f4c2 前)**:生成 `redis://密码@host` 会把密码当成**用户名**,导致 `invalid username-password pair or user is disabled`。升级后须 `git pull` + `sync-install-wizard-to-container.sh`。
|
||||
@@ -245,9 +245,9 @@ cd /opt/nexus && bash deploy/install-nexus-fresh.sh --skip-clone
|
||||
|
||||
```
|
||||
1. 主机是否为 1Panel-redis-xxxx?(不是 127.0.0.1)
|
||||
2. 用户名是否 root?(1Panel 默认 ACL 用户)
|
||||
2. 用户名是否误填 root?(多数 1Panel 应**留空**或填 default)
|
||||
3. 密码是否与 1Panel → Redis → 参数 一致?
|
||||
4. 代码是否 ≥ 807f4c2?(URL 须 redis://root:pass@ 或 redis://:pass@)
|
||||
4. 运行 `bash deploy/test-1panel-redis.sh` 看哪种 URL 可用
|
||||
5. 运行诊断:
|
||||
cd /opt/nexus && bash deploy/test-1panel-redis.sh
|
||||
```
|
||||
|
||||
+50
-11
@@ -243,8 +243,9 @@ def _mysql_connect_error_detail(host: str, port: int) -> str:
|
||||
def _redis_auth_error_detail() -> str:
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
||||
return (
|
||||
"Redis 认证失败。1Panel Redis:用户名填 root,密码填应用参数中的随机密码;"
|
||||
"主机为 1Panel-redis-xxxx(不是 127.0.0.1)。无 ACL 的旧版 Redis 用户名可留空。"
|
||||
"Redis 认证失败。1Panel:主机 1Panel-redis-xxxx;密码为应用参数;"
|
||||
"用户名通常留空(自动尝试 :pass@)或填 default。勿填 root(多数实例不支持)。"
|
||||
"诊断: bash deploy/test-1panel-redis.sh"
|
||||
)
|
||||
return "Redis 密码错误或需要 AUTH;请核对用户名/密码(无密码则留空)。"
|
||||
|
||||
@@ -337,6 +338,42 @@ def _test_redis_url(redis_url: str) -> tuple[bool, str]:
|
||||
return False, f"✗ Redis 连接失败: {e}"
|
||||
|
||||
|
||||
def _redis_url_candidates(req: InitDbRequest) -> list[tuple[str, str]]:
|
||||
"""Try order for 1Panel Redis (default user vs :pass@ vs explicit user)."""
|
||||
password = (req.redis_pass or "").strip()
|
||||
if not password:
|
||||
return [("无密码", _build_redis_url(req))]
|
||||
|
||||
user = (req.redis_user or "").strip()
|
||||
seen: set[str] = set()
|
||||
out: list[tuple[str, str]] = []
|
||||
|
||||
def add(label: str, candidate: InitDbRequest) -> None:
|
||||
url = _build_redis_url(candidate)
|
||||
if url in seen:
|
||||
return
|
||||
seen.add(url)
|
||||
out.append((label, url))
|
||||
|
||||
if user:
|
||||
add(f"用户 {user}", req)
|
||||
add("仅密码 (:pass@)", req.model_copy(update={"redis_user": ""}))
|
||||
if user.lower() != "default":
|
||||
add("用户 default", req.model_copy(update={"redis_user": "default"}))
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_redis_url(req: InitDbRequest) -> tuple[bool, str, str | None]:
|
||||
"""Pick first working Redis URL among 1Panel-compatible auth variants."""
|
||||
last_msg = "✗ 未配置 Redis"
|
||||
for label, url in _redis_url_candidates(req):
|
||||
ok, msg = _test_redis_url(url)
|
||||
last_msg = msg
|
||||
if ok:
|
||||
return True, f"✓ Redis 连接正常({label})", url
|
||||
return False, last_msg, None
|
||||
|
||||
|
||||
def _docker_wizard_defaults() -> dict[str, str] | None:
|
||||
"""Prefill install step 3 when running inside docker-compose.prod.yml."""
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1":
|
||||
@@ -351,7 +388,6 @@ def _docker_wizard_defaults() -> dict[str, str] | None:
|
||||
"redis_host": redis_host,
|
||||
"redis_port": "6379",
|
||||
"redis_db": "0",
|
||||
"redis_user": "root",
|
||||
}
|
||||
|
||||
|
||||
@@ -715,17 +751,16 @@ async def env_check():
|
||||
return payload
|
||||
|
||||
|
||||
async def _credential_checks(req: InitDbRequest) -> tuple[list[dict], bool]:
|
||||
async def _credential_checks(req: InitDbRequest) -> tuple[list[dict], bool, str | None]:
|
||||
"""Verify MySQL + Redis with credentials from install step 3 (TCP + AUTH)."""
|
||||
db_url = _make_db_url(req)
|
||||
redis_url = _build_redis_url(req)
|
||||
mysql_ok, mysql_msg = await _test_mysql_url(db_url)
|
||||
redis_ok, redis_msg = _test_redis_url(redis_url)
|
||||
redis_ok, redis_msg, redis_url = _resolve_redis_url(req)
|
||||
checks = [
|
||||
{"name": "MySQL", "required": "可连接", "current": mysql_msg, "pass": mysql_ok},
|
||||
{"name": "Redis", "required": "可连接", "current": redis_msg, "pass": redis_ok},
|
||||
]
|
||||
return checks, mysql_ok and redis_ok
|
||||
return checks, mysql_ok and redis_ok, redis_url if redis_ok else None
|
||||
|
||||
|
||||
@router.post("/test-credentials")
|
||||
@@ -737,8 +772,11 @@ async def test_credentials(req: InitDbRequest):
|
||||
400,
|
||||
"系统已初始化,无法重复检测。请继续步骤 4 创建管理员,或修正 .env 后使用连接检测。",
|
||||
)
|
||||
checks, all_pass = await _credential_checks(req)
|
||||
return {"checks": checks, "all_pass": all_pass}
|
||||
checks, all_pass, redis_url = await _credential_checks(req)
|
||||
payload: dict = {"checks": checks, "all_pass": all_pass}
|
||||
if redis_url:
|
||||
payload["resolved_redis_url"] = redis_url
|
||||
return payload
|
||||
|
||||
|
||||
@router.get("/connection-check")
|
||||
@@ -768,16 +806,17 @@ async def init_db(req: InitDbRequest):
|
||||
_reject_if_env_exists()
|
||||
|
||||
db_url = _make_db_url(req)
|
||||
redis_url = _build_redis_url(req)
|
||||
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
|
||||
|
||||
checks, cred_ok = await _credential_checks(req)
|
||||
checks, cred_ok, redis_url = await _credential_checks(req)
|
||||
if not cred_ok:
|
||||
failed = next((c for c in checks if not c["pass"]), checks[0])
|
||||
raise HTTPException(
|
||||
400,
|
||||
failed.get("current") or "MySQL 或 Redis 连接未通过,请先检测账号密码",
|
||||
)
|
||||
if not redis_url:
|
||||
redis_url = _build_redis_url(req)
|
||||
|
||||
try:
|
||||
patch_aiomysql_do_ping()
|
||||
|
||||
@@ -222,16 +222,39 @@ def test_build_redis_url_password_with_leading_colon():
|
||||
assert _build_redis_url(req) == "redis://:redis_WP3NyN@1Panel-redis-x:6379/0"
|
||||
|
||||
|
||||
def test_build_redis_url_1panel_root_user():
|
||||
def test_build_redis_url_with_default_user():
|
||||
from server.api.install import InitDbRequest, _build_redis_url
|
||||
|
||||
req = InitDbRequest(
|
||||
db_pass="db",
|
||||
redis_host="1Panel-redis-x",
|
||||
redis_user="root",
|
||||
redis_user="default",
|
||||
redis_pass="redis_WP3NyN",
|
||||
)
|
||||
assert _build_redis_url(req) == "redis://root:redis_WP3NyN@1Panel-redis-x:6379/0"
|
||||
assert _build_redis_url(req) == "redis://default:redis_WP3NyN@1Panel-redis-x:6379/0"
|
||||
|
||||
|
||||
def test_resolve_redis_url_falls_back_to_colon_pass(monkeypatch):
|
||||
from server.api.install import InitDbRequest, _resolve_redis_url
|
||||
|
||||
req = InitDbRequest(
|
||||
db_pass="db",
|
||||
redis_host="1Panel-redis-x",
|
||||
redis_user="root",
|
||||
redis_pass="secret",
|
||||
)
|
||||
calls: list[str] = []
|
||||
|
||||
def fake_test(url: str) -> tuple[bool, str]:
|
||||
calls.append(url)
|
||||
return url.startswith("redis://:secret@"), "ok" if url.startswith("redis://:secret@") else "fail"
|
||||
|
||||
monkeypatch.setattr("server.api.install._test_redis_url", fake_test)
|
||||
ok, msg, url = _resolve_redis_url(req)
|
||||
assert ok is True
|
||||
assert url == "redis://:secret@1Panel-redis-x:6379/0"
|
||||
assert "仅密码" in msg
|
||||
assert any(u.startswith("redis://root:") for u in calls)
|
||||
|
||||
|
||||
def test_install_reject_repeat_init_when_env_exists(monkeypatch, tmp_path):
|
||||
|
||||
@@ -197,9 +197,9 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 用户名</label>
|
||||
<p class="text-xs text-[var(--text-secondary)] mb-1">1Panel Redis 填 <code class="bg-slate-100 px-1 rounded">root</code>;无用户留空</p>
|
||||
<input x-model="form.redis_user" @input="invalidateCredCheck()" placeholder="1Panel: root"
|
||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 用户名(可选)</label>
|
||||
<p class="text-xs text-[var(--text-secondary)] mb-1">1Panel 多数<strong>留空</strong>(仅密码);不行再填 <code class="bg-slate-100 px-1 rounded">default</code>。勿填 root</p>
|
||||
<input x-model="form.redis_user" @input="invalidateCredCheck()" placeholder="通常留空"
|
||||
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
||||
</div>
|
||||
<div>
|
||||
@@ -669,6 +669,11 @@ function installWizard() {
|
||||
return !h || h === 'localhost' || h === '127.0.0.1' || h === 'host.docker.internal';
|
||||
},
|
||||
|
||||
isStaleRedisUser(user) {
|
||||
const u = (user || '').trim().toLowerCase();
|
||||
return u === 'root';
|
||||
},
|
||||
|
||||
applyDockerDefaults(defaults) {
|
||||
if (!defaults) return;
|
||||
if (this.isStaleDockerHost(this.form.db_host) && defaults.db_host) {
|
||||
@@ -682,7 +687,7 @@ function installWizard() {
|
||||
if (defaults.db_port) this.form.db_port = defaults.db_port;
|
||||
if (defaults.redis_port) this.form.redis_port = defaults.redis_port;
|
||||
if (defaults.redis_db) this.form.redis_db = defaults.redis_db;
|
||||
if (defaults.redis_user && !this.form.redis_user) this.form.redis_user = defaults.redis_user;
|
||||
if (this.isStaleRedisUser(this.form.redis_user)) this.form.redis_user = '';
|
||||
},
|
||||
|
||||
async refreshDockerDefaults() {
|
||||
|
||||
Reference in New Issue
Block a user