diff --git a/.env.sample b/.env.sample index 9f373e8..c4f0010 100644 --- a/.env.sample +++ b/.env.sample @@ -144,8 +144,9 @@ CONFIG_TRUSTED_PROXY= # 完成身份冲突扫描和人工处置后才能启用手机号、邮箱和微信身份唯一索引。 CONFIG_AUTH_UNIQUE_INDEXES_ENABLED=false -# 仅在首次创建 admin 账号时使用;启动脚本会生成 20 位字母数字随机值并持久化到 .env。 -# 已存在 admin 的升级环境不会使用或修改现有管理员密码。 +# 仅在数据库首次创建 admin 账号时使用;启动脚本会生成 20 位字母数字随机值并持久化到 .env。 +# 首次部署完成页会明确显示登录账号和密码;初始密码没有独立过期时间,首次登录后请立即修改。 +# admin 创建后不再使用该值;修改 .env 或执行升级不会重置现有管理员密码。 CONFIG_INITIAL_ADMIN_PASSWORD= CONFIG_TOKEN_SIGN_SK=easyai2025easyai diff --git a/.github/workflows/test-deployment-public-url.yml b/.github/workflows/test-deployment-public-url.yml index d78cd84..70c3fd1 100644 --- a/.github/workflows/test-deployment-public-url.yml +++ b/.github/workflows/test-deployment-public-url.yml @@ -12,6 +12,7 @@ on: - 'scripts/test-public-api-base-url.sh' - 'scripts/init-server-http-bind-ip.sh' - 'scripts/test-server-http-bind-ip.sh' + - 'scripts/init-security-env.sh' - 'scripts/test-deployment-public-url.sh' - '.github/workflows/test-deployment-public-url.yml' pull_request: @@ -25,6 +26,7 @@ on: - 'scripts/test-public-api-base-url.sh' - 'scripts/init-server-http-bind-ip.sh' - 'scripts/test-server-http-bind-ip.sh' + - 'scripts/init-security-env.sh' - 'scripts/test-deployment-public-url.sh' - '.github/workflows/test-deployment-public-url.yml' workflow_dispatch: @@ -37,7 +39,7 @@ jobs: - name: Check shell syntax run: | - bash -n start.sh update.sh scripts/init-public-api-base-url.sh scripts/init-server-http-bind-ip.sh scripts/test-public-api-base-url.sh scripts/test-server-http-bind-ip.sh scripts/test-deployment-public-url.sh + bash -n start.sh update.sh scripts/init-security-env.sh scripts/init-public-api-base-url.sh scripts/init-server-http-bind-ip.sh scripts/test-public-api-base-url.sh scripts/test-server-http-bind-ip.sh scripts/test-deployment-public-url.sh - name: Test migration helper run: bash scripts/test-public-api-base-url.sh diff --git a/.github/workflows/test-start-ps1.yml b/.github/workflows/test-start-ps1.yml index 39eb0f1..518de26 100644 --- a/.github/workflows/test-start-ps1.yml +++ b/.github/workflows/test-start-ps1.yml @@ -36,7 +36,17 @@ jobs: DEPLOY_IP: "192.168.1.100" run: | (Get-Content .env.sample) -replace '^SERVER_HTTP_PORT=.*$', 'SERVER_HTTP_PORT=4100' | Set-Content .env.sample -Encoding UTF8 - powershell -ExecutionPolicy Bypass -File .\start.ps1 + $output = powershell -ExecutionPolicy Bypass -File .\start.ps1 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { Write-Host $output; exit $LASTEXITCODE } + $adminPassword = (Select-String -Path .env -Pattern '^CONFIG_INITIAL_ADMIN_PASSWORD=' | ForEach-Object { $_.Line.Substring('CONFIG_INITIAL_ADMIN_PASSWORD='.Length) }) + if ([string]::IsNullOrWhiteSpace($adminPassword)) { throw 'Initial admin password was not generated' } + if (-not $output.Contains('登录账号: admin')) { throw 'Deployment output does not show the admin username' } + if (-not $output.Contains("登录密码: $adminPassword")) { throw 'Deployment output does not show the generated admin password' } + if (-not $output.Contains('初始密码没有独立过期时间')) { throw 'Deployment output does not explain the initial password lifetime' } + $existingOutput = powershell -ExecutionPolicy Bypass -File .\start.ps1 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { Write-Host $existingOutput; exit $LASTEXITCODE } + if (-not $existingOutput.Contains('本次部署不会生成或重置已有密码')) { throw 'Existing-env output does not explain password preservation' } + if ($existingOutput.Contains("登录密码: $adminPassword")) { throw 'Existing-env output must not report the env value as the current password' } - name: Verify .env run: | diff --git a/README.md b/README.md index 23a4322..62e60c9 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,20 @@ DEPLOY_ACCESS=domain DEPLOY_DOMAIN=demo.example.com DEPLOY_HTTPS=true CERTBOT_EM `start.sh` / `start.ps1` 和 `update.sh` / `update.ps1` 会自动初始化并持久化鉴权安全密钥。首次安装或存量升级发现 `WS_AUTH_WS_TICKET_SECRET` 缺失、过短时,会生成新的高强度随机值,并确保 `WS_AUTH_METHODS` 包含 `ws_ticket`;Compose 会把同一密钥同时注入 `easyai-server` 与 `ws-gateway`。已有合格密钥会原样保留,重复启动或升级不会轮换,用户无需手动填写或复制密钥。 -生成结果仅保存在本机 `.env`,脚本日志只显示变量名而不会输出密钥内容。不要把 `.env` 提交到 Git;集群部署时,各 `easyai-server` / `ws-gateway` 实例必须使用同一份持久化配置。 +生成结果仅保存在本机 `.env`。除首次安装完成页会明确显示登录用的初始管理员密码外,脚本日志对其他安全密钥只显示变量名,不输出密钥内容。不要把 `.env` 提交到 Git;集群部署时,各 `easyai-server` / `ws-gateway` 实例必须使用同一份持久化配置。 + +#### 初始管理员密码生命周期 + +首次安装时,部署脚本会生成 20 位随机密码,写入 `.env` 的 `CONFIG_INITIAL_ADMIN_PASSWORD`,并在部署完成页直接显示: + +```text +登录账号: admin +登录密码: <本次部署生成的随机密码> +``` + +后端只在数据库中不存在 `admin` 时读取该值并创建管理员。创建成功后,`.env` 中的值不再用于登录校验,修改该变量或执行升级都不会重置现有管理员密码。初始密码没有独立的自动过期时间,在管理员主动修改前持续有效,因此首次登录后应立即修改密码。 + +如果部署脚本检测到已有 `.env`,完成页不会把其中的值误报成当前密码:数据库已有 `admin` 时继续使用现有密码;数据库尚未创建 `admin` 时,可查看 `.env -> CONFIG_INITIAL_ADMIN_PASSWORD`。 #### 后端公开 API 地址 diff --git a/docs/Windows一键部署方案.md b/docs/Windows一键部署方案.md index cb8406f..c31c393 100644 --- a/docs/Windows一键部署方案.md +++ b/docs/Windows一键部署方案.md @@ -156,6 +156,15 @@ powershell -ExecutionPolicy Bypass -File .\start.ps1 - 本地访问:`http://127.0.0.1:3010` - 局域网访问:`http://:3010` +首次安装还会在完成页直接显示管理员登录信息: + +```text +登录账号: admin +登录密码: <本次部署生成的 20 位随机密码> +``` + +该密码只在数据库首次创建 `admin` 时使用,创建后修改 `.env` 不会重置现有密码。初始密码没有独立过期时间,首次登录后应立即修改。已有 `.env` 的部署只提示密码沿用规则,不会把可能已经失效的环境变量值显示成当前登录密码。 + --- ## 7. 实现清单 diff --git a/scripts/test-deployment-public-url.sh b/scripts/test-deployment-public-url.sh index 1d391c7..28e6414 100755 --- a/scripts/test-deployment-public-url.sh +++ b/scripts/test-deployment-public-url.sh @@ -98,10 +98,29 @@ DEPLOY_NON_INTERACTIVE=1 \ DEPLOY_DRY_RUN=1 \ DEPLOY_ACCESS=ip \ DEPLOY_IP=10.0.0.8 \ - bash start.sh >/dev/null + bash start.sh > "$TMP_DIR/first-install.log" grep -qx 'NUXT_PUBLIC_BASE_APIURL=http://10.0.0.8:4100' .env grep -qx 'CONFIG_PUBLIC_API_BASE_URL=http://10.0.0.8:4100' .env grep -qx 'SERVER_HTTP_BIND_IP=0.0.0.0' .env +initial_admin_password="$(awk -F= '$1 == "CONFIG_INITIAL_ADMIN_PASSWORD" { print $2; exit }' .env)" +[ "${#initial_admin_password}" -ge 12 ] || { + echo "Initial admin password was not generated" >&2 + exit 1 +} +grep -Fq '登录账号: admin' "$TMP_DIR/first-install.log" +grep -Fq "登录密码: ${initial_admin_password}" "$TMP_DIR/first-install.log" +grep -Fq '生命周期: 仅在数据库首次创建 admin 时使用;创建后修改 .env 不会重置密码。' "$TMP_DIR/first-install.log" +grep -Fq '有效期: 初始密码没有独立过期时间,在管理员修改密码前持续有效。' "$TMP_DIR/first-install.log" +DEPLOY_NON_INTERACTIVE=1 \ + DEPLOY_DRY_RUN=1 \ + DEPLOY_ACCESS=ip \ + DEPLOY_IP=10.0.0.8 \ + bash start.sh > "$TMP_DIR/existing-env.log" +grep -Fq '管理员账号: admin(本次部署不会生成或重置已有密码)' "$TMP_DIR/existing-env.log" +if grep -Fq "登录密码: ${initial_admin_password}" "$TMP_DIR/existing-env.log"; then + echo "Existing-env deployment must not report CONFIG_INITIAL_ADMIN_PASSWORD as the current login password" >&2 + exit 1 +fi if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then assert_compose_exposure 0.0.0.0 assert_canvas_ws_auth_config diff --git a/start.ps1 b/start.ps1 index 6b96d9c..ad05e9c 100644 --- a/start.ps1 +++ b/start.ps1 @@ -358,6 +358,32 @@ function Start-Services { Ok "EasyAI started" } +function Show-AdminLoginInfo { + Write-Host "" + if ($script:SecurityEnvMode -ne "new") { + Write-Host " 管理员账号: admin(本次部署不会生成或重置已有密码)" -ForegroundColor Yellow + Write-Host " 登录提示: 若数据库尚未创建 admin,请查看 .env -> CONFIG_INITIAL_ADMIN_PASSWORD;若 admin 已存在,请使用当前密码。" -ForegroundColor DarkGray + return + } + + $content = Get-Content (Join-Path $script:Root ".env") -Raw -Encoding UTF8 + $initialAdminPassword = Get-EnvValue $content "CONFIG_INITIAL_ADMIN_PASSWORD" + if ([string]::IsNullOrWhiteSpace($initialAdminPassword)) { + Fail "Cannot read initial admin password from .env -> CONFIG_INITIAL_ADMIN_PASSWORD" + } + + $heading = if ($script:DeployDryRun) { + " -------- 初始管理员(Docker 首次启动后生效)--------" + } else { + " -------- 初始管理员(首次登录后请立即修改密码)--------" + } + Write-Host $heading -ForegroundColor Yellow + Write-Host " 登录账号: " -NoNewline; Write-Host "admin" -ForegroundColor White + Write-Host " 登录密码: " -NoNewline; Write-Host $initialAdminPassword -ForegroundColor White + Write-Host " 生命周期: 仅在数据库首次创建 admin 时使用;创建后修改 .env 不会重置密码。" -ForegroundColor DarkGray + Write-Host " 有效期: 初始密码没有独立过期时间,在管理员修改密码前持续有效。" -ForegroundColor DarkGray +} + function Main { Init-ProjectDir @@ -425,19 +451,14 @@ function Main { Write-Host "================================" -ForegroundColor Yellow Write-Host "" Write-Host " 预期访问: " -NoNewline; Write-Host "http://${ip}:3010" -ForegroundColor Cyan - Write-Host "" - Write-Host " 初始管理员: admin;随机密码保存在 .env 的 CONFIG_INITIAL_ADMIN_PASSWORD" -ForegroundColor DarkGray } else { Write-Host "================================" -ForegroundColor Green Write-Host " 部署成功" -ForegroundColor Green Write-Host "================================" -ForegroundColor Green Write-Host "" Write-Host " 访问地址: " -NoNewline; Write-Host "http://${ip}:3010" -ForegroundColor Cyan - Write-Host "" - Write-Host " -------- 初始管理员(首次登录后请修改密码)--------" -ForegroundColor Yellow - Write-Host " 账号: " -NoNewline; Write-Host "admin" -ForegroundColor White - Write-Host " 密码位置: " -NoNewline; Write-Host ".env -> CONFIG_INITIAL_ADMIN_PASSWORD" -ForegroundColor White } + Show-AdminLoginInfo Write-Host "" Wait-ForExit } diff --git a/start.sh b/start.sh index ca95591..6a7e304 100755 --- a/start.sh +++ b/start.sh @@ -457,6 +457,32 @@ run_https_setup() { fi } +show_admin_login_info() { + echo "" + if [ "$SECURITY_ENV_MODE" != "new" ]; then + echo "管理员账号: admin(本次部署不会生成或重置已有密码)" + echo "登录提示: 若数据库尚未创建 admin,请查看 .env -> CONFIG_INITIAL_ADMIN_PASSWORD;若 admin 已存在,请使用当前密码。" + return 0 + fi + + local initial_admin_password + initial_admin_password="$(read_env_value .env "CONFIG_INITIAL_ADMIN_PASSWORD")" + if [ -z "$initial_admin_password" ]; then + echo "❌ 未能读取初始管理员密码: .env -> CONFIG_INITIAL_ADMIN_PASSWORD" >&2 + return 1 + fi + + if [ "$DEPLOY_DRY_RUN" = "1" ]; then + echo "-------- 初始管理员(Docker 首次启动后生效)--------" + else + echo "-------- 初始管理员(首次登录后请立即修改密码)--------" + fi + echo "登录账号: admin" + echo "登录密码: ${initial_admin_password}" + echo "生命周期: 仅在数据库首次创建 admin 时使用;创建后修改 .env 不会重置密码。" + echo "有效期: 初始密码没有独立过期时间,在管理员修改密码前持续有效。" +} + # ==================== 主流程 ==================== main() { init_project_dir @@ -551,6 +577,7 @@ main() { echo "HTTPS 已启用" fi fi + show_admin_login_info echo "" }