适用于 PowerShell 7.0 及以上版本
当我们谈论 PowerShell 与 REST API 时,通常是在讨论如何用 Invoke-RestMethod 调用外部服务。但 PowerShell 的能力不止于此——借助 .NET 的 HttpListener 类或社区开发的 Web 框架,你完全可以用 PowerShell 构建自己的 REST API 服务。
这种能力在内部工具场景中尤为实用。比如 CI/CD 流水线需要一个状态查询端点、监控系统需要一个数据聚合接口、第三方 Webhook 需要一个接收器——这些都不需要引入一整套 ASP.NET 或 Node.js 项目,几十行 PowerShell 脚本就能搞定。本文将从底层到高层,逐步演示三种方案。
使用 HttpListener 创建基础 REST API
System.Net.HttpListener 是 .NET 内置的 HTTP 服务器类,无需安装任何额外模块即可使用。下面这段代码创建了一个支持 GET 和 POST 路由的简易 API,返回 JSON 格式的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| using namespace System.Net using namespace System.Text
$script:dataStore = @{ tasks = [System.Collections.ArrayList]::new() 1..5 | ForEach-Object { @{ id = $_; title = "任务 $_"; status = if ($_ -le 3) { 'done' } else { 'pending' } } } }
$routes = @{ 'GET /api/tasks' = { param($ctx) $json = $script:dataStore.tasks | ConvertTo-Json -Depth 3 Send-JsonResponse $ctx $json 200 } 'GET /api/tasks/id' = { param($ctx) $id = [int]($ctx.Request.Url.Segments | Select-Object -Last 1) $task = $script:dataStore.tasks | Where-Object { $_.id -eq $id } if ($task) { Send-JsonResponse $ctx ($task | ConvertTo-Json) 200 } else { Send-JsonResponse $ctx '{"error":"任务不存在"}' 404 } } 'POST /api/tasks' = { param($ctx) $reader = [StreamReader]::new($ctx.Request.InputStream) $body = $reader.ReadToEnd() $reader.Close() $newTask = $body | ConvertFrom-Json $script:dataStore.tasks.Add(@{ id = ($script:dataStore.tasks.Count + 1) title = $newTask.title status = 'pending' }) | Out-Null Send-JsonResponse $ctx '{"message":"任务已创建"}' 201 } }
function Send-JsonResponse { param([HttpListenerContext]$Context, [string]$Body, [int]$StatusCode) $buffer = [Encoding]::UTF8.GetBytes($Body) $Context.Response.StatusCode = $StatusCode $Context.Response.ContentType = 'application/json; charset=utf-8' $Context.Response.ContentLength64 = $buffer.Length $Context.Response.OutputStream.Write($buffer, 0, $buffer.Length) $Context.Response.Close() }
$listener = [HttpListener]::new() $listener.Prefixes.Add('http://localhost:8080/') $listener.Start() Write-Host "API 服务已启动: http://localhost:8080/" -ForegroundColor Green
try { while ($listener.IsListening) { $ctx = $listener.GetContext() $method = $ctx.Request.HttpMethod $path = $ctx.Request.Url.AbsolutePath.TrimEnd('/') Write-Host " [$method] $path" -ForegroundColor Cyan
$routeKey = "$method $path" $handler = $routes[$routeKey] if (-not $handler -and $path -match '/api/tasks/\d+$') { $handler = $routes['GET /api/tasks/id'] }
if ($handler) { & $handler $ctx } else { Send-JsonResponse $ctx '{"error":"路由不存在"}' 404 } } } finally { $listener.Stop() Write-Host "API 服务已停止" -ForegroundColor Yellow }
|
在另一个终端中测试 API:
1 2 3 4 5 6 7 8
| Invoke-RestMethod http://localhost:8080/api/tasks
Invoke-RestMethod http://localhost:8080/api/tasks/2
Invoke-RestMethod http://localhost:8080/api/tasks -Method Post -Body '{"title":"新任务"}' -ContentType 'application/json'
|
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| id title status -- ----- ------ 1 任务 1 done 2 任务 2 done 3 任务 3 done 4 任务 4 pending 5 任务 5 pending
id title status -- ----- ------ 2 任务 2 done
message ------- 任务已创建
|
使用 Pode 框架构建完整 API
HttpListener 虽然灵活,但需要手动处理路由、中间件等逻辑。Pode 是一个专为 PowerShell 设计的跨平台 Web 框架,提供了路由、中间件、认证、静态文件、日志等开箱即用的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| Install-Module Pode -Force -Scope CurrentUser
Start-PodeServer { Add-PodeEndpoint -Address localhost -Port 3000 -Protocol Http
New-PodeLoggingMethod -File -Name 'api-errors' | Enable-PodeErrorLogging New-PodeLoggingMethod -File -Name 'api-requests' | New-PodeRequestLogging
New-PodeAuthScheme -Bearer | Add-PodeAuth -Name 'jwt-auth' -Sessionless { param($token) try { $payload = $token | Invoke-RestMethod -Uri 'https://your-idp/.well-known/jwks.json' if ([string]::IsNullOrEmpty($token)) { return @{ Message = 'Token 无效' } } return @{ User = @{ Name = 'api-user'; Role = 'admin' } } } catch { return @{ Message = "认证失败: $_" } } }
Add-PodeMiddleware -Name 'timer' -ScriptBlock { $WebEvent.Metadata['StartTime'] = [datetime]::UtcNow }
Add-PodeMiddleware -Name 'cors' -ScriptBlock { $WebEvent.Response.Headers.Add('Access-Control-Allow-Origin', '*') $WebEvent.Response.Headers.Add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE') if ($WebEvent.Method -eq 'OPTIONS') { Set-PodeResponseStatus -Code 204 } }
Add-PodeRoute -Method Get -Path '/health' -ScriptBlock { Write-PodeJsonResponse -Value @{ status = 'healthy' uptime = (Get-Date) - $PodeContext.Server.StartTime version = '1.0.0' hostname = $env:COMPUTERNAME } }
Add-PodeRoute -Method Get -Path '/api/items' -Authentication 'jwt-auth' -ScriptBlock { $page = [int]($WebEvent.Query['page'] ?? '1') $size = [int]($WebEvent.Query['size'] ?? '20') Write-PodeJsonResponse -Value @{ data = @( @{ id = 1; name = '项目 A'; status = 'active' } @{ id = 2; name = '项目 B'; status = 'archived' } @{ id = 3; name = '项目 C'; status = 'active' } ) page = $page size = $size total = 3 } }
Add-PodeRoute -Method Post -Path '/api/items' -Authentication 'jwt-auth' -ScriptBlock { $body = $WebEvent.Data if (-not $body.name) { Set-PodeResponseStatus -Code 400 Write-PodeJsonResponse -Value @{ error = 'name 字段必填' } return } Write-PodeJsonResponse -Value @{ id = Get-Random -Maximum 1000 name = $body.name status = 'created' message = '项目创建成功' } -StatusCode 201 }
Add-PodeStaticRoute -Path '/docs' -Source './public'
Write-Host "Pode API 服务运行在 http://localhost:3000" -ForegroundColor Green }
|
启动服务后测试各端点:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Invoke-RestMethod http://localhost:3000/health | Format-List
$headers = @{ Authorization = 'Bearer your-jwt-token-here' } Invoke-RestMethod http://localhost:3000/api/items -Headers $headers
Invoke-RestMethod 'http://localhost:3000/api/items?page=1&size=10' -Headers $headers
$body = @{ name = '项目 D' } | ConvertTo-Json Invoke-RestMethod http://localhost:3000/api/items -Method Post -Headers $headers -Body $body -ContentType 'application/json'
|
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| status : healthy uptime : 00:15:32.0180000 version : 1.0.0 hostname: SERVER01
data : {@{id=1; name=项目 A; status=active}, @{id=2; name=项目 B; status=archived}, @{id=3; name=项目 C; status=active}} page : 1 size : 20 total : 3
id : 847 name : 项目 D status : created message : 项目创建成功
|
API 部署与运维
将 API 从开发脚本变成生产服务,需要考虑日志记录、健康检查、自动重启等运维需求。以下脚本展示了如何将 Pode API 注册为 Windows 服务,并添加运维监控端点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| function Register-ApiService { param( [string]$Name = 'PowershellApi', [string]$ScriptPath = 'C:\api\server.ps1' )
$nssmPath = 'C:\tools\nssm.exe' if (-not (Test-Path $nssmPath)) { Write-Warning '请先安装 NSSM: choco install nssm' return }
& $nssmPath install $Name pwsh.exe "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`"" & $nssmPath set $Name DisplayName "PowerShell REST API Service" & $nssmPath set $Name Description "内部工具 REST API 服务" & $nssmPath set $Name Start SERVICE_AUTO_START & $nssmPath set $Name AppDirectory (Split-Path $ScriptPath) & $nssmPath set $Name AppStdout (Join-Path (Split-Path $ScriptPath) 'stdout.log') & $nssmPath set $Name AppStderr (Join-Path (Split-Path $ScriptPath) 'stderr.log') & $nssmPath set $Name AppRotateFiles 1 & $nssmPath set $Name AppRotateBytes 10485760
Write-Host "服务 [$Name] 已注册,使用以下命令管理:" -ForegroundColor Green Write-Host " 启动: nssm start $Name" Write-Host " 停止: nssm stop $Name" Write-Host " 状态: nssm status $Name" Write-Host " 删除: nssm remove $Name confirm" }
function Test-ApiHealth { param( [string]$BaseUrl = 'http://localhost:3000', [int]$IntervalSeconds = 30 )
$logFile = "C:\api\health-$(Get-Date -Format 'yyyyMMdd').log"
while ($true) { $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' try { $sw = [System.Diagnostics.Stopwatch]::StartNew() $response = Invoke-RestMethod "$BaseUrl/health" -TimeoutSec 5 $sw.Stop() $latency = $sw.ElapsedMilliseconds
$logEntry = "[$timestamp] OK | latency: ${latency}ms | uptime: $($response.uptime)" Write-Host $logEntry -ForegroundColor Green Add-Content -Path $logFile -Value $logEntry
} catch { $logEntry = "[$timestamp] FAIL | error: $($_.Exception.Message)" Write-Host $logEntry -ForegroundColor Red Add-Content -Path $logFile -Value $logEntry
}
Start-Sleep -Seconds $IntervalSeconds } }
function Deploy-ApiService { param([string]$ApiDir = 'C:\api')
$dirs = @($ApiDir, "$ApiDir\logs", "$ApiDir\public") $dirs | ForEach-Object { if (-not (Test-Path $_)) { New-Item -ItemType Directory -Path $_ -Force | Out-Null } }
Copy-Item '.\api-server.ps1' "$ApiDir\server.ps1" -Force Copy-Item '.\public\*' "$ApiDir\public\" -Recurse -Force
Register-ApiService -ScriptPath "$ApiDir\server.ps1"
Write-Host "部署完成!API 地址: http://localhost:3000" -ForegroundColor Green Write-Host "健康检查: http://localhost:3000/health" -ForegroundColor Cyan Write-Host "API 文档: http://localhost:3000/docs" -ForegroundColor Cyan }
|
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 服务 [PowershellApi] 已注册,使用以下命令管理: 启动: nssm start PowershellApi 停止: nssm stop PowershellApi 状态: nssm status PowershellApi 删除: nssm remove PowershellApi confirm
[2026-01-14 10:00:00] OK | latency: 12ms | uptime: 00:45:12.0000000 [2026-01-14 10:00:30] OK | latency: 8ms | uptime: 00:45:42.0000000 [2026-01-14 10:01:00] OK | latency: 15ms | uptime: 00:46:12.0000000 [2026-01-14 10:01:30] FAIL | error: Unable to connect to the remote server [2026-01-14 10:02:00] OK | latency: 10ms | uptime: 00:00:30.0000000
部署完成!API 地址: http://localhost:3000 健康检查: http://localhost:3000/health API 文档: http://localhost:3000/docs
|
注意事项
HttpListener 权限:在 Windows 上监听非 localhost 地址时,需要以管理员身份运行 netsh http add urlacl url=http://+:8080/ user=Everyone 来预留 URL 命名空间,否则会抛出”拒绝访问”异常。
并发处理:HttpListener 的 GetContext() 是同步阻塞调用,只能一次处理一个请求。生产环境应使用 BeginGetContext / EndGetContext 异步模式,或者直接采用 Pode 框架——它内部已处理好并发问题。
HTTPS 支持:生产环境务必启用 TLS。Pode 支持 Add-PodeEndpoint -ProtocolHttps -Certificate 参数直接绑定证书;HttpListener 则需要通过 netsh 绑定 SSL 证书到端口。
内存泄漏防范:长时间运行的 API 服务需要注意 IDisposable 对象(如 HttpListener、StreamReader)的释放。使用 try/finally 确保资源被正确清理,或使用 using 语句(PowerShell 7.0+ 支持)。
错误处理与日志:永远不要让未捕获的异常终止整个服务进程。在路由处理器中使用 try/catch,并将错误写入结构化日志文件,方便后续通过 ELK 或 Splunk 等工具分析。
跨平台部署:如果需要在 Linux 上运行,HttpListener 方案可以工作(依赖 .NET 的 Unix Socket 实现),但更推荐使用 Pode——它原生支持 Linux,可以搭配 systemd 或 Docker 容器化部署,实现与 Windows 服务对等的运维体验。