diff --git a/deploy/README-1panel.md b/deploy/README-1panel.md index c9ae2897..2bd8c3ab 100644 --- a/deploy/README-1panel.md +++ b/deploy/README-1panel.md @@ -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') diff --git a/deploy/test-1panel-redis.sh b/deploy/test-1panel-redis.sh index ccf284c2..56da39df 100644 --- a/deploy/test-1panel-redis.sh +++ b/deploy/test-1panel-redis.sh @@ -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 } diff --git a/docs/changelog/2026-06-06-install-redis-auto-resolve.md b/docs/changelog/2026-06-06-install-redis-auto-resolve.md new file mode 100644 index 00000000..e8517e41 --- /dev/null +++ b/docs/changelog/2026-06-06-install-redis-auto-resolve.md @@ -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`。 diff --git a/docs/project/nexus-1panel-operations-knowledge.md b/docs/project/nexus-1panel-operations-knowledge.md index ab6e36db..8cac39b1 100644 --- a/docs/project/nexus-1panel-operations-knowledge.md +++ b/docs/project/nexus-1panel-operations-knowledge.md @@ -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 ``` diff --git a/server/api/install.py b/server/api/install.py index 572643c7..97685b89 100644 --- a/server/api/install.py +++ b/server/api/install.py @@ -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() diff --git a/tests/test_security_unit.py b/tests/test_security_unit.py index c76992b6..41fddaaa 100644 --- a/tests/test_security_unit.py +++ b/tests/test_security_unit.py @@ -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): diff --git a/web/app/install.html b/web/app/install.html index 624865ab..7d4ec0c0 100644 --- a/web/app/install.html +++ b/web/app/install.html @@ -197,9 +197,9 @@
- -

1Panel Redis 填 root;无用户留空

- Redis 用户名(可选) +

1Panel 多数留空(仅密码);不行再填 default。勿填 root

+
@@ -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() {