# 许可证服务器历史内部部署脚本 # 状态:默认禁止执行,不作为客户交付入口 param( [string]$Server = "", [string]$User = "root", [string]$SshKey = "", [string]$RemotePath = "/opt/license-server", [switch]$SkipBuild = $false, [switch]$AcknowledgeLegacyInternalDeploy ) $ErrorActionPreference = "Stop" if (-not $AcknowledgeLegacyInternalDeploy) { Write-Host "[ERROR] license-server/deploy.ps1 是历史内部部署脚本,默认禁止执行。" -ForegroundColor Red Write-Host "许可证服务是授权中枢,正式部署前必须先确认目标服务器、密钥、版本镜像和回滚方案。" -ForegroundColor Yellow Write-Host "如确需排查历史链路,请追加 -AcknowledgeLegacyInternalDeploy,并显式传入 -Server 和 -SshKey。" -ForegroundColor Yellow exit 1 } if ([string]::IsNullOrWhiteSpace($Server) -or [string]::IsNullOrWhiteSpace($SshKey)) { throw "请显式传入 -Server 和 -SshKey,脚本不再默认指向旧服务器或旧 SSH key。" } function New-RandomHex { param([int]$Bytes = 32) $buffer = New-Object byte[] $Bytes [System.Security.Cryptography.RandomNumberGenerator]::Fill($buffer) return ([System.BitConverter]::ToString($buffer)).Replace("-", "").ToLower() } function New-RandomBase64Url { param([int]$Bytes = 18) $buffer = New-Object byte[] $Bytes [System.Security.Cryptography.RandomNumberGenerator]::Fill($buffer) $base64 = [Convert]::ToBase64String($buffer) return $base64.TrimEnd('=').Replace('+', '-').Replace('/', '_') } function Convert-ToEscapedEnvMultiline { param([string]$Value) return ($Value.Trim() -replace "`r", "" -replace "`n", "\n") } function New-RsaKeyPair { $rsa = [System.Security.Cryptography.RSA]::Create(2048) try { return @{ PrivateKey = $rsa.ExportPkcs8PrivateKeyPem() PublicKey = $rsa.ExportSubjectPublicKeyInfoPem() } } finally { $rsa.Dispose() } } function Get-EnvValue { param( [string]$Content, [string]$Key ) $match = [regex]::Match($Content, "(?m)^" + [regex]::Escape($Key) + "=(.*)$") if (-not $match.Success) { return $null } $value = $match.Groups[1].Value.Trim() if ($value.StartsWith('"') -and $value.EndsWith('"')) { return $value.Substring(1, $value.Length - 2) } return $value } function Test-EnvConfig { param([string]$Content) $requiredKeys = @( "POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_DB", "MAIN_DATABASE_URL", "MAIN_DB_ENCRYPTION_KEY", "DATABASE_URL", "RSA_PRIVATE_KEY", "RSA_PUBLIC_KEY", "AES_SECRET_KEY", "JWT_SECRET", "ADMIN_INITIAL_PASSWORD" ) foreach ($key in $requiredKeys) { $value = Get-EnvValue -Content $Content -Key $key if ([string]::IsNullOrWhiteSpace($value)) { throw ".env 缺少必填项: $key" } } $blockedPatterns = @( "your-jwt-secret", "your-rsa-private-key", "your-main-db-encryption-key", "your-32-byte-hex-key", "replace-with", "admin123", "license_secret" ) foreach ($pattern in $blockedPatterns) { if ($Content -match [regex]::Escape($pattern)) { throw ".env 中仍包含不安全占位值: $pattern" } } $jwtSecret = Get-EnvValue -Content $Content -Key "JWT_SECRET" if ($jwtSecret.Length -lt 32) { throw "JWT_SECRET 长度不足 32 字节" } $mainDbEncryptionKey = Get-EnvValue -Content $Content -Key "MAIN_DB_ENCRYPTION_KEY" if ($mainDbEncryptionKey.Length -lt 32) { throw "MAIN_DB_ENCRYPTION_KEY 长度不足 32 字节" } $aesSecretKey = Get-EnvValue -Content $Content -Key "AES_SECRET_KEY" if ($aesSecretKey.Length -lt 64) { throw "AES_SECRET_KEY 需要至少 64 个十六进制字符" } } Write-Host "========================================" -ForegroundColor Cyan Write-Host "许可证服务器部署" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" Write-Host "目标服务器: $User@$Server" -ForegroundColor Yellow Write-Host "远程路径: $RemotePath" -ForegroundColor Yellow Write-Host "" # SSH 命令封装 function Invoke-RemoteCommand { param([string]$Command) ssh -i $SshKey "$User@$Server" $Command if ($LASTEXITCODE -ne 0) { throw "远程命令执行失败: $Command" } } # SCP 上传 function Upload-File { param([string]$Local, [string]$Remote) scp -i $SshKey -r $Local "$User@$Server`:$Remote" if ($LASTEXITCODE -ne 0) { throw "文件上传失败: $Local" } } # 1. 检查 SSH 连接 Write-Host "1. 检查 SSH 连接..." -ForegroundColor White try { Invoke-RemoteCommand "echo 'SSH 连接成功'" Write-Host " SSH 连接成功 ✓" -ForegroundColor Green } catch { Write-Host " SSH 连接失败,请检查密钥和网络" -ForegroundColor Red exit 1 } # 2. 创建远程目录 Write-Host "2. 创建远程目录..." -ForegroundColor White Invoke-RemoteCommand "mkdir -p $RemotePath" Write-Host " 目录创建成功 ✓" -ForegroundColor Green # 3. 上传文件 Write-Host "3. 上传项目文件..." -ForegroundColor White # 创建临时目录存放要上传的文件 $tempDir = Join-Path $env:TEMP "license-server-deploy" if (Test-Path $tempDir) { Remove-Item -Recurse -Force $tempDir } New-Item -ItemType Directory -Path $tempDir | Out-Null # 复制必要文件 Copy-Item -Path "docker-compose.yml" -Destination $tempDir Copy-Item -Path "Dockerfile" -Destination $tempDir Copy-Item -Path "package*.json" -Destination $tempDir Copy-Item -Path "next.config.js" -Destination $tempDir Copy-Item -Path "tsconfig.json" -Destination $tempDir Copy-Item -Path "tailwind.config.ts" -Destination $tempDir Copy-Item -Path "postcss.config.js" -Destination $tempDir Copy-Item -Path "prisma" -Destination $tempDir -Recurse Copy-Item -Path "app" -Destination $tempDir -Recurse Copy-Item -Path "lib" -Destination $tempDir -Recurse Copy-Item -Path "components" -Destination $tempDir -Recurse -ErrorAction SilentlyContinue if (Test-Path "public") { Copy-Item -Path "public" -Destination $tempDir -Recurse } # 上传 Upload-File "$tempDir/*" $RemotePath Write-Host " 文件上传成功 ✓" -ForegroundColor Green # 清理临时目录 Remove-Item -Recurse -Force $tempDir # 4. 创建 .env 文件(如果不存在) Write-Host "4. 检查环境变量配置..." -ForegroundColor White $envExists = ssh -i $SshKey "$User@$Server" "test -f $RemotePath/.env && echo 'exists'" if ($envExists -ne "exists") { Write-Host " 生成安全 .env 配置..." -ForegroundColor Yellow $postgresUser = "license" $postgresPassword = New-RandomBase64Url $postgresDb = "license_db" $jwtSecret = New-RandomHex $mainDbEncryptionKey = New-RandomHex $aesSecretKey = New-RandomHex $adminInitialPassword = New-RandomBase64Url $rsaKeys = New-RsaKeyPair $mainDatabaseUrl = "postgresql://${postgresUser}:${postgresPassword}@postgres:5432/${postgresDb}?schema=main" $projectDatabaseUrl = "postgresql://${postgresUser}:${postgresPassword}@postgres:5432/${postgresDb}?schema=public" $envContent = @" # 许可证服务器环境变量 POSTGRES_USER=$postgresUser POSTGRES_PASSWORD=$postgresPassword POSTGRES_DB=$postgresDb MAIN_DATABASE_URL=$mainDatabaseUrl MAIN_DB_ENCRYPTION_KEY=$mainDbEncryptionKey DATABASE_URL=$projectDatabaseUrl RSA_PRIVATE_KEY=$(Convert-ToEscapedEnvMultiline $rsaKeys.PrivateKey) RSA_PUBLIC_KEY=$(Convert-ToEscapedEnvMultiline $rsaKeys.PublicKey) AES_SECRET_KEY=$aesSecretKey JWT_SECRET=$jwtSecret ADMIN_INITIAL_PASSWORD=$adminInitialPassword "@ ssh -i $SshKey "$User@$Server" "cat > $RemotePath/.env << 'EOF' $envContent EOF" Write-Host " 已生成安全 .env 文件 ✓" -ForegroundColor Green Write-Host " 初始管理员密码已写入远程 .env 的 ADMIN_INITIAL_PASSWORD,不在部署日志中输出明文" -ForegroundColor Yellow } else { $remoteEnv = ssh -i $SshKey "$User@$Server" "cat $RemotePath/.env" Test-EnvConfig -Content $remoteEnv Write-Host " .env 文件已存在且通过安全检查 ✓" -ForegroundColor Green } # 5. 构建和启动 Write-Host "5. 构建和启动服务..." -ForegroundColor White Invoke-RemoteCommand "cd $RemotePath && docker compose down 2>/dev/null || true" Invoke-RemoteCommand "cd $RemotePath && docker compose build --no-cache" Invoke-RemoteCommand "cd $RemotePath && docker compose up -d" Write-Host " 服务启动成功 ✓" -ForegroundColor Green # 6. 等待服务就绪 Write-Host "6. 等待服务就绪..." -ForegroundColor White Start-Sleep -Seconds 10 # 7. 运行数据库迁移 Write-Host "7. 运行数据库迁移..." -ForegroundColor White Invoke-RemoteCommand "cd $RemotePath && docker compose exec -T license-server npm run db:push" Write-Host " 数据库迁移成功 ✓" -ForegroundColor Green # 8. 检查服务状态 Write-Host "8. 检查服务状态..." -ForegroundColor White $status = ssh -i $SshKey "$User@$Server" "docker compose -f $RemotePath/docker-compose.yml ps --format 'table {{.Name}}\t{{.Status}}'" Write-Host $status Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host "部署完成!" -ForegroundColor Green Write-Host "========================================" -ForegroundColor Cyan Write-Host "" Write-Host "许可证服务器地址: http://$Server`:9527" -ForegroundColor Yellow Write-Host "" Write-Host "后续步骤:" -ForegroundColor White Write-Host "1. 查看安全配置: ssh -i $SshKey $User@$Server 'sed -n \"1,20p\" $RemotePath/.env'" -ForegroundColor Gray Write-Host "2. 重启服务: ssh -i $SshKey $User@$Server 'cd $RemotePath && docker compose restart'" -ForegroundColor Gray Write-Host "3. 初始化超管: ssh -i $SshKey $User@$Server 'cd $RemotePath && docker compose exec license-server npx ts-node scripts/init-admin.ts admin'" -ForegroundColor Gray