在本文中,你将使用 Microsoft.Identity.Web 在 ASP.NET Core Web API 中实现授权。 你将验证 范围 (委派权限)和 应用权限 (应用程序权限),以控制对受保护资源的访问。 这些示例使用Microsoft Entra ID作为标识提供者。
了解授权概念
本部分介绍了身份验证和授权之间的主要区别,并描述了Microsoft.Identity.Web在访问令牌中的验证内容。
身份验证与授权
| 概念 | Purpose | 结果 |
|---|---|---|
| 身份验证 | 验证标识 | 401 如果失败,则未经授权 |
| 授权 | 验证权限 | 403 如果不足,则禁止访问 |
什么内容进行了验证?
当 Web API 收到访问令牌时,Microsoft。Identity.Web 验证:
- 令牌签名 - 是否来自受信任的颁发机构?
- 令牌受众 - 它是否适用于此 API?
- 令牌过期 - 是否仍然有效?
- 权限范围/角色 - 客户端应用和主体(用户)是否具有正确的权限?
本指南重点介绍 #4 - 验证范围和应用权限。
范围(委派权限)
当用户委托应用代表其执行操作的权限时(例如,代表登录用户调用的 Web API)时,范围适用。
| 详细信息 | 价值 |
|---|---|
| 令牌声明 |
scp 或 scope (客户端应用); roles (用户) |
| 示例值 |
"access_as_user"、"User.Read"、"Files.ReadWrite" |
应用权限(应用程序权限)
当应用在没有用户上下文的情况下(例如使用客户端凭据的守护程序或后台服务)调用 Web API 本身时,应用权限适用。
| 详细信息 | 价值 |
|---|---|
| 令牌声明 | roles |
| 示例值 |
"Mail.Read.All"、"User.Read.All" |
使用 RequiredScope 验证作用域
该 RequiredScope 属性检查访问令牌是否包含至少一个指定的作用域。 当 API 仅提供用户委托的请求时,请使用此属性。
设置范围验证
按照以下步骤在 API 中启用范围验证。
1.在 API 中启用授权:
将身份验证和授权服务添加到应用程序管道:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization(); // Required for authorization
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization(); // Must be after UseAuthentication
app.MapControllers();
app.Run();
2.保护控制器或操作:
将 [Authorize] 和 [RequiredScope] 属性应用于控制器或单个操作:
using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web.Resource;
[Authorize]
[RequiredScope("access_as_user")]
public class TodoListController : ControllerBase
{
[HttpGet]
public IActionResult GetTodos()
{
// Only accessible if token has "access_as_user" scope
return Ok(new[] { "Todo 1", "Todo 2" });
}
}
应用范围模式
选择最适合在应用程序中管理作用域的模式。
模式 1:硬编码的范围
在开发阶段范围已固定且已知时,请使用此模式。
[Authorize]
[RequiredScope("access_as_user")]
public class TodoListController : ControllerBase
{
// All actions require "access_as_user" scope
}
若要接受多个范围中的任何一个,请将其列为参数:
[Authorize]
[RequiredScope("read", "write", "admin")]
public class TodoListController : ControllerBase
{
// Token must have "read" OR "write" OR "admin"
}
模式 2:从配置中获取范围
当需要为每个环境单独配置范围时,请使用此模式。 在配置文件中定义范围:
appsettings.json:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-api-client-id",
"Scopes": "access_as_user read write"
}
}
引用控制器中的配置密钥:
[Authorize]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class TodoListController : ControllerBase
{
// Scopes read from configuration
}
此方法允许在不重新编译的情况下更改范围。
模式 3:操作级作用域
当不同的操作需要不同的权限时,请使用此模式。 将 [RequiredScope] 应用于该单个操作方法:
[Authorize]
public class TodoListController : ControllerBase
{
[HttpGet]
[RequiredScope("read")]
public IActionResult GetTodos()
{
return Ok(todos);
}
[HttpPost]
[RequiredScope("write")]
public IActionResult CreateTodo([FromBody] Todo todo)
{
// Only tokens with "write" scope can create
return CreatedAtAction(nameof(GetTodos), todo);
}
[HttpDelete("{id}")]
[RequiredScope("admin")]
public IActionResult DeleteTodo(int id)
{
// Only tokens with "admin" scope can delete
return NoContent();
}
}
了解验证流
当请求到达时,中间件按以下顺序处理它:
- ASP.NET Core身份验证中间件验证令牌
-
RequiredScope属性检查是否为scp或scope声明 - 如果令牌至少包含一个匹配范围,则请求继续。
- 如果未找到匹配的范围,API 将返回 403 禁止响应。
以下示例显示了典型的错误响应:
{
"error": "insufficient_scope",
"error_description": "The token does not have the required scope 'access_as_user'."
}
使用 RequiredScopeOrAppPermission 验证应用权限
该 RequiredScopeOrAppPermission 属性验证 范围 (委托)或 应用权限 (应用程序)。 当 API 从同一终结点为用户委托的应用和守护程序/服务应用提供服务时,请使用此属性。
如果 API 仅提供用户委托的请求,请改用 RequiredScope 。
设置范围或应用权限验证
应用属性以接受任意令牌类型:
using Microsoft.Identity.Web.Resource;
[Authorize]
[RequiredScopeOrAppPermission(
AcceptedScope = new[] { "access_as_user" },
AcceptedAppPermission = new[] { "TodoList.ReadWrite.All" }
)]
public class TodoListController : ControllerBase
{
[HttpGet]
public IActionResult GetTodos()
{
// Accessible with EITHER:
// - User-delegated token with "access_as_user" scope, OR
// - App-only token with "TodoList.ReadWrite.All" app permission
return Ok(todos);
}
}
从设置配置应用权限
在配置中存储范围和应用权限,以在不重新编译的情况下更改它们。
appsettings.json:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-api-client-id",
"Scopes": "access_as_user",
"AppPermissions": "TodoList.ReadWrite.All TodoList.Admin"
}
}
引用控制器中的配置密钥:
[Authorize]
[RequiredScopeOrAppPermission(
RequiredScopesConfigurationKey = "AzureAd:Scopes",
RequiredAppPermissionsConfigurationKey = "AzureAd:AppPermissions"
)]
public class TodoListController : ControllerBase
{
// Scopes and app permissions from configuration
}
比较令牌声明差异
下表显示了声明在用户委托令牌和仅限应用令牌之间有何差异:
| 令牌类型 | 索赔 | 示例值 |
|---|---|---|
| 用户委托 |
scp 或 scope |
"access_as_user User.Read" |
| 仅限应用 | roles |
["TodoList.ReadWrite.All"] |
以下示例显示了用户委派的令牌:
{
"aud": "api://your-api-client-id",
"iss": "https://login.microsoftonline.com/.../v2.0",
"scp": "access_as_user",
"sub": "user-object-id",
...
}
以下示例显示了仅限应用的令牌:
{
"aud": "api://your-api-client-id",
"iss": "https://login.microsoftonline.com/.../v2.0",
"roles": ["TodoList.ReadWrite.All"],
"sub": "app-object-id",
...
}
创建授权策略
对于复杂的授权方案,请使用 ASP.NET Core授权策略。 策略使你可以集中规则,组合多个要求,并编写可测试的授权逻辑。
| 益处 | Description |
|---|---|
| 集中式逻辑 | 定义授权规则一次,在任何地方重复使用 |
| 可组合 | 合并多个要求(范围 + 声明 + 自定义逻辑) |
| 可测试 | 更易于单元测试授权逻辑 |
| 灵活 | 范围验证之外的自定义要求 |
模式 1:使用 RequireScope 定义策略
定义需要特定作用域的命名策略,然后在控制器上引用它们:
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("TodoReadPolicy", policyBuilder =>
{
policyBuilder.RequireScope("read", "access_as_user");
});
options.AddPolicy("TodoWritePolicy", policyBuilder =>
{
policyBuilder.RequireScope("write", "admin");
});
});
var app = builder.Build();
将策略应用于控制器操作:
[Authorize]
public class TodoListController : ControllerBase
{
[HttpGet]
[Authorize(Policy = "TodoReadPolicy")]
public IActionResult GetTodos()
{
return Ok(todos);
}
[HttpPost]
[Authorize(Policy = "TodoWritePolicy")]
public IActionResult CreateTodo([FromBody] Todo todo)
{
return CreatedAtAction(nameof(GetTodos), todo);
}
}
模式 2:使用 ScopeAuthorizationRequirement 定义策略
请使用 ScopeAuthorizationRequirement 来更明确地定义范围要求。
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CustomPolicy", policyBuilder =>
{
policyBuilder.AddRequirements(
new ScopeAuthorizationRequirement(new[] { "access_as_user" })
);
});
});
模式 3:设置默认策略
设置自动应用于所有 [Authorize] 属性的默认策略:
builder.Services.AddAuthorization(options =>
{
var defaultPolicy = new AuthorizationPolicyBuilder()
.RequireScope("access_as_user")
.Build();
options.DefaultPolicy = defaultPolicy;
});
每个 [Authorize] 属性现在都需要范围 access_as_user :
[Authorize] // Automatically requires "access_as_user" scope
public class TodoListController : ControllerBase
{
// All actions protected by default policy
}
模式 4:合并多个要求
在单个策略中合并范围、角色和身份验证要求:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminPolicy", policyBuilder =>
{
policyBuilder.RequireScope("admin");
policyBuilder.RequireRole("Admin"); // Also check role claim
policyBuilder.RequireAuthenticatedUser();
});
});
模式 5:从配置生成策略
从配置加载范围以保持策略环境特定:
var requiredScopes = builder.Configuration["AzureAd:Scopes"]?.Split(' ');
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiAccessPolicy", policyBuilder =>
{
if (requiredScopes != null)
{
policyBuilder.RequireScope(requiredScopes);
}
});
});
按租户筛选请求
限制对来自特定Microsoft Entra租户的令牌的 API 访问。 当多租户 API 应仅接受来自已批准的客户租户的请求时,这非常有用。
限制对允许租户的访问
定义一个策略,用于根据允许列表检查租户 ID 声明:
builder.Services.AddAuthorization(options =>
{
string[] allowedTenants =
{
"14c2f153-90a7-4689-9db7-9543bf084dad", // Contoso tenant
"af8cc1a0-d2aa-4ca7-b829-00d361edb652", // Fabrikam tenant
"979f4440-75dc-4664-b2e1-2cafa0ac67d1" // Northwind tenant
};
options.AddPolicy("AllowedTenantsOnly", policyBuilder =>
{
policyBuilder.RequireClaim(
"http://schemas.microsoft.com/identity/claims/tenantid",
allowedTenants
);
});
// Apply to all endpoints by default
options.DefaultPolicy = options.GetPolicy("AllowedTenantsOnly");
});
在设置中配置租户筛选
在配置中存储允许的租户 ID 来管理它们,而无需更改代码。
appsettings.json:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "your-api-client-id",
"AllowedTenants": [
"14c2f153-90a7-4689-9db7-9543bf084dad",
"af8cc1a0-d2aa-4ca7-b829-00d361edb652"
]
}
}
读取租户列表并在启动时创建策略:
var allowedTenants = builder.Configuration.GetSection("AzureAd:AllowedTenants")
.Get<string[]>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AllowedTenantsOnly", policyBuilder =>
{
policyBuilder.RequireClaim(
"http://schemas.microsoft.com/identity/claims/tenantid",
allowedTenants ?? Array.Empty<string>()
);
});
});
将范围与租户筛选相结合
创建需要有效范围和已批准的租户的策略:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SecureApiAccess", policyBuilder =>
{
// Require specific scope
policyBuilder.RequireScope("access_as_user");
// AND require specific tenant
policyBuilder.RequireClaim(
"http://schemas.microsoft.com/identity/claims/tenantid",
allowedTenants
);
});
});
遵循最佳做法
应用这些建议以生成安全、可维护的授权逻辑。
可执行的操作
1. 始终将 [Authorize] 与范围验证配合使用:
[Authorize] // Authentication
[RequiredScope("access_as_user")] // Authorization
public class MyController : ControllerBase { }
2.对特定于环境的作用域使用配置:
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
3.应用最低权限:
[HttpGet]
[RequiredScope("read")] // Only read permission needed
[HttpPost]
[RequiredScope("write")] // Write permission for modifications
4.将策略用于复杂的授权:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
{
policy.RequireScope("admin");
policy.RequireClaim("department", "IT");
});
});
5. 在开发中启用详细的错误响应:
if (builder.Environment.IsDevelopment())
{
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
}
禁忌事项
1. 使用[Authorize]时不要跳过RequiredScope:
// Wrong - RequiredScope won't work without [Authorize]
[RequiredScope("access_as_user")]
public class MyController : ControllerBase { }
// Correct
[Authorize]
[RequiredScope("access_as_user")]
public class MyController : ControllerBase { }
2. 不要在生产环境中硬编码租户 ID:
// Wrong
policyBuilder.RequireClaim("tid", "14c2f153-90a7-4689-9db7-9543bf084dad");
// Better - use configuration
var tenants = Configuration.GetSection("AllowedTenants").Get<string[]>();
policyBuilder.RequireClaim("tid", tenants);
3.不要将范围与角色混淆:
// Wrong - This checks roles claim, not scopes
[RequiredScope("Admin")] // "Admin" is typically a role, not a scope
// Correct
[RequiredScope("access_as_user")] // Scope
[Authorize(Roles = "Admin")] // Role
4.不要在生产错误消息中公开敏感范围信息:
为生产环境配置适当的日志记录级别和错误处理。
排查授权问题
使用以下指南诊断常见的授权问题。
403 禁止访问 - 缺少范围
错误: 即使使用有效的令牌,API 也会返回 403。
诊断:
- 在 jwt.ms 解码令牌。
- 检查
scp或scope声明。 - 验证值是否与属性
RequiredScope匹配。
Solution:
- 确保客户端应用程序在获取令牌时请求正确的范围。
- 验证范围是否在 Microsoft Entra 的 API 应用程序注册中公开。
- 根据需要授予管理员同意。
RequiredScope 不起作用
症状: 该属性似乎将被忽略。
检查:
- 是否添加了该
[Authorize]属性? -
app.UseAuthorization()是在app.UseAuthentication()之后被调用的吗? - 是否已
services.AddAuthorization()注册?
找不到配置密钥
错误: 作用域验证以无提示方式失败。
检查:
{
"AzureAd": {
"Scopes": "access_as_user" // Matches RequiredScopesConfigurationKey
}
}
确保配置路径完全匹配。