2026-06-02 19:25:48 +08:00

289 lines
11 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 许可证服务器历史内部部署脚本
# 状态:默认禁止执行,不作为客户交付入口
param(
[string]$Server = "",
[string]$User = "root",
[string]$SshKey = "",
[string]$RemotePath = "/opt/license-server",
[string]$PremiumModuleRegistryBase = "",
[string]$PremiumModuleVersion = "1.0.0",
[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",
"LICENSE_REQUEST_SIGNING_SECRET",
"MODULE_PULL_TOKEN_SECRET",
"PREMIUM_MODULE_REGISTRY_BASE",
"PREMIUM_MODULE_VERSION",
"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",
"registry.example.com",
"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
if ([string]::IsNullOrWhiteSpace($PremiumModuleRegistryBase)) {
throw "请通过 -PremiumModuleRegistryBase 指定授权模块镜像站路径,例如 registry.example.com/flnxi/premium"
}
$postgresUser = "license"
$postgresPassword = New-RandomBase64Url
$postgresDb = "license_db"
$jwtSecret = New-RandomHex
$mainDbEncryptionKey = New-RandomHex
$aesSecretKey = New-RandomHex
$licenseRequestSigningSecret = New-RandomHex
$modulePullTokenSecret = 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
LICENSE_REQUEST_SIGNING_SECRET=$licenseRequestSigningSecret
MODULE_PULL_TOKEN_SECRET=$modulePullTokenSecret
PREMIUM_MODULE_REGISTRY_BASE=$PremiumModuleRegistryBase
PREMIUM_MODULE_VERSION=$PremiumModuleVersion
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 npm run init-admin -- admin'" -ForegroundColor Gray
Write-Host "4. 初始化项目: ssh -i $SshKey $User@$Server 'cd $RemotePath && docker compose exec license-server npm run init-project -- flnxi-sucai'" -ForegroundColor Gray