PowerShell 技能连载 - REST API 开发

适用于 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

# 创建 API 数据存储
$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
# 安装 Pode 模块(首次使用)
Install-Module Pode -Force -Scope CurrentUser

# 创建 api-server.ps1
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

# JWT 认证中间件
New-PodeAuthScheme -Bearer | Add-PodeAuth -Name 'jwt-auth' -Sessionless {
param($token)
try {
$payload = $token | Invoke-RestMethod -Uri 'https://your-idp/.well-known/jwks.json'
# 简化演示:直接验证 token 非空
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
}

# 全局中间件:CORS 支持
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
}
}

# GET /api/items - 获取列表
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
}
}

# POST /api/items - 创建项目
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
# 将 API 注册为 Windows 服务
function Register-ApiService {
param(
[string]$Name = 'PowershellApi',
[string]$ScriptPath = 'C:\api\server.ps1'
)

# 使用 NSSM(Non-Sucking Service Manager)注册服务
$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"
}

# 运维监控脚本:定时检查 API 可用性
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

# 可选:自动重启服务
# Restart-Service -Name 'PowershellApi' -Force
}

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

注意事项

  1. HttpListener 权限:在 Windows 上监听非 localhost 地址时,需要以管理员身份运行 netsh http add urlacl url=http://+:8080/ user=Everyone 来预留 URL 命名空间,否则会抛出”拒绝访问”异常。

  2. 并发处理HttpListenerGetContext() 是同步阻塞调用,只能一次处理一个请求。生产环境应使用 BeginGetContext / EndGetContext 异步模式,或者直接采用 Pode 框架——它内部已处理好并发问题。

  3. HTTPS 支持:生产环境务必启用 TLS。Pode 支持 Add-PodeEndpoint -ProtocolHttps -Certificate 参数直接绑定证书;HttpListener 则需要通过 netsh 绑定 SSL 证书到端口。

  4. 内存泄漏防范:长时间运行的 API 服务需要注意 IDisposable 对象(如 HttpListenerStreamReader)的释放。使用 try/finally 确保资源被正确清理,或使用 using 语句(PowerShell 7.0+ 支持)。

  5. 错误处理与日志:永远不要让未捕获的异常终止整个服务进程。在路由处理器中使用 try/catch,并将错误写入结构化日志文件,方便后续通过 ELK 或 Splunk 等工具分析。

  6. 跨平台部署:如果需要在 Linux 上运行,HttpListener 方案可以工作(依赖 .NET 的 Unix Socket 实现),但更推荐使用 Pode——它原生支持 Linux,可以搭配 systemd 或 Docker 容器化部署,实现与 Windows 服务对等的运维体验。

PowerShell 技能连载 - REST API 设计与实现

适用于 PowerShell 7.0 及以上版本

PowerShell 不仅能调用 API,还能创建 API。PolarisPode 等模块让 PowerShell 可以快速搭建 Web 服务器,处理 HTTP 请求。虽然不建议用 PowerShell 替代专业的 Web 框架,但在内网工具、运维自动化接口、轻量级中间服务等场景中,PowerShell API 服务器非常实用——快速实现一个健康检查接口、暴露系统信息给监控平台、提供配置查询 API。

本文将讲解如何使用 PowerShell 构建轻量级 REST API 服务。

使用 Pode 搭建 API

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
# 安装 Pode 模块
Install-Module -Name Pode -Force -Scope CurrentUser

# 创建 API 服务器脚本
$startApi = {
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http

# 日志中间件
Add-PodeMiddleware -Name 'Logger' -ScriptBlock {
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$method = $WebEvent.Method
$path = $WebEvent.Path
Write-PodeHost "[$timestamp] $method $path"
}

# GET /api/health —— 健康检查
Add-PodeRoute -Method Get -Path '/api/health' -ScriptBlock {
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$uptime = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)

Write-PodeJsonResponse -Value @{
status = 'healthy'
timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
computer = $env:COMPUTERNAME
uptime = "$uptime 天"
cpuUsage = "$($cpu.LoadPercentage)%"
memoryUsage = "$([math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize * 100, 1))%"
}
}

# GET /api/processes —— 进程列表
Add-PodeRoute -Method Get -Path '/api/processes' -ScriptBlock {
$top = if ($WebEvent.Query['top']) { [int]$WebEvent.Query['top'] } else { 10 }
$sortBy = if ($WebEvent.Query['sort']) { $WebEvent.Query['sort'] } else { 'Memory' }

$processes = Get-Process | Sort-Object WorkingSet64 -Descending |
Select-Object -First $top |
ForEach-Object {
@{
name = $_.Name
pid = $_.Id
memoryMB = [math]::Round($_.WorkingSet64 / 1MB, 1)
cpu = [math]::Round($_.CPU, 2)
}
}

Write-PodeJsonResponse -Value @{
count = $processes.Count
data = $processes
}
}

# GET /api/disks —— 磁盘信息
Add-PodeRoute -Method Get -Path '/api/disks' -ScriptBlock {
$disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
$totalGB = [math]::Round($_.Size / 1GB, 2)
$freeGB = [math]::Round($_.FreeSpace / 1GB, 2)
@{
drive = $_.DeviceID
totalGB = $totalGB
freeGB = $freeGB
usagePct = [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
}
}

Write-PodeJsonResponse -Value @{ data = $disks }
}
}
}

Write-Host "API 服务器启动中..." -ForegroundColor Cyan
Write-Host "端点:" -ForegroundColor Yellow
Write-Host " GET http://localhost:8080/api/health" -ForegroundColor Green
Write-Host " GET http://localhost:8080/api/processes?top=5" -ForegroundColor Green
Write-Host " GET http://localhost:8080/api/disks" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
API 服务器启动中...
端点:
GET http://localhost:8080/api/health
GET http://localhost:8080/api/processes?top=5
GET http://localhost:8080/api/disks
[2025-07-23 08:30:15] GET /api/health

POST 接口与请求处理

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
# 在同一 Pode 服务器中添加 POST 接口
$apiWithPost = {
Start-PodeServer {
Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http

# POST /api/execute —— 执行命令(需要认证)
Add-PodeRoute -Method Post -Path '/api/execute' -ScriptBlock {
$body = $WebEvent.Data

if (-not $body.command) {
Set-PodeResponseStatus -Code 400
Write-PodeJsonResponse -Value @{ error = '缺少 command 参数' }
return
}

# 安全限制:只允许特定命令
$allowed = @('Get-Process', 'Get-Service', 'Get-EventLog', 'Get-CimInstance')
$cmdName = ($body.command -split '\s+')[0]
if ($cmdName -notin $allowed) {
Set-PodeResponseStatus -Code 403
Write-PodeJsonResponse -Value @{ error = "命令不允许:$cmdName" }
return
}

try {
$result = Invoke-Expression $body.command -ErrorAction Stop
$jsonResult = $result | ConvertTo-Json -Depth 3 -Compress

Write-PodeJsonResponse -Value @{
success = $true
command = $body.command
result = $result
}
} catch {
Set-PodeResponseStatus -Code 500
Write-PodeJsonResponse -Value @{
success = $false
error = $_.Exception.Message
}
}
}

# POST /api/alert —— 接收告警
Add-PodeRoute -Method Post -Path '/api/alert' -ScriptBlock {
$body = $WebEvent.Data

$alertInfo = @{
timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss')
source = $body.source
level = $body.level
message = $body.message
hostname = $body.hostname
receivedAt = Get-Date -Format 'HH:mm:ss'
}

# 记录到文件
$alertInfo | ConvertTo-Json | Add-Content "C:\Logs\alerts.json" -Encoding UTF8

Write-PodeJsonResponse -Value @{
received = $true
id = [guid]::NewGuid().ToString()
}
}

# GET /api/alerts —— 查询告警
Add-PodeRoute -Method Get -Path '/api/alerts' -ScriptBlock {
$count = if ($WebEvent.Query['count']) { [int]$WebEvent.Query['count'] } else { 10 }
$level = $WebEvent.Query['level']

if (Test-Path "C:\Logs\alerts.json") {
$alerts = Get-Content "C:\Logs\alerts.json" -Tail $count |
ConvertFrom-Json |
Where-Object { if ($level) { $_.level -eq $level } else { $true } }

Write-PodeJsonResponse -Value @{
count = $alerts.Count
data = $alerts
}
} else {
Write-PodeJsonResponse -Value @{ count = 0; data = @() }
}
}
}
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
# 调用 POST 接口
$body = @{ command = "Get-Process | Select-Object -First 3 Name,Id" } | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8081/api/execute" -Method Post -Body $body -ContentType "application/json"

# 发送告警
$alert = @{
source = "monitor"
level = "ERROR"
message = "磁盘空间不足"
hostname = "SRV01"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8081/api/alert" -Method Post -Body $alert -ContentType "application/json"

API 客户端封装

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
# 封装为可复用的 API 客户端函数
function Get-ServerHealth {
param([string]$ServerUrl = "http://localhost:8080")

try {
$response = Invoke-RestMethod -Uri "$ServerUrl/api/health" -TimeoutSec 5
return $response
} catch {
Write-Host "健康检查失败:$($_.Exception.Message)" -ForegroundColor Red
return $null
}
}

function Get-ServerProcesses {
param(
[string]$ServerUrl = "http://localhost:8080",
[int]$Top = 10
)

return Invoke-RestMethod -Uri "$ServerUrl/api/processes?top=$Top" -TimeoutSec 10
}

# 批量检查多台服务器
$servers = @(
"http://web-01:8080",
"http://web-02:8080",
"http://db-01:8080"
)

Write-Host "集群健康检查:" -ForegroundColor Cyan
foreach ($server in $servers) {
$health = Get-ServerHealth -ServerUrl $server
if ($health) {
$status = $health.status
$color = if ($status -eq "healthy") { "Green" } else { "Red" }
Write-Host " $server : $status (CPU: $($health.cpuUsage), MEM: $($health.memoryUsage))" -ForegroundColor $color
} else {
Write-Host " $server : 不可达" -ForegroundColor Red
}
}

执行结果示例:

1
2
3
4
集群健康检查:
http://web-01:8080 : healthy (CPU: 35%, MEM: 72.4%)
http://web-02:8080 : healthy (CPU: 28%, MEM: 65.1%)
http://db-01:8080 : healthy (CPU: 42%, MEM: 85.3%)

注意事项

  1. 安全风险:暴露命令执行接口极度危险,生产环境必须加认证、限白名单、审计日志
  2. 性能限制:PowerShell HTTP 服务器性能有限,不适合高并发场景(建议使用反向代理)
  3. 认证:生产 API 必须添加认证(API Key、JWT、Basic Auth),Pode 内置认证中间件
  4. HTTPS:生产环境使用 HTTPS,Pode 支持自签名证书和 Let’s Encrypt
  5. 服务化:长期运行的 API 应注册为 Windows 服务,使用 nssmRegister-PodeService
  6. 错误处理:API 接口必须有完善的错误处理和统一的错误响应格式

PowerShell 技能连载 - 网络编程与 REST API

适用于 PowerShell 5.1 及以上版本

在现代运维和自动化场景中,与 REST API 交互已成为 PowerShell 的核心能力之一——无论是调用云服务 API 管理 Azure/AWS 资源、与 GitHub/GitLab 交互管理代码仓库、对接企业内部的 ITSM 系统创建工单,还是查询第三方服务获取天气、汇率等数据,都离不开 HTTP 请求。PowerShell 内置的 Invoke-RestMethodInvoke-WebRequest 提供了强大且易用的 HTTP 客户端功能。

本文将从基础 HTTP 请求讲起,逐步深入到认证、JSON 处理、分页请求和错误重试等进阶话题。

基础 HTTP 请求

PowerShell 提供两个主要的 HTTP 命令:Invoke-RestMethod(自动解析响应)和 Invoke-WebRequest(返回原始响应对象):

1
2
3
4
5
6
7
8
9
10
11
12
13
# GET 请求(Invoke-RestMethod 自动解析 JSON)
$response = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/posts/1'
$response | Format-List

# GET 请求(Invoke-WebRequest 返回完整响应对象)
$webResponse = Invoke-WebRequest -Uri 'https://jsonplaceholder.typicode.com/posts/1'
Write-Host "状态码:$($webResponse.StatusCode)"
Write-Host "内容类型:$($webResponse.Headers.'Content-Type')"
Write-Host "内容长度:$($webResponse.Headers.'Content-Length') 字节"

# 将响应内容转换为 JSON 对象
$content = $webResponse.Content | ConvertFrom-Json
$content | Select-Object id, title

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
userId : 1
id : 1
title : sunt aut facere repellat provident occaecati excepturi optio
body : quia et suscipit...

状态码:200
内容类型:application/json; charset=utf-8
内容长度:292 字节

id title
-- -----
1 sunt aut facere repellat provident occaecati excepturi optio

POST/PUT/DELETE 请求

REST API 的核心是 CRUD 操作——创建(POST)、读取(GET)、更新(PUT)、删除(DELETE):

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
# POST 请求(创建资源)
$newPost = @{
title = 'PowerShell REST API 指南'
body = '这是一篇关于 PowerShell 调用 REST API 的文章'
userId = 1
} | ConvertTo-Json

$created = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/posts' `
-Method Post `
-Body $newPost `
-ContentType 'application/json; charset=utf-8'

Write-Host "已创建文章,ID: $($created.id)"
$created | Format-List

# PUT 请求(更新资源)
$updateData = @{
id = 1
title = '更新后的标题'
body = '更新后的内容'
userId = 1
} | ConvertTo-Json

$updated = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/posts/1' `
-Method Put `
-Body $updateData `
-ContentType 'application/json; charset=utf-8'

# DELETE 请求
$deleteResponse = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/posts/1' `
-Method Delete
Write-Host "删除状态码:$($deleteResponse.StatusCode)"

执行结果示例:

1
2
3
4
5
6
已创建文章,ID: 101

title : PowerShell REST API 指南
body : 这是一篇关于 PowerShell 调用 REST API 的文章
userId : 1
id : 101

认证方式

大多数生产 API 需要认证。以下是常见的认证方式:

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
# 方式一:Bearer Token 认证
$token = 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
$headers = @{
Authorization = "Bearer $token"
Accept = 'application/vnd.github.v3+json'
}

$repos = Invoke-RestMethod -Uri 'https://api.github.com/user/repos' `
-Headers $headers
$repos | Select-Object name, private, language, stargazers_count |
Format-Table -AutoSize

# 方式二:API Key 作为查询参数
$apiKey = 'your-api-key'
$weather = Invoke-RestMethod -Uri "https://api.openweathermap.org/data/2.5/weather?q=Beijing&appid=$apiKey&units=metric"
Write-Host "北京当前温度:$($weather.main.temp)°C"
Write-Host "天气描述:$($weather.weather[0].description)"

# 方式三:基本认证
$cred = Get-Credential
Invoke-RestMethod -Uri 'https://api.example.com/data' `
-Authentication Basic `
-Credential $cred

# 方式四:OAuth2 客户端凭据流
$tokenResponse = Invoke-RestMethod -Uri 'https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token' `
-Method Post `
-Body @{
client_id = 'app-client-id'
client_secret = 'app-client-secret'
scope = 'https://graph.microsoft.com/.default'
grant_type = 'client_credentials'
}

$accessToken = $tokenResponse.access_token
Write-Host "获取到 Token,有效期:$($tokenResponse.expires_in) 秒"

执行结果示例:

1
2
3
4
5
6
7
8
9
name             private language stargazers_count
---- -------- -------- ----------------
my-project False PowerShell 12
internal-tools True C# 5

北京当前温度:22.3°C
天气描述:晴

获取到 Token,有效期:3599

JSON 处理进阶

PowerShell 的 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
# ConvertTo-Json 的深度问题(默认只序列化 2 层)
$data = @{
Level1 = @{
Level2 = @{
Level3 = '深层嵌套数据'
}
}
}

# 默认深度 2,Level3 会丢失
$json shallow = $data | ConvertTo-Json
Write-Host "默认深度:`n$jsonshallow"

# 设置深度为 10,保留完整结构
$jsonDeep = $data | ConvertTo-Json -Depth 10
Write-Host "`n深度 10:`n$jsonDeep"

# 处理数组中的空值
$arrayData = @(
@{ Name = 'Item1'; Value = 100 }
@{ Name = 'Item2'; Value = $null }
@{ Name = 'Item3'; Value = 300 }
)

# 默认会忽略空值属性,使用 -EnumsAsStrings 保持完整
$jsonArray = $arrayData | ConvertTo-Json -Depth 5
Write-Host "`n数组 JSON:`n$jsonArray"

# 从 API 响应中提取嵌套数据
$response = Invoke-RestMethod -Uri 'https://jsonplaceholder.typicode.com/users'
$response | Select-Object name, email,
@{N='城市'; E={$_.address.city}},
@{N='公司'; E={$_.company.name}} |
Format-Table -AutoSize

执行结果示例:

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
默认深度:
{
"Level1": {
"Level2": "System.Collections.Hashtable"
}
}

深度 10:
{
"Level1": {
"Level2": {
"Level3": "深层嵌套数据"
}
}
}

数组 JSON:
[
{ "Name": "Item1", "Value": 100 },
{ "Name": "Item2" },
{ "Name": "Item3", "Value": 300 }
]

name email 城市 公司
---- ----- ---- ----
Leanne Graham Sincere@april.biz Gwenborough Romaguera-Crona
Ervin Howell Shanna@melissa.tv Wisokyburgh Deckow-Crist

分页请求处理

当 API 返回大量数据时,通常使用分页。以下是一个通用的分页请求函数:

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
function Invoke-RestMethodPaged {
<#
.SYNOPSIS
自动处理分页 API 请求
#>
param(
[Parameter(Mandatory)]
[string]$Uri,

[int]$PageSize = 100,
[int]$MaxPages = 100,
[hashtable]$Headers,
[string]$PageParam = 'page',
[string]$PerPageParam = 'per_page'
)

$allResults = [System.Collections.Generic.List[PSObject]]::new()
$page = 1

while ($page -le $MaxPages) {
$separator = if ($Uri -match '\?') { '&' } else { '?' }
$pageUri = "${Uri}${separator}${PerPageParam}=${PageSize}&${PageParam}=${page}"

$params = @{
Uri = $pageUri
Method = 'Get'
}
if ($Headers) { $params.Headers = $Headers }

try {
$response = Invoke-RestMethod @params
} catch {
Write-Warning "第 ${page} 页请求失败:$($_.Exception.Message)"
break
}

if (-not $response -or $response.Count -eq 0) {
break
}

$allResults.AddRange($response)
Write-Host "已获取第 ${page} 页,累计 $($allResults.Count) 条" -ForegroundColor Cyan

if ($response.Count -lt $PageSize) {
break
}

$page++
}

Write-Host "`n总计获取 $($allResults.Count) 条数据" -ForegroundColor Green
return $allResults
}

# 示例:获取 GitHub 仓库的所有 Issues
$issues = Invoke-RestMethodPaged `
-Uri 'https://api.github.com/repos/PowerShell/PowerShell/issues' `
-Headers @{ Authorization = "Bearer $token" } `
-PageSize 50

$issues | Select-Object number, title, state,
@{N='labels'; E={$_.labels.name -join ', '}} |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
已获取第 1 页,累计 50
已获取第 2 页,累计 100
已获取第 3 页,累计 112

总计获取 112 条数据

number title state labels
------ ----- ----- -------
24501 Fix null coalescing in pipelines open Bug, Help Wanted
24498 Add -AsHashtable to ConvertFrom... open Enhancement
24495 Update ReadMe with new links closed Documentation

错误处理与重试

网络请求不可避免会遇到超时、限流等问题。良好的错误处理和重试机制是健壮 API 调用的基础:

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
function Invoke-ApiWithRetry {
<#
.SYNOPSIS
带重试机制的 API 调用
#>
param(
[Parameter(Mandatory)]
[string]$Uri,

[ValidateSet('Get','Post','Put','Delete','Patch')]
[string]$Method = 'Get',

$Body,
[hashtable]$Headers,
[int]$MaxRetries = 3,
[int]$RetryDelaySeconds = 2
)

$attempt = 0

while ($attempt -lt $MaxRetries) {
$attempt++
try {
$params = @{
Uri = $Uri
Method = $Method
}
if ($Body) { $params.Body = $Body }
if ($Headers) { $params.Headers = $Headers }

$response = Invoke-RestMethod @params -ErrorAction Stop
return $response

} catch {
$statusCode = $null
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
}

if ($statusCode -eq 429) {
# 限流:使用 Retry-After 头的值
$retryAfter = $_.Exception.Response.Headers['Retry-After']
$waitTime = if ($retryAfter) { [int]$retryAfter } else { $RetryDelaySeconds * 2 }
Write-Warning "API 限流 (429),等待 ${waitTime} 秒后重试(第 ${attempt} 次)"
Start-Sleep -Seconds $waitTime
}
elseif ($statusCode -ge 500) {
# 服务器错误:指数退避重试
$waitTime = $RetryDelaySeconds * [math]::Pow(2, $attempt - 1)
Write-Warning "服务器错误 ($statusCode),${waitTime} 秒后重试(第 ${attempt} 次)"
Start-Sleep -Seconds $waitTime
}
elseif ($statusCode -ge 400) {
# 客户端错误:不重试
Write-Error "请求失败 ($statusCode):$($_.Exception.Message)"
return $null
}
else {
# 其他错误(网络超时等):重试
Write-Warning "请求异常:$($_.Exception.Message)(第 ${attempt} 次)"
Start-Sleep -Seconds $RetryDelaySeconds
}
}
}

Write-Error "已达最大重试次数 ($MaxRetries),请求失败"
return $null
}

# 使用带重试的 API 调用
$result = Invoke-ApiWithRetry -Uri 'https://api.github.com/rate_limit' `
-Headers @{ Authorization = "Bearer $token" } `
-MaxRetries 3

执行结果示例:

1
2
WARNING: API 限流 (429),等待 60 秒后重试(第 1 次)
WARNING: 服务器错误 (503),2 秒后重试(第 2 次)

注意事项

  1. TLS 版本:某些 API 要求 TLS 1.2,使用 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 强制设置
  2. JSON 序列化深度ConvertTo-Json 默认深度为 2,嵌套对象会被截断,务必根据实际结构调整 -Depth 参数
  3. 编码问题:发送中文内容时,确保 -ContentType 包含 charset=utf-8,避免乱码
  4. 大型响应:处理大量数据时,考虑使用流式读取或将结果分页处理,避免一次性加载到内存
  5. 敏感信息:不要在脚本中硬编码 API Key 和 Token,使用环境变量或 Azure Key Vault 等安全存储
  6. 超时设置:生产环境建议设置合理的超时 -TimeoutSec,避免请求无限等待