You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Get-AzureADAuditSignInLogs调用触发节流限制的规避方案

Azure AD 登录日志拉取节流问题修复方案

核心原因

当前实现触发429节流有三个明确问题:

  1. 使用-All:$True一次性拉取7天全量日志,单请求承载数据量过大,直接触发接口单请求负载阈值
  2. 重试逻辑使用固定200秒休眠,没有读取接口返回的Retry-After响应头要求的等待时长,要么等待时长不足触发二次节流,要么等待过久浪费时间
  3. 所用2.0.2.105版本的AzureADPreview模块本身存在节流处理缺陷,内置自动分页的请求密度过高,不会自动适配节流规则

非Graph API的可行解决方法

  • 拆分查询时间窗口:将7天的查询范围拆分为1-2小时粒度的小时间块逐块拉取,将单请求返回数据量控制在1万条以内,从根源降低单请求负载,这是最有效的规避手段
  • 动态重试逻辑:捕获429异常时从响应头读取Retry-After值作为休眠时长,设置最多5次的重试上限,解决固定时长重试的适配问题
  • 手动控制分页节奏:弃用-All:$True的自动快速分页逻辑,手动指定-Top 1000(接口支持的单页最大返回条数),每拉取完一页休眠2-3秒再请求下一页,控制单位时间内的请求频率
  • 升级模块版本:将AzureADPreview模块升级到2.0.2.177以上的稳定版本,修复旧版本内置的节流适配bug

注意:不要使用多线程、多账号并发的方式尝试绕过节流,AAD审计接口的节流计数是按租户维度统计的,并发请求只会触发更长时间的限流。

修正后可直接运行的代码

# 拉取最近7天的Azure AD登录日志
CLS
$totalDays = 7
$windowHours = 2 # 每2小时一个查询块,日志量大可调整为1小时
$maxRetries = 5
$Report = [System.Collections.Generic.List[Object]]::new()

$currentEnd = Get-Date
$currentStart = $currentEnd.AddDays(-$totalDays)

Write-Host "开始拉取Azure Active Directory登录日志..."

while ($currentStart -lt $currentEnd) {
    $windowEnd = $currentStart.AddHours($windowHours)
    if ($windowEnd -gt $currentEnd) { $windowEnd = $currentEnd }
    # 格式化时间为过滤器要求的UTC格式
    $filterStart = Get-Date $currentStart -Format "yyyy-MM-ddTHH:mm:ssZ"
    $filterEnd = Get-Date $windowEnd -Format "yyyy-MM-ddTHH:mm:ssZ"
    $filter = "createdDateTime ge $filterStart and createdDateTime lt $filterEnd"
    
    $retryCount = 0
    $windowSuccess = $false
    while (-not $windowSuccess -and $retryCount -lt $maxRetries) {
        try {
            Write-Host "正在拉取时间窗口 $filterStart 至 $filterEnd 的日志..."
            # 手动分页,单页1000条,弃用-All参数
            $page = Get-AzureADAuditSignInLogs -Filter $filter -Top 1000
            # 处理当前页数据
            foreach ($Rec in $page) {
                switch ($Rec.Status.ErrorCode) {
                    "0" { $Status = "Success" }
                    default { $Status = $Rec.Status.FailureReason }
                }
                $ReportLine = [PSCustomObject] @{
                    TimeStamp   = Get-Date($Rec.CreatedDateTime) -Format g
                    User        = $Rec.UserPrincipalName
                    Name        = $Rec.UserDisplayName
                    IPAddress   = $Rec.IpAddress
                    ClientApp   = $Rec.ClientAppUsed
                    Device      = $Rec.DeviceDetail.OperatingSystem
                    Location    = $Rec.Location.City + ", " + $Rec.Location.State + ", " + $Rec.Location.CountryOrRegion
                    Appname     = $Rec.AppDisplayName
                    Resource    = $Rec.ResourceDisplayName
                    Status      = $Status
                    Correlation = $Rec.CorrelationId
                    Interactive = $Rec.IsInteractive
                }
                $Report.Add($ReportLine)
            }
            # 处理后续分页
            while ($null -ne $page.NextLink) {
                Start-Sleep -Seconds 2 # 翻页前短休眠控频
                $page = Get-AzureADAuditSignInLogs -NextLink $page.NextLink
                foreach ($Rec in $page) {
                    switch ($Rec.Status.ErrorCode) {
                        "0" { $Status = "Success" }
                        default { $Status = $Rec.Status.FailureReason }
                    }
                    $ReportLine = [PSCustomObject] @{
                        TimeStamp   = Get-Date($Rec.CreatedDateTime) -Format g
                        User        = $Rec.UserPrincipalName
                        Name        = $Rec.UserDisplayName
                        IPAddress   = $Rec.IpAddress
                        ClientApp   = $Rec.ClientAppUsed
                        Device      = $Rec.DeviceDetail.OperatingSystem
                        Location    = $Rec.Location.City + ", " + $Rec.Location.State + ", " + $Rec.Location.CountryOrRegion
                        Appname     = $Rec.AppDisplayName
                        Resource    = $Rec.ResourceDisplayName
                        Status      = $Status
                        Correlation = $Rec.CorrelationId
                        Interactive = $Rec.IsInteractive
                    }
                    $Report.Add($ReportLine)
                }
            }
            $windowSuccess = $true
        }
        catch {
            $retryCount++
            # 优先读取Retry-After头的等待时长,读不到默认等60秒
            $retryAfter = 60
            if ($_.Exception.Response.Headers -and $_.Exception.Response.Headers.Contains("Retry-After")) {
                $retryAfter = [int]$_.Exception.Response.Headers.GetValues("Retry-After")[0]
            }
            Write-Host "触发节流,等待 $retryAfter 秒后重试,当前第 $retryCount 次重试..."
            Start-Sleep -Seconds $retryAfter
        }
    }
    # 移动到下一个时间窗口
    $currentStart = $windowEnd
    Start-Sleep -Seconds 1 # 时间窗口之间加短间隔
}

Write-Host "处理完成,共拉取到 $($Report.Count) 条登录审计记录。"

内容的提问来源于stack exchange,提问作者Yahtzee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 00:03:20