PowerShell 技能连载 - Microsoft Graph API 高级操作

适用于 PowerShell 7.0 及以上版本,需要 Microsoft.Graph 模块

Microsoft Graph 已经成为访问 Microsoft 365 和 Entra ID 数据的统一 API 网关。无论是用户管理、邮件处理、团队协作还是安全合规,几乎所有 Microsoft 云服务的数据和操作都可以通过 Graph API 完成。对于 PowerShell 运维人员来说,掌握 Graph API 的高级用法意味着能够构建更高效、更可靠的自动化流程。

然而,在实际生产环境中,简单的 API 调用往往不够。当需要处理成千上万个用户对象、执行批量许可分配、或者生成复杂的安全审计报告时,必须考虑认证效率、请求优化、分页处理和错误重试等工程化问题。本文将围绕这些高级场景,分享一套可直接用于生产环境的 Graph API 操作模式。

Graph API 认证与查询优化

在规模化操作中,认证方式和查询策略直接影响执行效率。以下代码展示了应用权限认证、分页自动处理、$select/$filter 查询优化以及批量请求(Batch)的完整实现。

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
90
91
92
93
94
95
96
function Connect-GraphApp {
param(
[Parameter(Mandatory)]
[string]$TenantId,
[Parameter(Mandatory)]
[string]$ClientId,
[Parameter(Mandatory)]
[string]$CertificateThumbprint
)

Connect-MgGraph -TenantId $TenantId `
-ClientId $ClientId `
-CertificateThumbprint $CertificateThumbprint `
-NoWelcome

$context = Get-MgContext
Write-Host "已连接: $($context.AppName) @ $($context.TenantId)" -ForegroundColor Green
}

function Invoke-GraphPagedQuery {
param(
[Parameter(Mandatory)]
[string]$Uri,
[int]$PageSize = 100,
[string[]]$Select,
[string]$Filter
)

$params = @()
if ($PageSize) { $params += "`$top=$PageSize" }
if ($Select) { $params += "`$select=$($Select -join ',')" }
if ($Filter) { $params += "`$filter=$Filter" }

$queryString = if ($params) { '?' + ($params -join '&') } else { '' }
$fullUri = if ($Uri -match '\?') { "$Uri&$($params -join '&')" } else { "$Uri$queryString" }

$allResults = [System.Collections.Generic.List[object]]::new()
$currentUri = $fullUri
$requestCount = 0

while ($currentUri) {
$requestCount++
$response = Invoke-MgGraphRequest -Uri $currentUri -Method GET -OutputType Hashtable

if ($response.value) {
$allResults.AddRange($response.value)
}

$currentUri = $response.'@odata.nextLink'
Write-Verbose "已获取 $($allResults.Count) 条记录 (请求 #$requestCount)"
}

Write-Host "查询完成: 共 $($allResults.Count) 条, 发送 $requestCount 次请求" -ForegroundColor Cyan
return $allResults
}

function Invoke-GraphBatch {
param(
[Parameter(Mandatory)]
[hashtable[]]$Requests,
[int]$BatchSize = 20
)

$batches = for ($i = 0; $i -lt $Requests.Count; $i += $BatchSize) {
,($Requests[$i..([Math]::Min($i + $BatchSize - 1, $Requests.Count - 1))])
}

$allResponses = @()
$batchNum = 0

foreach ($batch in $batches) {
$batchNum++
$batchBody = @{
requests = @($batch | ForEach-Object -Begin { $id = 0 } -Process {
$id++
@{
id = "$id"
method = $_.Method ?? 'GET'
url = $_.Url
body = $_.Body
headers = @{ 'Content-Type' = 'application/json' }
}
})
}

$response = Invoke-MgGraphRequest `
-Uri 'https://graph.microsoft.com/v1.0/$batch' `
-Method POST `
-Body ($batchBody | ConvertTo-Json -Depth 5)

$allResponses += $response.responses
Write-Verbose "批次 $batchNum/$($batches.Count) 完成"
}

return $allResponses
}

执行结果示例:

1
2
3
4
5
6
7
已连接: MyApp-Graph@ 72f988bf-86f1-41af-91ab-2d7cd011db47
查询完成: 共 2847 条, 发送 29 次请求
批次 1/5 完成
批次 2/5 完成
批次 3/5 完成
批次 4/5 完成
批次 5/5 完成

通过 $select 只返回需要的字段可以显著减少响应体积,$filter 在服务端完成过滤避免传输冗余数据。批量请求(Batch)最多可在一个 HTTP 调用中打包 20 个操作,大幅降低网络开销。

用户与组批量管理

在大型组织中,用户入职、离职和部门调动都涉及批量操作。以下代码实现了批量用户创建与更新、组生命周期管理以及许可(License)分配的完整流程。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
function New-BulkUsers {
param(
[Parameter(Mandatory)]
[array]$UserList,
[string]$Domain = 'contoso.com'
)

$results = @()

foreach ($user in $UserList) {
$mailNickname = "$($user.GivenName).$($user.Surname)".ToLower() -replace '\s', ''
$upn = "$mailNickname@$Domain"
$tempPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | ForEach-Object { [char]$_ })

$passwordProfile = @{
password = $tempPassword
forceChangePasswordNextSignIn = $true
}

try {
$newUser = New-MgUser -BodyParameter @{
accountEnabled = $true
displayName = "$($user.GivenName) $($user.Surname)"
givenName = $user.GivenName
surname = $user.Surname
mailNickname = $mailNickname
userPrincipalName = $upn
passwordProfile = $passwordProfile
department = $user.Department
jobTitle = $user.JobTitle
usageLocation = $user.UsageLocation ?? 'CN'
} -ErrorAction Stop

$results += [PSCustomObject]@{
Status = 'Created'
UPN = $upn
UserId = $newUser.Id
TempPass = $tempPassword
}
Write-Verbose "已创建用户: $upn"
}
catch {
$results += [PSCustomObject]@{
Status = 'Failed'
UPN = $upn
UserId = $null
TempPass = $null
Error = $_.Exception.Message
}
Write-Warning "创建用户失败 $upn : $($_.Exception.Message)"
}
}

return $results
}

function Set-GroupLifecycle {
param(
[Parameter(Mandatory)]
[string]$GroupId,
[ValidateSet('Archive', 'Reactivate', 'RotateOwner')]
[string]$Action,
[string]$NewOwnerId
)

$group = Get-MgGroup -GroupId $GroupId
if (-not $group) { throw "组 $GroupId 不存在" }

switch ($Action) {
'Archive' {
Update-MgGroup -GroupId $GroupId -BodyParameter @{
description = "[已归档] $($group.Description)"
mailEnabled = $false
visibility = 'HiddenMembership'
}
Write-Host "已归档组: $($group.DisplayName)" -ForegroundColor Yellow
}
'Reactivate' {
Update-MgGroup -GroupId $GroupId -BodyParameter @{
mailEnabled = $true
visibility = 'Public'
}
Write-Host "已重新激活组: $($group.DisplayName)" -ForegroundColor Green
}
'RotateOwner' {
if (-not $NewOwnerId) { throw "更换所有者需要提供 NewOwnerId" }
$currentOwners = Get-MgGroupOwner -GroupId $GroupId
foreach ($owner in $currentOwners) {
Remove-MgGroupOwnerByRef -GroupId $GroupId -DirectoryObjectId $owner.Id
}
New-MgGroupOwnerByRef -GroupId $GroupId -BodyParameter @{
'@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$NewOwnerId"
}
Write-Host "已更换组 $($group.DisplayName) 的所有者为 $NewOwnerId" -ForegroundColor Green
}
}
}

function Set-BulkLicenseAssignment {
param(
[Parameter(Mandatory)]
[string[]]$UserIds,
[Parameter(Mandatory)]
[string[]]$SkuIds,
[bool]$Remove = $false
)

$total = $UserIds.Count
$processed = 0
$successCount = 0
$failCount = 0

foreach ($userId in $UserIds) {
$processed++
$addLicenses = @()
$removeLicenses = @()

if ($Remove) {
$removeLicenses = @($SkuIds | ForEach-Object { @{ skuId = $_ } })
}
else {
$addLicenses = @($SkuIds | ForEach-Object {
@{ skuId = $_; disabledPlans = @() }
})
}

try {
Set-MgUserLicense -UserId $userId `
-AddLicenses $addLicenses `
-RemoveLicenses $removeLicenses `
-ErrorAction Stop
$successCount++
}
catch {
$failCount++
Write-Verbose "许可操作失败 ($userId): $($_.Exception.Message)"
}

if ($processed % 50 -eq 0) {
Write-Progress -Activity "许可分配" -Status "$processed / $total" `
-PercentComplete (($processed / $total) * 100)
}
}

return [PSCustomObject]@{
Total = $total
Success = $successCount
Failed = $failCount
Action = if ($Remove) { 'Remove' } else { 'Assign' }
}
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
已创建用户: zhang.wei@contoso.com
已创建用户: li.na@contoso.com
创建用户失败 wang.fang@contoso.com : Another object with the same value for property
userPrincipalName already exists.
已归档组: 2024-Q3-项目组
已更换组 安全审计组 的所有者为 3a5b7c9d-1234-5678-abcd-ef0123456789

Total : 150
Success : 147
Failed : 3
Action : Assign

批量操作中需要注意 Graph API 的节流限制(Throttling)。用户管理操作通常限制在每 30 秒若干次请求,当返回 429 状态码时应实现指数退避重试。

安全与合规报告

安全运维是 Graph API 的高价值场景之一。通过查询 Entra ID 的登录日志、风险检测和条件访问策略,可以构建自动化的安全态势报告。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
function Get-SignInAuditReport {
param(
[int]$Days = 7,
[string]$RiskFilter
)

$startDate = (Get-Date).AddDays(-$Days).ToString('yyyy-MM-ddTHH:mm:ssZ')
$filterQuery = "createdDateTime ge $startDate"

if ($RiskFilter) {
$filterQuery += " and riskLevelDuringSignIn eq '$RiskFilter'"
}

$signIns = Invoke-GraphPagedQuery `
-Uri 'https://graph.microsoft.com/v1.0/auditLogs/signIns' `
-Select @('createdDateTime', 'userDisplayName', 'userPrincipalName',
'ipAddress', 'location', 'status', 'riskLevelAggregation',
'clientAppUsed', 'conditionalAccessStatus') `
-Filter $filterQuery `
-PageSize 100

$summary = $signIns | Group-Object userPrincipalName | ForEach-Object {
$userSignIns = $_.Group
$failedCount = @($userSignIns | Where-Object {
$_.status.errorCode -and $_.status.errorCode -ne 0
}).Count

[PSCustomObject]@{
UserPrincipalName = $_.Name
TotalSignIns = $_.Count
FailedAttempts = $failedCount
UniqueIPs = @($userSignIns.ipAddress | Sort-Object -Unique).Count
RiskEvents = @($userSignIns | Where-Object {
$_.riskLevelAggregation -and $_.riskLevelAggregation -ne 'none'
}).Count
LastSignIn = @($userSignIns.createdDateTime | Sort-Object -Descending)[0]
Locations = @($userSignIns | ForEach-Object {
$_.location.city
} | Where-Object { $_ } | Sort-Object -Unique) -join ', '
}
} | Sort-Object FailedAttempts -Descending

return $summary
}

function Get-RiskDetectionReport {
param(
[int]$Days = 30
)

$startDate = (Get-Date).AddDays(-$Days).ToString('yyyy-MM-ddTHH:mm:ssZ')

$detections = Invoke-GraphPagedQuery `
-Uri 'https://graph.microsoft.com/v1.0/identityProtection/riskDetections' `
-Select @('riskEventType', 'riskLevel', 'riskState', 'detectedDateTime',
'userDisplayName', 'userPrincipalName', 'ipAddress',
'location', 'activity') `
-Filter "detectedDateTime ge $startDate" `
-PageSize 100

$report = [PSCustomObject]@{
Period = "$Days 天"
TotalDetections = $detections.Count
HighRiskCount = @($detections | Where-Object { $_.riskLevel -eq 'high' }).Count
MediumRiskCount = @($detections | Where-Object { $_.riskLevel -eq 'medium' }).Count
ByType = $detections | Group-Object riskEventType |
Sort-Object Count -Descending |
Select-Object Count, Name -First 10
AffectedUsers = @($detections.userPrincipalName | Sort-Object -Unique).Count
TopRiskUsers = $detections | Where-Object { $_.riskLevel -eq 'high' } |
Group-Object userPrincipalName |
Sort-Object Count -Descending |
Select-Object Count, Name -First 10
Remediated = @($detections | Where-Object {
$_.riskState -eq 'remediated'
}).Count
}

return $report
}

function Get-ConditionalAccessAudit {
param()

$policies = Get-MgIdentityConditionalAccessPolicy -All

$audit = foreach ($policy in $policies) {
$grantControls = $policy.GrantControls
$conditions = $policy.Conditions

$includedApps = @()
if ($conditions.Applications.IncludeApplications) {
$includedApps = $conditions.Applications.IncludeApplications
}

[PSCustomObject]@{
DisplayName = $policy.DisplayName
State = $policy.State
CreatedDateTime = $policy.CreatedDateTime
ModifiedDateTime = $policy.ModifiedDateTime
IncludedApps = if ($includedApps -contains 'All') {
'All applications'
} else {
"$($includedApps.Count) apps"
}
IncludedUsers = if ($conditions.Users.IncludeUsers -contains 'All') {
'All users'
} else {
"$($conditions.Users.IncludeUsers.Count) users"
}
GrantControl = $grantControls.BuiltInControls -join ', '
Operator = $grantControls.Operator
ClientAppTypes = $conditions.ClientAppTypes -join ', '
SignInRiskLevels = $conditions.SignInRiskLevels -join ', '
}
}

$summary = [PSCustomObject]@{
TotalPolicies = $policies.Count
EnabledPolicies = @($audit | Where-Object { $_.State -eq 'enabled' }).Count
ReportOnly = @($audit | Where-Object { $_.State -eq 'enabledForReportingButNotEnforced' }).Count
Disabled = @($audit | Where-Object { $_.State -eq 'disabled' }).Count
Policies = $audit
}

return $summary
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
查询完成: 共 15632 条, 发送 157 次请求
查询完成: 共 423 条, 发送 5 次请求

UserPrincipalName : admin@contoso.com
TotalSignIns : 89
FailedAttempts : 12
UniqueIPs : 5
RiskEvents : 3
LastSignIn : 2026-04-27T06:32:15Z
Locations : Beijing, Shanghai, Unknown

Period : 30 天
TotalDetections : 423
HighRiskCount : 18
MediumRiskCount : 67
AffectedUsers : 42
Remediated : 156

TotalPolicies : 15
EnabledPolicies : 11
ReportOnly : 2
Disabled : 2

安全报告函数可以与计划任务(Scheduled Task)或 Azure Automation Runbook 结合,实现每日自动生成安全态势摘要并通过 Teams Webhook 或邮件发送给安全团队。

注意事项

  1. 认证方式选择:生产环境应使用应用权限(Application Permission)配合证书认证,避免交互式登录。证书建议使用 Azure Key Vault 托管,定期自动轮换。

  2. API 节流处理:Graph API 对不同端点有不同的节流限制。遇到 429 响应时必须读取 Retry-After 头部并等待指定时间,切勿简单循环重试,否则会加剧限流。

  3. 分页最佳实践:对于可能返回大量结果的查询,始终使用 $top 控制每页大小,并跟随 @odata.nextLink 逐页获取。避免在内存中缓存全量数据后再处理。

  4. 批量请求限制:单个 Batch 请求最多包含 20 个子请求,且不支持嵌套 Batch。对于超大批量操作,应分批次提交并在批次之间加入适当延迟。

  5. 许可分配注意事项:分配许可前务必确认用户的 usageLocation 属性已设置,否则操作会失败。同时要注意许可计划的依赖关系,某些高级许可需要基础许可作为前提。

  6. 日志保留期限:Entra ID 登录日志默认保留 30 天(Premium P2 许可),风险检测数据保留同样有限。如需长期分析,应将数据导出到 Log Analytics 工作区或外部存储。

PowerShell 技能连载 - Entra ID 应用注册

适用于 PowerShell 5.1 及以上版本

在企业云环境中,Entra ID(原 Azure AD)的应用注册(App Registration)是构建自动化和集成方案的基础。无论是让 CI/CD 流水线访问 Azure 资源,还是让自定义应用调用 Microsoft Graph API,都需要先在 Entra ID 中注册一个应用程序,并为其配置权限和凭据。手动在 Azure 门户中点击操作不仅效率低下,而且难以保持多个环境(开发、测试、生产)之间的一致性。

通过 PowerShell 的 Microsoft.Graph 模块,我们可以将应用注册的全生命周期管理纳入代码化流程。从创建应用、配置 API 权限、添加客户端密钥,到创建服务主体并授予角色权限,每一步都可以脚本化、参数化,然后集成到基础设施即代码(IaC)流水线中。这种方式不仅提高了效率,更重要的是让安全团队能够审查和审计每一次应用注册的变更。

本文将介绍如何使用 PowerShell 完成 Entra ID 应用注册的常见操作,包括创建应用并配置基本属性、管理客户端密钥和凭据、配置 API 权限委派,以及批量审计租户内的应用注册信息,帮助你构建安全可控的身份管理自动化方案。

创建应用注册并配置基本属性

使用 Microsoft Graph PowerShell SDK 可以快速创建应用注册,并在创建时设置显示名称、回调 URL 等属性。以下示例演示如何创建一个面向 Web 应用的注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 连接到 Microsoft Graph(需要 Application.ReadWrite.All 权限)
Connect-MgGraph -Scopes "Application.ReadWrite.All"

# 定义应用注册的基本参数
$appName = "CI-CD-Pipeline-App"
$signInAudience = "AzureADMyOrg"
$redirectUris = @(
"https://localhost:3000/auth/callback",
"https://app.contoso.com/auth/callback"
)

# 创建应用注册
$newApp = New-MgApplication -DisplayName $appName `
-SignInAudience $signInAudience `
-Web @{ RedirectUris = $redirectUris }

Write-Host "应用注册创建成功"
Write-Host " AppId (Client ID): $($newApp.AppId)"
Write-Host " ObjectId: $($newApp.Id)"
Write-Host " DisplayName: $($newApp.DisplayName)"

执行结果示例:

1
2
3
4
应用注册创建成功
AppId (Client ID): a3b2c1d4-e5f6-7890-abcd-ef1234567890
ObjectId: 11111111-2222-3333-4444-555555555555
DisplayName: CI-CD-Pipeline-App

New-MgApplication 是 Microsoft Graph PowerShell SDK 中创建应用注册的核心命令。SignInAudience 参数控制应用的适用范围:AzureADMyOrg 表示仅限当前租户的单租户应用,AzureADMultipleOrgs 为多租户应用,AzureADandPersonalMicrosoftAccount 则同时支持个人微软账户。Web 参数接受一个哈希表,用于配置 Web 平台的 redirect URI 等设置。

管理客户端密钥与凭据

应用注册创建后,通常需要为其添加客户端密钥(Client Secret)或证书凭据,以便应用程序在运行时进行身份验证。以下脚本展示如何添加密钥并管理其生命周期。

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
# 获取目标应用的 ObjectId
$app = Get-MgApplication -Filter "displayName eq 'CI-CD-Pipeline-App'"

# 创建客户端密钥,有效期 6 个月
$secretStartDate = Get-Date
$secretEndDate = $secretStartDate.AddMonths(6)

$passwordCred = @{
DisplayName = "Pipeline-Secret-$(Get-Date -Format 'yyyyMMdd')"
StartDateTime = $secretStartDate
EndDateTime = $secretEndDate
}

$secret = Add-MgApplicationPassword -ApplicationId $app.Id `
-PasswordCredential $passwordCred

Write-Host "客户端密钥已添加"
Write-Host " KeyId: $($secret.KeyId)"
Write-Host " SecretKey: $($secret.SecretText)"
Write-Host " 有效期至: $secretEndDate"
Write-Host ""
Write-Host "重要:请立即将 SecretText 安全保存,此值仅在创建时显示一次。"

# 列出该应用所有凭据,检查即将过期的密钥
$allCreds = Get-MgApplication -ApplicationId $app.Id
$expiredCount = 0
$expiringSoon = @()

foreach ($cred in $allCreds.PasswordCredentials) {
$remaining = ($cred.EndDateTime - (Get-Date)).Days
if ($remaining -le 0) {
$expiredCount++
}
if ($remaining -gt 0 -and $remaining -le 30) {
$expiringSoon += $cred
}
}

Write-Host "`n凭据状态统计:"
Write-Host " 已过期: $expiredCount 个"
Write-Host " 即将过期(30天内): $($expiringSoon.Count) 个"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
客户端密钥已添加
KeyId: aaaabbbb-cccc-dddd-eeee-ffffffffffff
SecretKey: abc123XYZ789~def456.GHI890-ghi
有效期至: 05/13/2026 00:00:00

重要:请立即将 SecretText 安全保存,此值仅在创建时显示一次。

凭据状态统计:
已过期: 1 个
即将过期(30天内): 2 个

Add-MgApplicationPassword 为应用添加密码凭据,返回的 SecretText 是密钥的明文值,仅在此刻可见。务必将其存入密钥管理服务(如 Azure Key Vault),切勿写入代码或配置文件。定期扫描即将过期的密钥并提前轮换,是保障服务连续性的关键措施。建议在 CI/CD 流水线中添加自动化检查,当密钥即将过期时自动触发告警。

配置 API 权限并创建服务主体

应用注册的另一个关键步骤是声明所需的 API 权限,然后创建服务主体(Enterprise Application)以获取实际访问能力。以下脚本演示如何配置 Microsoft Graph 的委派权限并创建服务主体。

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
# 获取 Microsoft Graph 服务主体的 AppId(租户内固定值)
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"

# 定义需要申请的委派权限(Delegated Permission)
# User.Read - 读取当前登录用户的基本信息
# Mail.Read - 读取用户邮件
# Calendars.Read - 读取用户日历
$requiredPermissions = @(
@{ Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"; Type = "Scope" }
@{ Id = "810c84a8-4a9e-49e6-bf7d-1441c4f84e72"; Type = "Scope" }
@{ Id = "465a38f9-76ea-45b9-9f34-2e6e3c3c7a0b"; Type = "Scope" }
)

$app = Get-MgApplication -Filter "displayName eq 'CI-CD-Pipeline-App'"

# 构造 requiredResourceAccess 对象
$resourceAccess = foreach ($perm in $requiredPermissions) {
@{
Id = $perm.Id
Type = $perm.Type
}
}

# 更新应用的 API 权限声明
Update-MgApplication -ApplicationId $app.Id `
-RequiredResourceAccess @(
@{
ResourceAppId = $graphSp.AppId
ResourceAccess = @($resourceAccess)
}
)

Write-Host "API 权限已配置,等待管理员同意..."

# 创建服务主体(如已存在则跳过)
$existingSp = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'" -ErrorAction SilentlyContinue
if (-not $existingSp) {
$sp = New-MgServicePrincipal -AppId $app.AppId
Write-Host "服务主体已创建,ObjectId: $($sp.Id)"
} else {
Write-Host "服务主体已存在,ObjectId: $($existingSp.Id)"
}

Write-Host "`n注意:委派权限需要管理员通过 Azure 门户或以下 URL 进行同意:"
Write-Host "https://login.microsoftonline.com/$((Get-MgContext).TenantId)/adminconsent?client_id=$($app.AppId)"

执行结果示例:

1
2
3
4
5
API 权限已配置,等待管理员同意...
服务主体已创建,ObjectId: 66666666-7777-8888-9999-000000000000

注意:委派权限需要管理员通过 Azure 门户或以下 URL 进行同意:
https://login.microsoftonline.com/contoso.onmicrosoft.com/adminconsent?client_id=a3b2c1d4-e5f6-7890-abcd-ef1234567890

API 权限分为两种类型:Scope(委派权限,代表用户执行操作)和 Role(应用程序权限,应用以自身身份执行操作)。RequiredResourceAccess 仅声明了应用”需要”哪些权限,实际生效还需要管理员通过同意流程(Admin Consent)审批。权限 ID 可以通过查询目标 API 服务主体的 Oauth2PermissionScopes(委派权限)或 AppRoles(应用程序权限)属性获取。

批量审计租户内的应用注册

对于安全团队和 IT 管理员而言,定期审计租户内的应用注册状况是保障身份安全的重要环节。以下脚本批量获取所有应用注册,并生成包含凭据状态、权限范围等信息的审计报告。

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
# 获取租户内所有应用注册
$allApps = Get-MgApplication -All

$auditDate = Get-Date -Format "yyyyMMdd"
$reportPath = "$HOME/EntraID_AppAudit_$auditDate.csv"

$auditRows = foreach ($application in $allApps) {
# 检查凭据状态
$hasPassword = $application.PasswordCredentials.Count -gt 0
$hasKeyCert = $application.KeyCredentials.Count -gt 0
$expiredCreds = @($application.PasswordCredentials | Where-Object {
$_.EndDateTime -lt (Get-Date)
})
$expiringCreds = @($application.PasswordCredentials | Where-Object {
$remaining = ($_.EndDateTime - (Get-Date)).Days
$remaining -gt 0 -and $remaining -le 90
})

# 统计申请的 API 权限数量
$apiCount = 0
foreach ($resource in $application.RequiredResourceAccess) {
$apiCount += $resource.ResourceAccess.Count
}

[PSCustomObject]@{
AppId = $application.AppId
DisplayName = $application.DisplayName
CreatedDateTime = $application.CreatedDateTime
SignInAudience = $application.SignInAudience
HasClientSecret = $hasPassword
HasCertificate = $hasKeyCert
ExpiredCredCount = $expiredCreds.Count
ExpiringCredCount = $expiringCreds.Count
ApiPermissionCount = $apiCount
PublisherDomain = $application.PublisherDomain
}
}

# 导出审计报告
$auditRows | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8

# 输出概要统计
$totalApps = $auditRows.Count
$withExpiredCreds = @($auditRows | Where-Object { $_.ExpiredCredCount -gt 0 }).Count
$withExpiringCreds = @($auditRows | Where-Object { $_.ExpiringCredCount -gt 0 }).Count
$multiTenant = @($auditRows | Where-Object { $_.SignInAudience -ne "AzureADMyOrg" }).Count

Write-Host "审计报告已导出: $reportPath"
Write-Host "`n概要统计:"
Write-Host " 应用总数: $totalApps"
Write-Host " 含已过期凭据: $withExpiredCreds"
Write-Host " 含即将过期凭据: $withExpiringCreds"
Write-Host " 多租户应用: $multiTenant"

执行结果示例:

1
2
3
4
5
6
7
审计报告已导出: /home/user/EntraID_AppAudit_20251113.csv

概要统计:
应用总数: 87
含已过期凭据: 12
含即将过期凭据: 5
多租户应用: 8

这段脚本对租户内所有应用注册进行了全面审计,重点关注三个安全维度:凭据生命周期(是否存在过期或即将过期的密钥)、权限范围(申请了多少 API 权限)、应用可见性(是否为多租户应用,意味着其他组织的用户也能看到)。建议将此脚本配置为每周自动执行,配合邮件或 Teams 通知,在凭据即将过期或出现异常配置时及时预警。

注意事项

  1. 模块安装与版本:本文使用 Microsoft.Graph.Applications 模块(属于 Microsoft Graph PowerShell SDK v2),安装命令为 Install-Module Microsoft.Graph.Applications -Scope CurrentUser。请确保模块版本不低于 2.0,旧版 AzureAD 模块已停止更新,不建议在新项目中使用。

  2. 权限最小化原则:创建和管理应用注册需要 Application.ReadWrite.All 等高权限范围。在生产环境中,建议使用专用的高权限账户执行管理操作,日常应用开发使用 Application.Read.All 只读权限进行查询。遵循最小权限原则,降低因凭据泄露导致的攻击面。

  3. 密钥安全存储:客户端密钥(Client Secret)仅在创建时返回一次明文值,务必立即存入 Azure Key Vault 或其他受认可的密钥管理系统。禁止将密钥硬编码在代码中、写入配置文件或提交到 Git 仓库。对于生产环境,优先使用证书凭据而非密码凭据,安全性更高。

  4. 管理员同意流程:应用声明 API 权限后,部分高敏感权限(如 Mail.ReadFiles.Read.All)需要全局管理员在 Azure 门户或通过同意 URL 手动审批。自动化场景中可以使用 New-MgOauth2PermissionGrant 命令编程式同意,但这本身也需要 DelegatedPermissionGrant.Write.All 权限。

  5. 多租户应用的安全风险:当 SignInAudience 设为多租户模式时,其他 Entra ID 租户的用户也可以登录并授权该应用。在创建多租户应用前,务必评估是否有业务需求,并在应用中实现必要的租户验证逻辑,防止未预期的跨租户访问。

  6. 定期清理废弃应用:随着时间推移,租户中会积累大量不再使用的应用注册。建议定期运行审计脚本,标记超过 90 天无登录记录、凭据已过期且未续期的应用,经安全团队确认后通过 Remove-MgApplication 删除,减少潜在的攻击入口。

PowerShell 技能连载 - 条件访问策略管理

适用于 PowerShell 5.1 及以上版本

在混合办公和零信任架构日益普及的今天,条件访问(Conditional Access)已成为 Microsoft Entra ID(原 Azure AD)中最核心的安全控制手段之一。通过条件访问策略,管理员可以根据用户位置、设备状态、风险等级等信号,动态决定是否允许访问特定资源。然而,随着策略数量增长,手动管理门户中的数十条策略变得极其低效且容易出错。

PowerShell 与 Microsoft Graph API 的结合为条件访问策略的管理提供了自动化能力。无论是批量审计现有策略、快速创建标准化的安全基线策略,还是在紧急安全事件中快速调整策略状态,脚本化操作都比手动点击门户界面更可靠、更快速。特别是在多租户环境下,统一的脚本可以帮助安全团队确保所有租户的策略配置保持一致。

本文将介绍如何使用 PowerShell 通过 Microsoft Graph API 查询、创建、更新和报告条件访问策略,帮助你在日常运维和安全运营中提升效率。

连接 Microsoft Graph 并获取现有策略

操作条件访问策略需要 Policy.Read.AllPolicy.ReadWrite.ConditionalAccess 等权限。以下代码展示了如何连接 Graph 并列出所有现有策略的关键信息。

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
# 连接 Microsoft Graph,请求条件访问策略所需权限
Connect-MgGraph -Scopes `
"Policy.Read.All", `
"Policy.ReadWrite.ConditionalAccess", `
"Agreement.Read.All"

# 获取所有条件访问策略
$allPolicies = Get-MgIdentityConditionalAccessPolicy

# 输出策略摘要
$summary = foreach ($policy in $allPolicies) {
[PSCustomObject]@{
名称 = $policy.DisplayName
状态 = $policy.State
创建时间 = $policy.CreatedDateTime.ToString("yyyy-MM-dd")
修改时间 = $policy.ModifiedDateTime.ToString("yyyy-MM-dd")
包含用户 = if ($policy.Conditions.Users.IncludeUsers -contains "All") {
"所有用户"
} else {
"$($policy.Conditions.Users.IncludeUsers.Count) 个用户/组"
}
}
}

$summary | Format-Table -AutoSize

上述脚本首先连接到 Microsoft Graph 并声明必要的权限范围。然后使用 Get-MgIdentityConditionalAccessPolicy cmdlet 拉取所有条件访问策略,并通过 foreach 循环提取关键信息生成摘要对象。注意判断 IncludeUsers 是否包含 “All” 来显示友好的用户范围描述。

执行结果示例:

1
2
3
4
5
6
7
名称                                  状态     创建时间   修改时间   包含用户
---- ---- -------- -------- --------
要求所有用户使用 MFA enabled 2024-03-15 2025-09-20 所有用户
阻止来自高风险国家的登录 enabled 2024-06-01 2025-08-12 所有用户
要求管理员使用合规设备 enabled 2024-08-20 2025-10-01 3 个用户/组
仅限批准的客户端应用 enabled 2025-01-10 2025-01-10 所有用户
标记为报告专用的新设备策略 enabled forReportingButNotEnforced 2025-10-15 2025-10-15 所有用户

创建条件访问策略

创建策略时需要构造条件访问策略的完整参数对象,包括条件(Conditions)和访问控制(GrantControls)。以下示例创建一条要求特定组用户必须使用 MFA 的策略。

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
# 定义目标组和排除组
$targetGroupId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$excludeGroupId = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

# 构建策略参数
$newPolicyParams = @{
DisplayName = "要求运维组使用 MFA - 自动创建"
State = "enabledForReportingButNotEnforced"
Conditions = @{
Applications = @{
IncludeApplications = @("All")
}
Users = @{
IncludeUsers = @("None")
IncludeGroups = @($targetGroupId)
ExcludeGroups = @($excludeGroupId)
}
ClientAppTypes = @("browser", "mobileAppsAndDesktopClients")
Locations = @{
IncludeLocations = @("All")
}
}
GrantControls = @{
Operator = "OR"
BuiltInControls = @("mfa")
}
}

# 创建策略,初始设为仅报告模式以避免影响生产环境
$newPolicy = New-MgIdentityConditionalAccessPolicy -BodyParameter $newPolicyParams

Write-Host "策略已创建,ID: $($newPolicy.Id)"
Write-Host "当前状态: $($newPolicy.State)(仅报告模式)"
Write-Host "请在验证后手动切换为 'enabled'"

这个示例有几个值得注意的设计决策。首先,策略初始状态设为 enabledForReportingButNotEnforced(仅报告模式),这样在确认策略不会产生意外阻断之前,不会影响用户正常访问。其次,IncludeApplications 设为 @("All") 表示该策略应用于所有云应用。最后,GrantControls 使用 OR 操作符和 mfa 内置控制,表示只要满足 MFA 要求即可放行。

执行结果示例:

1
2
3
策略已创建,ID: abc12345-6789-def0-1234-567890abcdef
当前状态: enabledForReportingButNotEnforced(仅报告模式)
请在验证后手动切换为 'enabled'

批量切换策略状态

在安全事件响应场景中,可能需要快速启用或禁用一批条件访问策略。以下脚本演示如何按名称模式批量切换策略状态,并记录操作日志。

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
# 定义要切换的策略名称关键词和目标状态
$namePattern = "高风险"
$targetState = "disabled"

# 获取匹配的策略
$matchedPolicies = Get-MgIdentityConditionalAccessPolicy | `
Where-Object { $_.DisplayName -like "*$namePattern*" }

if ($matchedPolicies.Count -eq 0) {
Write-Host "未找到匹配 '$namePattern' 的策略"
return
}

Write-Host "找到 $($matchedPolicies.Count) 条匹配策略:"

# 记录操作日志
$logEntries = foreach ($policy in $matchedPolicies) {
$previousState = $policy.State

Write-Host (" 切换: {0} ({1} -> {2})" -f `
$policy.DisplayName, $previousState, $targetState)

# 更新策略状态
Update-MgIdentityConditionalAccessPolicy `
-ConditionalAccessPolicyId $policy.Id `
-BodyParameter @{ State = $targetState }

# 构建日志记录
[PSCustomObject]@{
操作时间 = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
策略ID = $policy.Id
策略名称 = $policy.DisplayName
原状态 = $previousState
新状态 = $targetState
操作人 = "自动化脚本"
}
}

# 导出操作日志到 CSV
$logPath = "ConditionalAccess_ChangeLog_{0:yyyyMMdd_HHmmss}.csv" -f (Get-Date)
$logEntries | Export-Csv -Path $logPath -NoTypeInformation -Encoding UTF8
Write-Host "`n操作日志已保存至: $logPath"

这段代码的核心逻辑是先按名称关键词筛选策略,然后逐一切换状态。每次操作都记录到对象数组中,最终导出为带时间戳的 CSV 文件。这种日志记录方式在安全审计中非常重要,可以追溯每次策略变更的详细上下文。使用 foreach 而非管道中的 ForEach-Object,使代码逻辑更清晰且方便调试。

执行结果示例:

1
2
3
4
5
找到 2 条匹配策略:
切换: 阻止来自高风险国家的登录 (enabled -> disabled)
切换: 高风险会话要求重新认证 (enabled -> disabled)

操作日志已保存至: ConditionalAccess_ChangeLog_20251021_143022.csv

生成条件访问策略合规报告

定期审计条件访问策略的配置是否符合安全基线要求是安全运营的重要环节。以下脚本生成一份包含策略详情和合规性检查的 HTML 报告。

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
# 定义合规检查规则
$complianceRules = @{
MustHaveMFAPolicy = "必须有一条针对所有用户的 MFA 策略"
MustBlockLegacyAuth = "必须阻止旧版身份验证协议"
MustHaveDeviceCompliance = "建议包含设备合规性检查策略"
}

# 获取所有策略并分析
$allPolicies = Get-MgIdentityConditionalAccessPolicy

$reportData = foreach ($policy in $allPolicies) {
$conditions = $policy.Conditions
$grantControls = $policy.GrantControls

# 提取关键属性用于分析
$appliesToAllUsers = $conditions.Users.IncludeUsers -contains "All"
$requiresMFA = $grantControls.BuiltInControls -contains "mfa"
$blocksAccess = $grantControls.BuiltInControls -contains "block"
$clientAppTypes = $conditions.ClientAppTypes -join ", "

[PSCustomObject]@{
策略名称 = $policy.DisplayName
状态 = $policy.State
应用于所有用户 = if ($appliesToAllUsers) { "是" } else { "否" }
要求MFA = if ($requiresMFA) { "是" } else { "否" }
阻止访问 = if ($blocksAccess) { "是" } else { "否" }
客户端类型 = $clientAppTypes
}
}

# 合规性检查
$hasEnabledMFA = $allPolicies | Where-Object {
$_.State -eq "enabled" -and
$_.Conditions.Users.IncludeUsers -contains "All" -and
$_.GrantControls.BuiltInControls -contains "mfa"
}

$hasBlockLegacy = $allPolicies | Where-Object {
$_.State -eq "enabled" -and
$_.Conditions.ClientAppTypes -contains "exchangeActiveSyncClients" -and
$_.GrantControls.BuiltInControls -contains "block"
}

Write-Host "=== 条件访问策略合规报告 ==="
Write-Host ("报告生成时间: {0}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"))
Write-Host ("策略总数: {0}" -f $allPolicies.Count)
Write-Host ""
Write-Host ("[合规检查] 全局 MFA 策略: {0}" -f $(if ($hasEnabledMFA) { "PASS" } else { "FAIL" }))
Write-Host ("[合规检查] 阻止旧版认证: {0}" -f $(if ($hasBlockLegacy) { "PASS" } else { "FAIL" }))
Write-Host ""
Write-Host "策略明细:"
$reportData | Format-Table -AutoSize

这个脚本实现了两层逻辑。第一层通过 foreach 循环为每条策略生成详细属性记录,用于展示策略的配置细节。第二层通过 Where-Object 进行合规性检查,判断是否存在启用的全局 MFA 策略和旧版认证阻止策略。这种结构化的报告方式可以帮助安全团队快速识别配置缺口。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
=== 条件访问策略合规报告 ===
报告生成时间: 2025-10-21 14:30:00
策略总数: 5

[合规检查] 全局 MFA 策略: PASS
[合规检查] 阻止旧版认证: FAIL

策略明细:

策略名称 状态 应用于所有用户 要求MFA 阻止访问 客户端类型
-------- ---- -------------- ------- -------- ----------
要求所有用户使用 MFA enabled 是 是 否 browser, mobileAppsAndDesktopClients
阻止来自高风险国家的登录 enabled 是 否 是 browser, mobileAppsAndDesktopClients
要求管理员使用合规设备 enabled 否 否 否 browser, mobileAppsAndDesktopClients
仅限批准的客户端应用 enabled 是 否 否 browser, mobileAppsAndDesktopClients
标记为报告专用的新设备策略 enabled 否 否 否 browser, mobileAppsAndDesktopClients

注意事项

  1. 权限要求:管理条件访问策略需要 Entra ID 中的条件访问管理员或安全管理员角色。使用 Connect-MgGraph 时,确保请求了 Policy.ReadWrite.ConditionalAccess 权限,且管理员已在门户中同意该权限。

  2. 先报告后启用:新建策略时务必先将 State 设为 enabledForReportingButNotEnforced(仅报告模式),在日志中确认策略不会误阻断正常用户后,再切换为 enabled。误启用的策略可能导致大面积锁定。

  3. 策略冲突检测:多条策略之间可能产生冲突或叠加效果。例如一条策略要求 MFA,另一条策略要求合规设备,两者同时满足时用户需要完成两种验证。建议在脚本中记录策略间的覆盖关系。

  4. 命名规范:为策略建立统一的命名规范(如 [类别] - [描述]),便于脚本按名称模式筛选和批量操作。避免使用无意义的默认名称如”New Policy”。

  5. 排除紧急访问账户:每条策略都应排除紧急访问(Break Glass)账户,确保在策略配置错误或服务中断时,紧急账户仍能登录进行修复。建议在脚本中加入自动化检查。

  6. 操作日志留存:所有通过脚本执行的策略变更都应记录到外部日志(CSV、数据库或 SIEM),包含操作时间、策略 ID、变更前后状态和操作人信息,以满足安全审计和合规要求。

PowerShell 技能连载 - Microsoft Graph 用户管理

适用于 PowerShell 5.1 及以上版本

为什么需要脚本化用户管理

在企业的 Microsoft 365 环境中,用户管理是一项高频且繁琐的日常工作。新员工入职需要创建账号、分配许可证、加入对应部门的安全组;员工离职则需要禁用账号、回收许可证、转移邮箱权限。如果通过 Microsoft 365 管理中心手动操作,不仅效率低下,还容易遗漏步骤导致安全风险。

Microsoft Graph PowerShell 模块(Microsoft.Graph)是微软官方推荐的 Entra ID(原 Azure AD)管理工具。它通过统一的 Graph API 端点提供用户全生命周期管理能力,包括创建、查询、更新、删除以及许可证分配。相比旧版 AzureAD 模块,Graph 模块支持更细粒度的权限控制和更好的分页性能。

本文将围绕用户管理的核心场景,演示如何使用 Microsoft.Graph.Users 模块完成用户创建与初始化、批量查询与筛选、以及用户生命周期自动化操作。

连接 Microsoft Graph 并准备环境

在开始管理用户之前,需要先安装模块并建立经过身份验证的连接。连接时通过 -Scopes 参数声明本次操作所需的最低权限,Graph 服务会根据登录账户的角色和已授予的同意来决定是否放行。

1
2
3
4
5
6
7
8
9
10
11
# 安装 Microsoft Graph Users 模块(仅首次)
Install-Module -Name Microsoft.Graph.Users -Scope CurrentUser -Force

# 连接到 Graph API,声明用户读写权限
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.Read.All"

# 确认连接状态
$context = Get-MgContext
Write-Host "已连接账户: $($context.Account)"
Write-Host "租户标识: $($context.TenantId)"
Write-Host "已授权范围: $($context.Scopes -join ', ')"
1
2
3
已连接账户: admin@contoso.com
租户标识: contoso.onmicrosoft.com
已授权范围: User.ReadWrite.All, Directory.Read.All

创建用户并完成初始配置

新员工入职时,通常需要一次性完成多项配置:创建账户、设置初始密码、填写部门信息。下面的脚本将多个步骤组织在一起,并为每个步骤添加错误处理,确保任何一个环节失败都能及时发现。

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
# 定义新用户的完整信息
$newUsers = @(
@{
DisplayName = "李明"
MailNickname = "ming.li"
UserPrincipalName = "ming.li@contoso.com"
Department = "工程部"
JobTitle = "高级开发工程师"
UsageLocation = "CN"
}
@{
DisplayName = "王芳"
MailNickname = "fang.wang"
UserPrincipalName = "fang.wang@contoso.com"
Department = "市场部"
JobTitle = "市场经理"
UsageLocation = "CN"
}
)

# 生成随机初始密码
function New-RandomPassword {
$length = 16
$chars = @()
# 确保包含各类字符
$chars += [char](Get-Random -Minimum 65 -Maximum 91) # 大写字母
$chars += [char](Get-Random -Minimum 97 -Maximum 123) # 小写字母
$chars += [char](Get-Random -Minimum 48 -Maximum 58) # 数字
$chars += [char](Get-Random -Minimum 35 -Maximum 39) # 特殊字符
# 填充剩余长度
foreach ($i in 1..($length - 4)) {
$chars += [char](Get-Random -Minimum 33 -Maximum 127)
}
# 打乱顺序并返回
$password = ($chars | Sort-Object { Get-Random }) -join ''
return $password
}

# 批量创建用户
$results = @()

foreach ($userInfo in $newUsers) {
$tempPassword = New-RandomPassword
$body = @{
AccountEnabled = $true
DisplayName = $userInfo.DisplayName
MailNickname = $userInfo.MailNickname
UserPrincipalName = $userInfo.UserPrincipalName
Department = $userInfo.Department
JobTitle = $userInfo.JobTitle
UsageLocation = $userInfo.UsageLocation
PasswordProfile = @{
ForceChangePasswordNextSignIn = $true
Password = $tempPassword
}
}

try {
$user = New-MgUser -BodyParameter $body
$results += [PSCustomObject]@{
Status = "成功"
UPN = $userInfo.UserPrincipalName
UserId = $user.Id
TempPwdLen = $tempPassword.Length
}
Write-Host "[OK] 已创建用户: $($userInfo.DisplayName)" -ForegroundColor Green
}
catch {
$results += [PSCustomObject]@{
Status = "失败"
UPN = $userInfo.UserPrincipalName
UserId = $_.Exception.Message.Substring(0, [Math]::Min(60, $_.Exception.Message.Length))
TempPwdLen = 0
}
Write-Host "[FAIL] 创建失败: $($userInfo.DisplayName) - $($_.Exception.Message)" -ForegroundColor Red
}
}

# 输出创建结果汇总
$results | Format-Table -AutoSize
1
2
3
4
5
6
7
[OK] 已创建用户: 李明
[OK] 已创建用户: 王芳

Status UPN UserId TempPwdLen
------ --- ------ ----------
成功 ming.li@contoso.com a1b2c3d4-e5f6-7890-abcd-ef1234567890 16
成功 fang.wang@contoso.com b2c3d4e5-f6a7-8901-bcde-f12345678901 16

查询与筛选用户信息

用户数据是企业目录的核心资产,掌握高效的查询技巧对日常运维至关重要。Graph API 支持 OData 筛选语法,可以对用户属性进行精确匹配和排序。对于复杂筛选条件,还可以使用 -ConsistencyLevel eventual 开启高级查询功能。

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
# 基本查询:列出所有启用的用户
$activeUsers = Get-MgUser -All -Filter "accountEnabled eq true" |
Sort-Object DisplayName |
Select-Object DisplayName, UserPrincipalName, Department, JobTitle

Write-Host "当前活跃用户数: $($activeUsers.Count)"
$activeUsers | Format-Table -AutoSize

# 按部门筛选
$engineers = Get-MgUser -Filter "department eq '工程部'" `
-ConsistencyLevel eventual -CountVariable engCount -All

Write-Host "`n工程部人数: $engCount"
foreach ($eng in $engineers) {
Write-Host " - $($eng.DisplayName) ($($eng.UserPrincipalName))"
}

# 模糊搜索:查找显示名包含"李"的用户
$searchResult = Get-MgUser -Filter "startsWith(displayName,'李')" `
-ConsistencyLevel eventual -All

Write-Host "`n搜索'李'姓用户结果:"
foreach ($item in $searchResult) {
Write-Host " $($item.DisplayName) | $($item.Department) | $($item.Mail)"
}

# 查看用户的详细信息(包括扩展属性)
$targetUser = Get-MgUser -UserId "ming.li@contoso.com" -Property DisplayName, UserPrincipalName, Department, JobTitle, CreatedDateTime, LastPasswordChangeDateTime, SignInActivity

[PSCustomObject]@{
显示名 = $targetUser.DisplayName
邮箱 = $targetUser.UserPrincipalName
部门 = $targetUser.Department
职位 = $targetUser.JobTitle
创建时间 = $targetUser.AdditionalProperties.createdDateTime
上次改密 = $targetUser.AdditionalProperties.lastPasswordChangeDateTime
上次登录 = $targetUser.AdditionalProperties.signInActivity.lastSignInDateTime
} | Format-List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
当前活跃用户数: 128

DisplayName UserPrincipalName Department JobTitle
----------- ----------------- ---------- --------
Alice Chen alice.chen@contoso.com 工程部 前端开发
李明 ming.li@contoso.com 工程部 高级开发工程师
王芳 fang.wang@contoso.com 市场部 市场经理

工程部人数: 45
- Alice Chen (alice.chen@contoso.com)
- 李明 (ming.li@contoso.com)

搜索'李'姓用户结果:
李明 | 工程部 | ming.li@contoso.com
李娜 | 财务部 | na.li@contoso.com

显示名 : 李明
邮箱 : ming.li@contoso.com
部门 : 工程部
职位 : 高级开发工程师
创建时间 : 2025-10-10T02:30:00Z
上次改密 : 2025-10-10T02:30:00Z
上次登录 : 2025-10-10T08:15:00Z

用户生命周期自动化

离职流程是用户管理中最容易出问题的环节。一个完整的离职处理需要按顺序执行:禁用账户、移除组成员身份、回收许可证、更新描述信息。将这个过程封装为函数,可以确保每次操作都不遗漏步骤。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
function Disable-UserLifecycle {
<#
.SYNOPSIS
执行用户离职处理流程
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string]$UserPrincipalName,

[string]$Reason = "员工离职"
)

Write-Host "`n========== 离职处理: $UserPrincipalName ==========" -ForegroundColor Cyan

# 第一步:获取用户信息
try {
$user = Get-MgUser -UserId $UserPrincipalName -ErrorAction Stop
Write-Host "[1/5] 用户信息已获取: $($user.DisplayName)" -ForegroundColor Green
}
catch {
Write-Host "[1/5] 用户不存在或无法访问: $UserPrincipalName" -ForegroundColor Red
return
}

# 第二步:禁用账户
if ($PSCmdlet.ShouldProcess($UserPrincipalName, "禁用账户")) {
Update-MgUser -UserId $UserPrincipalName -AccountEnabled:$false
Write-Host "[2/5] 账户已禁用" -ForegroundColor Green
}

# 第三步:移除所有组成员身份
$memberships = Get-MgUserMemberOf -UserId $UserPrincipalName -All
$groupCount = 0

foreach ($member in $memberships) {
# 仅处理组类型(过滤掉目录角色等)
if ($member.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.group') {
try {
Remove-MgGroupMember -GroupId $member.Id -DirectoryObjectId $user.Id -ErrorAction SilentlyContinue
$groupCount++
}
catch {
# 动态组无法手动移除成员,记录即可
Write-Host " 跳过动态组: $($member.AdditionalProperties.displayName)" -ForegroundColor Yellow
}
}
}
Write-Host "[3/5] 已从 $groupCount 个组中移除" -ForegroundColor Green

# 第四步:回收许可证
$licenses = Get-MgUserLicenseDetail -UserId $UserPrincipalName -ErrorAction SilentlyContinue
$licenseSkuIds = @()

foreach ($lic in $licenses) {
$licenseSkuIds += $lic.SkuId
}

if ($licenseSkuIds.Count -gt 0) {
$removeLicenses = @()
foreach ($skuId in $licenseSkuIds) {
$removeLicenses += @{ SkuId = $skuId }
}
Set-MgUserLicense -UserId $UserPrincipalName `
-AddLicenses @() `
-RemoveLicenses $removeLicenses
Write-Host "[4/5] 已回收 $($licenseSkuIds.Count) 个许可证" -ForegroundColor Green
}
else {
Write-Host "[4/5] 无需回收许可证" -ForegroundColor Yellow
}

# 第五步:更新描述记录离职原因和日期
$leaveDate = Get-Date -Format "yyyy-MM-dd"
Update-MgUser -UserId $UserPrincipalName `
-AboutMe "$Reason - 处理日期: $leaveDate"
Write-Host "[5/5] 离职记录已更新 ($leaveDate)" -ForegroundColor Green

Write-Host "`n========== 处理完成 ==========" -ForegroundColor Cyan

# 返回处理结果
return [PSCustomObject]@{
User = $user.DisplayName
UPN = $UserPrincipalName
Disabled = $true
GroupsRemoved = $groupCount
LicensesRevoked = $licenseSkuIds.Count
ProcessedDate = $leaveDate
}
}

# 执行离职处理
$leavingUsers = @("ming.li@contoso.com", "fang.wang@contoso.com")

$reports = @()
foreach ($upn in $leavingUsers) {
$report = Disable-UserLifecycle -UserPrincipalName $upn -Reason "合同到期离职"
$reports += $report
}

# 输出处理报告
Write-Host "`n离职处理汇总报告:"
$reports | 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
========== 离职处理: ming.li@contoso.com ==========
[1/5] 用户信息已获取: 李明
[2/5] 账户已禁用
[3/5] 已从 5 个组中移除
[4/5] 已回收 1 个许可证
[5/5] 离职记录已更新 (2025-10-10)

========== 处理完成 ==========

========== 离职处理: fang.wang@contoso.com ==========
[1/5] 用户信息已获取: 王芳
[2/5] 账户已禁用
[3/5] 已从 3 个组中移除
[4/5] 已回收 2 个许可证
[5/5] 离职记录已更新 (2025-10-10)

========== 处理完成 ==========

离职处理汇总报告:

User UPN Disabled GroupsRemoved LicensesRevoked ProcessedDate
---- --- -------- ------------- --------------- -------------
李明 ming.li@contoso.com True 5 1 2025-10-10
王芳 fang.wang@contoso.com True 3 2 2025-10-10

注意事项

  1. 权限最小化原则:连接 Graph API 时应按操作类型声明最小权限集。只读操作用 User.Read.All,写操作才需要 User.ReadWrite.All。避免在脚本中统一请求全部权限,防止权限滥用带来的安全审计风险。

  2. ConsistencyLevel 与高级查询:使用 startsWithendsWitheq 对非索引属性筛选时,必须添加 -ConsistencyLevel eventual 参数,并使用 -CountVariable 接收匹配数量。否则 Graph API 会返回”不支持的查询”错误。

  3. 分页与 -All 参数:默认情况下 Get-MgUser 只返回前 100 条记录。用户数超过 100 时必须加 -All 参数自动处理分页,或者通过 -Top-Skip 参数手动分页控制内存占用。

  4. 密码策略合规:创建用户时的初始密码必须满足 Entra ID 的密码复杂度要求(至少 8 位,包含大小写字母、数字和特殊字符中的三类)。建议配合 ForceChangePasswordNextSignIn = $true,确保用户首次登录时强制修改密码。

  5. 许可证回收时机:禁用账户后许可证并不会自动释放,必须显式调用 Set-MgUserLicense -RemoveLicenses 回收。如果许可证余额紧张,建议在离职流程中优先执行许可证回收步骤,避免因中间步骤失败导致许可证被占用。

  6. 错误处理与幂等性:批量操作用户时,务必对每个步骤添加 try/catch 错误处理。对于可能重复执行的场景(如入职脚本被运行两次),应在创建前先检查用户是否已存在,使用 Get-MgUser -Filter 替代直接创建,保证脚本的幂等性。

PowerShell 技能连载 - Microsoft Graph API 高级操作

适用于 PowerShell 5.1 及以上版本,需要 Microsoft Graph PowerShell SDK 或 Azure AD 租户

Microsoft Graph API 是微软云服务的统一入口——Azure AD(Entra ID)用户管理、Teams 消息、SharePoint 文件、Outlook 邮件、OneDrive,几乎所有 Microsoft 365 服务都通过 Graph API 暴露。Microsoft Graph PowerShell SDK 封装了这些 API,让运维人员可以用 PowerShell 管理整个 Microsoft 365 生态。

本文将讲解 Microsoft Graph PowerShell SDK 的高级用法和实用的管理场景。

连接与认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 安装 Microsoft Graph PowerShell SDK
# Install-Module Microsoft.Graph -Scope CurrentUser

# 连接到 Graph API(交互式登录)
Connect-MgGraph -Scopes "User.Read.All", "Group.Read.All", "Directory.Read.All"

# 查看当前连接信息
Get-MgContext | Format-List

# 使用应用权限连接(适合自动化)
# $clientId = "your-app-id"
# $tenantId = "your-tenant-id"
# $certThumbprint = "cert-thumbprint"
# Connect-MgGraph -ClientId $clientId -TenantId $tenantId -CertificateThumbprint $certThumbprint

# 断开连接
# Disconnect-MgGraph

执行结果示例:

1
2
3
4
5
6
ClientId              : 14d82eec-204b-4c2f-b7e8-296a70dab67e
TenantId : contoso.onmicrosoft.com
Scopes : {User.Read.All, Group.Read.All, Directory.Read.All}
AuthType : Delegated
TokenCredentialType : InteractiveBrowser
Account : admin@contoso.onmicrosoft.com

用户管理

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
# 查询用户
# 获取所有用户
$users = Get-MgUser -All -Property DisplayName, Mail, JobTitle, Department, AccountEnabled
Write-Host "总用户数:$($users.Count)" -ForegroundColor Cyan

# 按部门筛选
$engineering = $users | Where-Object { $_.Department -eq "工程部" }
Write-Host "工程部人数:$($engineering.Count)"

# 搜索特定用户
$targetUser = Get-MgUser -Filter "DisplayName eq '张三'" -Property DisplayName, Mail, JobTitle
$targetUser | Format-List DisplayName, Mail, JobTitle

# 创建新用户
$newUser = @{
AccountEnabled = $true
DisplayName = "测试用户"
MailNickname = "testuser"
UserPrincipalName = "testuser@contoso.onmicrosoft.com"
PasswordProfile = @{
Password = "TempP@ss123!"
ForceChangePasswordNextSignIn = $true
}
}

# $created = New-MgUser -BodyParameter $newUser
# Write-Host "已创建用户:$($created.DisplayName) ($($created.Id))"

# 批量禁用离职用户
$disabledUsers = Get-MgUser -Filter "AccountEnabled eq false" -Property DisplayName, Mail
Write-Host "`n已禁用账户:$($disabledUsers.Count) 个" -ForegroundColor Yellow
$disabledUsers | Select-Object DisplayName, Mail | Format-Table -AutoSize

# 查看用户的登录日志
$signIns = Get-MgAuditLogSignIn -Top 5 -Filter "UserId eq '$($targetUser.Id)'"
$signIns | Select-Object CreatedDateTime, AppDisplayName, ClientAppUsed, Status |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
总用户数:2456
工程部人数:128

DisplayName : 张三
Mail : zhangsan@contoso.com
JobTitle : 高级工程师

已禁用账户:15 个
DisplayName Mail
----------- ----
离职用户A usera@contoso.com
离职用户B userb@contoso.com

CreatedDateTime AppDisplayName ClientAppUsed Status
--------------- -------------- ------------- ------
2025-07-28T07:30:15Z Outlook Browser Success
2025-07-28T07:15:22Z Microsoft Teams Desktop App Success

组与许可证管理

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
# 组管理
$groups = Get-MgGroup -All -Property DisplayName, Description, GroupTypes, MembershipRule
Write-Host "总组数:$($groups.Count)" -ForegroundColor Cyan

# 查找安全组
$securityGroups = $groups | Where-Object { $_.GroupTypes -notcontains "Unified" }
Write-Host "安全组:$($securityGroups.Count) 个"

# 查找 Microsoft 365 组
$m365Groups = $groups | Where-Object { $_.GroupTypes -contains "Unified" }
Write-Host "M365 组:$($m365Groups.Count) 个"

# 查看组成员
$groupName = "工程部-开发组"
$group = Get-MgGroup -Filter "DisplayName eq '$groupName'"
$members = Get-MgGroupMember -GroupId $group.Id -All
Write-Host "`n$groupName 成员($($members.Count) 人):" -ForegroundColor Cyan
$members | ForEach-Object {
$user = Get-MgUser -UserId $_.Id -Property DisplayName, Mail
Write-Host " $($user.DisplayName) - $($user.Mail)"
}

# 许可证管理
$subscribedSkus = Get-MgSubscribedSku
Write-Host "`n许可证概览:" -ForegroundColor Cyan
foreach ($sku in $subscribedSkus) {
$available = $sku.PrepaidUnits.Enabled - $sku.ConsumedUnits
Write-Host " $($sku.SkuPartNumber):已购买 $($sku.PrepaidUnits.Enabled),已分配 $($sku.ConsumedUnits),可用 $available"
}

# 批量添加用户到组
function Add-UsersToGroup {
param(
[string]$GroupName,
[string[]]$UserEmails
)

$group = Get-MgGroup -Filter "DisplayName eq '$GroupName'"
if (-not $group) {
Write-Host "组不存在:$GroupName" -ForegroundColor Red
return
}

foreach ($email in $UserEmails) {
$user = Get-MgUser -Filter "Mail eq '$email'" -Property Id, DisplayName
if ($user) {
try {
New-MgGroupMember -GroupId $group.Id -DirectoryObjectId $user.Id
Write-Host "已添加:$($user.DisplayName) => $GroupName" -ForegroundColor Green
} catch {
Write-Host "添加失败:$email - $($_.Exception.Message)" -ForegroundColor Yellow
}
}
}
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
总组数:89
安全组:65
M365 组:24

工程部-开发组 成员(15 人):
张三 - zhangsan@contoso.com
李四 - lisi@contoso.com
王五 - wangwu@contoso.com

许可证概览:
ENTERPRISEPACK:已购买 500,已分配 245,可用 255
EMS_E5:已购买 200,已分配 180,可用 20

设备与条件访问

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
# 设备管理
$devices = Get-MgDevice -All -Property DisplayName, OperatingSystem, ApproximateLastSignInDateTime, TrustType
Write-Host "已注册设备:$($devices.Count)" -ForegroundColor Cyan

$deviceSummary = $devices | Group-Object OperatingSystem | Sort-Object Count -Descending
$deviceSummary | ForEach-Object {
Write-Host " $($_.Name):$($_.Count) 台" -ForegroundColor Cyan
}

# 查找过期设备(90 天未登录)
$cutoff = (Get-Date).AddDays(-90)
$staleDevices = $devices | Where-Object {
$_.ApproximateLastSignInDateTime -and
[datetime]$_.ApproximateLastSignInDateTime -lt $cutoff
}
Write-Host "`n过期设备(90天未活跃):$($staleDevices.Count) 台" -ForegroundColor Yellow

# 条件访问策略
$caPolicies = Get-MgIdentityConditionalAccessPolicy
Write-Host "`n条件访问策略:$($caPolicies.Count) 条" -ForegroundColor Cyan
$caPolicies | Select-Object DisplayName, State |
Format-Table -AutoSize

# 导出用户报告
function Export-MgUserReport {
param([string]$OutputPath = "C:\Reports\users.csv")

$users = Get-MgUser -All -Property DisplayName, Mail, Department, JobTitle, AccountEnabled, CreatedDateTime

$report = $users | Select-Object DisplayName, Mail, Department, JobTitle,
@{N='Enabled'; E={$_.AccountEnabled}},
@{N='Created'; E={$_.CreatedDateTime.ToString('yyyy-MM-dd')}}

$report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
Write-Host "用户报告已导出:$OutputPath ($($users.Count) 条)" -ForegroundColor Green
}

Export-MgUserReport

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
已注册设备:356
Windows:245 台
macOS:67 台
iOS:32 台
Android:12 台

过期设备(90天未活跃):23 台

条件访问策略:8 条
DisplayName State
----------- -----
要求 MFA enabled
阻止旧版浏览器 enabled
要求合规设备 enabled
报告专用模式 - 位置检测 enabledForReportingButNotEnforced

用户报告已导出:C:\Reports\users.csv (2456 条)

注意事项

  1. 权限范围:连接时指定最小必要权限,避免使用过大的权限范围
  2. 分页:大量数据需要使用 -All 参数或手动处理分页,默认只返回部分结果
  3. 速率限制:Graph API 有请求频率限制,批量操作时添加 Start-Sleep 避免触发限流
  4. 应用权限 vs 委托权限:自动化脚本使用应用权限(证书认证),交互操作使用委托权限(用户登录)
  5. Consent:某些权限需要管理员同意(Admin Consent),首次使用时需要授权
  6. 模块更新:Graph API 更新频繁,定期更新 SDK 模块:Update-Module Microsoft.Graph

PowerShell 技能连载 - Microsoft Graph API 集成

适用于 PowerShell 5.1 及以上版本,需安装 Microsoft.Graph 模块

Microsoft Graph 是 Microsoft 365 平台的统一 API 网关——它整合了 Azure AD(现称 Entra ID)、Exchange Online、SharePoint、Teams、OneDrive 等所有 Microsoft 365 服务的数据和操作。通过 PowerShell 的 Microsoft.Graph 模块,运维人员可以用脚本化管理用户、组、许可证、设备策略等,替代传统的多个独立模块。

本文将讲解 Microsoft Graph PowerShell 的连接、用户管理、组操作和常用自动化场景。

连接与认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 安装 Microsoft Graph 模块
Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force

# 选择性安装(更快)
# Install-Module -Name Microsoft.Graph.Users, Microsoft.Graph.Groups -Scope CurrentUser

# 连接到 Microsoft Graph(交互式登录)
Connect-MgGraph -Scopes "User.Read.All", "Group.Read.All", "Directory.Read.All"

# 查看当前连接信息
Get-MgContext | Select-Object Account, Tenant, Scopes |
Format-List

# 使用应用权限连接(自动化场景)
$clientId = "your-app-client-id"
$tenantId = "your-tenant-id"
$certThumbprint = "ABC123DEF456"

Connect-MgGraph -ClientId $clientId -TenantId $tenantId `
-CertificateThumbprint $certThumbprint

# 断开连接
Disconnect-MgGraph

执行结果示例:

1
2
3
4
5
Account : admin@contoso.com
Tenant : contoso.onmicrosoft.com
Scopes : {User.Read.All, Group.Read.All, Directory.Read.All}

Welcome To Microsoft Graph!

用户管理

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
# 列出所有用户
Get-MgUser -All -ConsistencyLevel eventual |
Select-Object DisplayName, UserPrincipalName, Mail, Department, JobTitle |
Format-Table -AutoSize

# 搜索特定用户
$user = Get-MgUser -Filter "displayName eq 'John Doe'" -ConsistencyLevel eventual
$user | Select-Object Id, DisplayName, UserPrincipalName, Department

# 创建新用户
$newUser = @{
AccountEnabled = $true
DisplayName = "张伟"
MailNickname = "wei.zhang"
UserPrincipalName = "wei.zhang@contoso.com"
Department = "IT"
JobTitle = "DevOps 工程师"
PasswordProfile = @{
ForceChangePasswordNextSignIn = $true
Password = "TempP@ssw0rd123!"
}
}

New-MgUser -BodyParameter $newUser
Write-Host "用户已创建:wei.zhang@contoso.com" -ForegroundColor Green

# 更新用户信息
Update-MgUser -UserId "wei.zhang@contoso.com" -Department "DevOps"

# 禁用用户账户
Update-MgUser -UserId "wei.zhang@contoso.com" -AccountEnabled:$false

# 获取用户的组成员身份
Get-MgUserMemberOf -UserId "john.doe@contoso.com" |
ForEach-Object {
$group = Get-MgGroup -GroupId $_.Id
[PSCustomObject]@{
GroupName = $group.DisplayName
GroupType = $group.GroupTypes -join ','
}
} | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DisplayName  UserPrincipalName       Department  JobTitle
----------- ----------------- ---------- --------
John Doe john.doe@contoso.com Engineering Senior Dev
Jane Smith jane.smith@contoso.com HR HR Manager

Id : 12345678-abcd-...
DisplayName : John Doe
UserPrincipalName : john.doe@contoso.com

用户已创建:wei.zhang@contoso.com

GroupName GroupType
--------- ---------
All Users {}
IT-Admins {}
Developers {DynamicMembership}

组管理

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
# 列出所有组
Get-MgGroup -All |
Select-Object DisplayName, Description,
@{N='类型'; E={if ($_.GroupTypes -contains 'Unified') { 'Microsoft 365' } else { 'Security' }}},
@{N='成员数'; E={$_.Members.Count}} |
Sort-Object DisplayName |
Format-Table -AutoSize

# 创建安全组
New-MgGroup -DisplayName "Cloud-Admins" `
-Description "云平台管理员组" `
-MailEnabled:$false `
-SecurityEnabled:$true

# 添加成员到组
$userId = (Get-MgUser -Filter "displayName eq 'John Doe'").Id
$groupId = (Get-MgGroup -Filter "displayName eq 'Cloud-Admins'").Id

New-MgGroupMember -GroupId $groupId -DirectoryObjectId $userId
Write-Host "已将 John Doe 添加到 Cloud-Admins 组" -ForegroundColor Green

# 查看组成员
Get-MgGroupMember -GroupId $groupId |
ForEach-Object {
Get-MgUser -UserId $_.Id |
Select-Object DisplayName, UserPrincipalName
} | Format-Table -AutoSize

# 创建动态组(基于规则的自动成员管理)
$dynamicGroup = @{
DisplayName = "All-Engineering"
Description = "工程部门所有成员"
GroupTypes = @("DynamicMembership")
MailEnabled = $false
SecurityEnabled = $true
MembershipRule = 'user.department -eq "Engineering"'
MembershipRuleProcessingState = "On"
}

New-MgGroup -BodyParameter $dynamicGroup

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
DisplayName    类型           成员数
----------- ---- ------
All Users Security 245
IT-Admins Security 12
Developers Microsoft 365 35
Cloud-Admins Security 3

已将 John Doe 添加到 Cloud-Admins

DisplayName UserPrincipalName
----------- -----------------
John Doe john.doe@contoso.com
Jane Smith jane.smith@contoso.com

许可证管理

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
# 查看租户可用的许可证
Get-MgSubscribedSku |
Select-Object SkuPartNumber,
@{N='已用'; E={$_.PrepaidUnits.Enabled - $_.Consumed}},
@{N='总量'; E={$_.PrepaidUnits.Enabled}},
@{N='已消耗'; E={$_.Consumed}} |
Format-Table -AutoSize

# 为用户分配许可证
$license = Get-MgSubscribedSku | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK'

Set-MgUserLicense -UserId "wei.zhang@contoso.com" `
-AddLicenses @{ SkuId = $license.SkuId } `
-RemoveLicenses @()

Write-Host "已分配 Office 365 E3 许可证" -ForegroundColor Green

# 批量分配许可证
$users = Get-MgUser -Filter "department eq 'Engineering'" -ConsistencyLevel eventual -All
$skuId = (Get-MgSubscribedSku | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK').SkuId

foreach ($user in $users) {
Set-MgUserLicense -UserId $user.Id `
-AddLicenses @{ SkuId = $skuId } `
-RemoveLicenses @()
Write-Host "已分配:$($user.DisplayName)" -ForegroundColor Green
}

执行结果示例:

1
2
3
4
5
6
7
8
SkuPartNumber       已用 总量 已消耗
------------ ---- ---- ------
ENTERPRISEPACK 120 250 130
EMS 45 100 55

已分配 Office 365 E3 许可证
已分配:John Doe
已分配:Alice Smith

注意事项

  1. 权限范围:使用 -Scopes 参数指定最小必要权限,避免过度授权
  2. ConsistencyLevel:部分高级查询需要添加 -ConsistencyLevel eventual$Count 参数
  3. 分页处理:大量结果时使用 -All 参数自动处理分页,否则只返回第一页
  4. 应用权限:自动化脚本应使用应用权限(Client Credentials 流),而非用户委派权限
  5. Graph API 版本:MgGraph 模块默认使用 v1.0 端点,使用 Invoke-MgGraphRequest 可以调用 beta 端点
  6. 速率限制:Microsoft Graph 有 API 调用频率限制,大批量操作时添加适当延迟