PAYATHON 2026

PowerShell 调用 Azure Billing REST API 出现授权错误

支付小周

结论

问题不在 client_secret 的类型。APPkey 本质上也是应用的客户端密钥。既然已经成功取得访问令牌,说明客户端凭据认证基本成功。

缺少的是 Azure RBAC 授权。需要为该应用在当前租户中的服务主体(Service Principal)授予目标订阅或更具体计费范围的读取权限,例如内置的 Billing Reader。客户端密钥只能证明应用身份,不会自动赋予应用访问订阅资源或账单数据的权限。

为什么取得 Token 后仍然被拒绝

Azure 的身份认证和权限验证是两个独立环节:

  1. Microsoft Entra ID 使用 client_id 和 client_secret 验证应用身份并签发 Token。
  2. Azure Resource Manager 根据 Token 中的服务主体身份及其 Azure RBAC 角色,判断是否允许访问资源。

因此,能够取得 Token,并不代表能够读取订阅账单。

错误信息中的:

does not have authorization to perform action
'Microsoft.Consumption/usageDetails/read'
over scope '/subscriptions/XXXX'

说明请求中的客户端身份已经被识别,但该客户端在目标订阅范围内没有相应权限。也可能是权限分配到了错误的租户、订阅或对象。

如果错误文本中的 usageDetial 不是转录笔误,还需要检查实际请求和完整错误信息。正确的资源操作通常是 Microsoft.Consumption/usageDetails/read。

配置服务主体权限

可以在 Azure 门户中完成角色分配:

  1. 打开目标订阅。
  2. 进入“访问控制 (IAM)”。
  3. 选择“添加角色分配”。
  4. 选择 Billing Reader。
  5. 将成员类型设为“用户、组或服务主体”。
  6. 搜索应用名称或 Application (client) ID,选中对应的服务主体。
  7. 完成角色分配后,重新获取 Token。

如果只需读取成本和使用量,也可以根据组织的权限策略评估 Cost Management Reader。该角色是否覆盖当前调用的 API,要看角色定义中是否包含所需操作。Reader 通常也包含资源读取操作,但授权范围更广,未必符合最小权限原则。

使用 Azure CLI 分配角色的示例:

$subscriptionId = "<subscription-id>"
$appId = "<application-client-id>"

az role assignment create `
  --assignee $appId `
  --role "Billing Reader" `
  --scope "/subscriptions/$subscriptionId"

如果使用服务主体的对象 ID,可以明确指定主体类型:

$servicePrincipalObjectId = "<service-principal-object-id>"

az role assignment create `
  --assignee-object-id $servicePrincipalObjectId `
  --assignee-principal-type ServicePrincipal `
  --role "Billing Reader" `
  --scope "/subscriptions/$subscriptionId"

应用注册页面中的应用对象 ID,与企业应用对应的服务主体对象 ID不是同一个对象。Azure RBAC 角色实际授予的是租户中的服务主体。

PowerShell 调用示例

建议为 Azure Resource Manager 获取 Token。以下示例使用 Microsoft Entra ID v2 Token 端点:

$tenantId       = "<tenant-id>"
$clientId       = "<application-client-id>"
$clientSecret   = "<client-secret>"
$subscriptionId = "<subscription-id>"

$tokenUri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"

$tokenBody = @{
    client_id     = $clientId
    client_secret = $clientSecret
    scope         = "https://management.azure.com/.default"
    grant_type    = "client_credentials"
}

$tokenResponse = Invoke-RestMethod `
    -Method Post `
    -Uri $tokenUri `
    -Body $tokenBody `
    -ContentType "application/x-www-form-urlencoded"

$headers = @{
    Authorization = "Bearer $($tokenResponse.access_token)"
}

$usageUri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Consumption/usageDetails?api-version=2023-05-01"

$result = Invoke-RestMethod `
    -Method Get `
    -Uri $usageUri `
    -Headers $headers

$result.value

如果必须继续使用 v1 Token 端点,resource 应指向 Azure Resource Manager:

$tokenUri = "https://login.microsoftonline.com/$tenantId/oauth2/token"

$tokenBody = @{
    client_id     = $clientId
    client_secret = $clientSecret
    resource      = "https://management.azure.com/"
    grant_type    = "client_credentials"
}

原示例中的字段名:

client_secrect

拼写有误,正确名称是:

client_secret

如果实际使用了错误字段,通常无法成功取得有效 Token。当前既然已经成功取得 Token,实际代码中可能没有这个拼写问题。

仍然报错时的检查项

  • 确认角色分配位于请求所用的同一个订阅,而不是其他订阅。
  • 确认 Token 来自该订阅所属的租户。
  • 确认角色分配给了“企业应用”对应的服务主体,而不是只修改了应用注册中的 API permissions。
  • Azure Resource Manager API 主要使用 Azure RBAC。仅在“API 权限”中添加 Microsoft Graph 权限,无法解决这个错误。
  • 角色分配可能需要一点时间才能生效。分配完成后应重新获取 Token,不要复用旧 Token。
  • 确认请求中的 $subscriptionId、计费范围和账户类型无误。Enterprise Agreement、Microsoft Customer Agreement、合作伙伴账户等计费模型支持的范围和接口可能不同。
  • billingPeriods/{billingPeriod} 是较早的调用形式。新实现应优先按照当前 Consumption Usage Details 文档使用订阅级端点,并通过查询参数筛选日期。
  • 如果订阅尚未注册相关资源提供程序,可以检查 Microsoft.Consumption 的注册状态。不过,当前错误已经明确指向 RBAC 授权,应先处理角色分配。

简单来说,client_id + client_secret 用于认证,订阅范围内的 Azure RBAC 角色用于授权。更换另一种客户端密钥不会自动解决权限错误。

备注:内容仅供参考。