PowerShell 调用 Azure Billing REST API 出现授权错误
结论
问题不在 client_secret 的类型。APPkey 本质上也是应用的客户端密钥。既然已经成功取得访问令牌,说明客户端凭据认证基本成功。
缺少的是 Azure RBAC 授权。需要为该应用在当前租户中的服务主体(Service Principal)授予目标订阅或更具体计费范围的读取权限,例如内置的 Billing Reader。客户端密钥只能证明应用身份,不会自动赋予应用访问订阅资源或账单数据的权限。
为什么取得 Token 后仍然被拒绝
Azure 的身份认证和权限验证是两个独立环节:
- Microsoft Entra ID 使用
client_id和client_secret验证应用身份并签发 Token。 - 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 门户中完成角色分配:
- 打开目标订阅。
- 进入“访问控制 (IAM)”。
- 选择“添加角色分配”。
- 选择 Billing Reader。
- 将成员类型设为“用户、组或服务主体”。
- 搜索应用名称或 Application (client) ID,选中对应的服务主体。
- 完成角色分配后,重新获取 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 角色用于授权。更换另一种客户端密钥不会自动解决权限错误。
备注:内容仅供参考。
