fix(install): correct Redis URL auth for 1Panel root user
Password must use redis://:pass@ or redis://root:pass@ host; wizard adds redis_user field defaulting to root on 1Panel Docker installs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -119,7 +119,7 @@ Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安
|
|||||||
1. **应用商店** → 安装 **MySQL**、**Redis**
|
1. **应用商店** → 安装 **MySQL**、**Redis**
|
||||||
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
|
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
|
||||||
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`)
|
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`)
|
||||||
4. 填写 MySQL 密码;Redis 若设了密码一并填写
|
4. 填写 MySQL 密码;Redis:**用户名 `root`** + 应用参数中的密码(见 1Panel 文档)
|
||||||
|
|
||||||
### 常见错误 1045(Access denied for 'nexus'@'172.18.x.x')
|
### 常见错误 1045(Access denied for 'nexus'@'172.18.x.x')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 2026-06-06 — 修复安装向导 Redis URL 与 1Panel root 用户
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
步骤 3 Redis 连接失败根因:`_build_redis_url` 生成 `redis://pass@host`(密码被当成用户名);1Panel Redis ACL 用户名为 `root`。
|
||||||
|
|
||||||
|
## 修复
|
||||||
|
|
||||||
|
- URL 密码格式改为 `redis://:pass@host` 或 `redis://root:pass@host`
|
||||||
|
- 向导新增 Redis 用户名,Docker/1Panel 预填 `root`
|
||||||
|
- 错误提示说明 1Panel 用法
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `server/api/install.py`
|
||||||
|
- `web/app/install.html`
|
||||||
|
- `tests/test_security_unit.py`
|
||||||
|
- `deploy/README-1panel.md`
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_security_unit.py -k build_redis_url -q
|
||||||
|
bash deploy/sync-install-wizard-to-container.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
步骤 3:Redis 用户 `root`、密码 `redis_WP3NyN`、主机 `1Panel-redis-xxxx` → 检测通过。
|
||||||
|
|
||||||
|
## 已有 .env 时手动修正
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXUS_REDIS_URL="redis://root:你的密码@1Panel-redis-xxxx:6379/0"
|
||||||
|
```
|
||||||
+16
-8
@@ -62,6 +62,7 @@ class InitDbRequest(BaseModel):
|
|||||||
redis_host: str = "127.0.0.1"
|
redis_host: str = "127.0.0.1"
|
||||||
redis_port: str = "6379"
|
redis_port: str = "6379"
|
||||||
redis_db: str = "0"
|
redis_db: str = "0"
|
||||||
|
redis_user: str = ""
|
||||||
redis_pass: str = ""
|
redis_pass: str = ""
|
||||||
api_port: str = "8600"
|
api_port: str = "8600"
|
||||||
timezone: str = "Asia/Shanghai"
|
timezone: str = "Asia/Shanghai"
|
||||||
@@ -92,11 +93,17 @@ def _make_db_url(req: InitDbRequest | CreateAdminRequest) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _build_redis_url(req: InitDbRequest) -> str:
|
def _build_redis_url(req: InitDbRequest) -> str:
|
||||||
url = "redis://"
|
"""Build redis:// URL. Password-only auth must use redis://:pass@host (leading colon)."""
|
||||||
if req.redis_pass:
|
user = (req.redis_user or "").strip()
|
||||||
url += f"{quote_plus(req.redis_pass)}@"
|
password = (req.redis_pass or "").strip()
|
||||||
url += f"{req.redis_host}:{req.redis_port}/{req.redis_db}"
|
auth = ""
|
||||||
return url
|
if user and password:
|
||||||
|
auth = f"{quote_plus(user)}:{quote_plus(password)}@"
|
||||||
|
elif password:
|
||||||
|
auth = f":{quote_plus(password)}@"
|
||||||
|
elif user:
|
||||||
|
auth = f"{quote_plus(user)}@"
|
||||||
|
return f"redis://{auth}{req.redis_host}:{req.redis_port}/{req.redis_db}"
|
||||||
|
|
||||||
|
|
||||||
def _read_1panel_hosts_file() -> dict[str, str]:
|
def _read_1panel_hosts_file() -> dict[str, str]:
|
||||||
@@ -236,10 +243,10 @@ def _mysql_connect_error_detail(host: str, port: int) -> str:
|
|||||||
def _redis_auth_error_detail() -> str:
|
def _redis_auth_error_detail() -> str:
|
||||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
||||||
return (
|
return (
|
||||||
"Redis 密码错误或用户被禁用(1Panel Redis 请在步骤 3 填写应用参数中的密码;"
|
"Redis 认证失败。1Panel Redis:用户名填 root,密码填应用参数中的随机密码;"
|
||||||
"无密码则留空)。主机应为 1Panel-redis-xxxx,不是 127.0.0.1。"
|
"主机为 1Panel-redis-xxxx(不是 127.0.0.1)。无 ACL 的旧版 Redis 用户名可留空。"
|
||||||
)
|
)
|
||||||
return "Redis 密码错误或需要 AUTH;请核对 Redis 密码(无密码则留空)。"
|
return "Redis 密码错误或需要 AUTH;请核对用户名/密码(无密码则留空)。"
|
||||||
|
|
||||||
|
|
||||||
def _redis_connect_error_detail(host: str, port: int) -> str:
|
def _redis_connect_error_detail(host: str, port: int) -> str:
|
||||||
@@ -344,6 +351,7 @@ def _docker_wizard_defaults() -> dict[str, str] | None:
|
|||||||
"redis_host": redis_host,
|
"redis_host": redis_host,
|
||||||
"redis_port": "6379",
|
"redis_port": "6379",
|
||||||
"redis_db": "0",
|
"redis_db": "0",
|
||||||
|
"redis_user": "root",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,29 @@ async def test_install_test_credentials_all_pass(monkeypatch, tmp_path):
|
|||||||
assert len(out["checks"]) == 2
|
assert len(out["checks"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_redis_url_password_with_leading_colon():
|
||||||
|
from server.api.install import InitDbRequest, _build_redis_url
|
||||||
|
|
||||||
|
req = InitDbRequest(
|
||||||
|
db_pass="db",
|
||||||
|
redis_host="1Panel-redis-x",
|
||||||
|
redis_pass="redis_WP3NyN",
|
||||||
|
)
|
||||||
|
assert _build_redis_url(req) == "redis://:redis_WP3NyN@1Panel-redis-x:6379/0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_redis_url_1panel_root_user():
|
||||||
|
from server.api.install import InitDbRequest, _build_redis_url
|
||||||
|
|
||||||
|
req = InitDbRequest(
|
||||||
|
db_pass="db",
|
||||||
|
redis_host="1Panel-redis-x",
|
||||||
|
redis_user="root",
|
||||||
|
redis_pass="redis_WP3NyN",
|
||||||
|
)
|
||||||
|
assert _build_redis_url(req) == "redis://root:redis_WP3NyN@1Panel-redis-x:6379/0"
|
||||||
|
|
||||||
|
|
||||||
def test_install_reject_repeat_init_when_env_exists(monkeypatch, tmp_path):
|
def test_install_reject_repeat_init_when_env_exists(monkeypatch, tmp_path):
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from server.api import install as install_api
|
from server.api import install as install_api
|
||||||
|
|||||||
+15
-6
@@ -197,14 +197,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4 mt-3">
|
<div class="grid grid-cols-2 gap-4 mt-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
|
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 用户名</label>
|
||||||
<input x-model="form.redis_db" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
|
<p class="text-xs text-[var(--text-secondary)] mb-1">1Panel Redis 填 <code class="bg-slate-100 px-1 rounded">root</code>;无用户留空</p>
|
||||||
</div>
|
<input x-model="form.redis_user" @input="invalidateCredCheck()" placeholder="1Panel: root"
|
||||||
<div>
|
|
||||||
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 密码(可选)</label>
|
|
||||||
<input x-model="form.redis_pass" @input="invalidateCredCheck()" type="password" 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">
|
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>
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
|
||||||
|
<input x-model="form.redis_db" @input="invalidateCredCheck()" 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>
|
||||||
|
<div class="mt-3">
|
||||||
|
<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 → 参数中的密码;无密码留空</p>
|
||||||
|
<input x-model="form.redis_pass" @input="invalidateCredCheck()" type="password" 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -636,6 +643,7 @@ function installWizard() {
|
|||||||
redis_host: '127.0.0.1',
|
redis_host: '127.0.0.1',
|
||||||
redis_port: '6379',
|
redis_port: '6379',
|
||||||
redis_db: '0',
|
redis_db: '0',
|
||||||
|
redis_user: '',
|
||||||
redis_pass: '',
|
redis_pass: '',
|
||||||
api_port: '8600',
|
api_port: '8600',
|
||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
@@ -674,6 +682,7 @@ function installWizard() {
|
|||||||
if (defaults.db_port) this.form.db_port = defaults.db_port;
|
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_port) this.form.redis_port = defaults.redis_port;
|
||||||
if (defaults.redis_db) this.form.redis_db = defaults.redis_db;
|
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;
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshDockerDefaults() {
|
async refreshDockerDefaults() {
|
||||||
|
|||||||
Reference in New Issue
Block a user