使用 Microsoft.Identity.Web 在 Web API 中实现授权。

在本文中,你将使用 Microsoft.Identity.Web 在 ASP.NET Core Web API 中实现授权。 你将验证 范围 (委派权限)和 应用权限 (应用程序权限),以控制对受保护资源的访问。 这些示例使用Microsoft Entra ID作为标识提供者。

了解授权概念

本部分介绍了身份验证和授权之间的主要区别,并描述了Microsoft.Identity.Web在访问令牌中的验证内容。

身份验证与授权

概念 Purpose 结果
身份验证 验证标识 401 如果失败,则未经授权
授权 验证权限 403 如果不足,则禁止访问

什么内容进行了验证?

当 Web API 收到访问令牌时,Microsoft。Identity.Web 验证:

  1. 令牌签名 - 是否来自受信任的颁发机构?
  2. 令牌受众 - 它是否适用于此 API?
  3. 令牌过期 - 是否仍然有效?
  4. 权限范围/角色 - 客户端应用和主体(用户)是否具有正确的权限?

本指南重点介绍 #4 - 验证范围和应用权限

范围(委派权限)

当用户委托应用代表其执行操作的权限时(例如,代表登录用户调用的 Web API)时,范围适用。

详细信息 价值
令牌声明 scpscope (客户端应用); 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();
    }
}

了解验证流

当请求到达时,中间件按以下顺序处理它:

  1. ASP.NET Core身份验证中间件验证令牌
  2. RequiredScope 属性检查是否为 scpscope 声明
  3. 如果令牌至少包含一个匹配范围,则请求继续。
  4. 如果未找到匹配的范围,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
}

比较令牌声明差异

下表显示了声明在用户委托令牌和仅限应用令牌之间有何差异:

令牌类型 索赔 示例值
用户委托 scpscope "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。

诊断:

  1. jwt.ms 解码令牌。
  2. 检查scpscope声明。
  3. 验证值是否与属性 RequiredScope 匹配。

Solution:

  • 确保客户端应用程序在获取令牌时请求正确的范围。
  • 验证范围是否在 Microsoft Entra 的 API 应用程序注册中公开。
  • 根据需要授予管理员同意。

RequiredScope 不起作用

症状: 该属性似乎将被忽略。

检查:

  1. 是否添加了该 [Authorize] 属性?
  2. app.UseAuthorization()是在app.UseAuthentication()之后被调用的吗?
  3. 是否已 services.AddAuthorization() 注册?

找不到配置密钥

错误: 作用域验证以无提示方式失败。

检查:

{
  "AzureAd": {
    "Scopes": "access_as_user" // Matches RequiredScopesConfigurationKey
  }
}

确保配置路径完全匹配。