本教程系列演示如何使用 Microsoft 标识平台保护 ASP.NET Core Web API,以限制其仅对授权用户和客户端应用的访问权限。 生成的 Web API 使用委派的权限(范围)和应用程序权限(应用角色)。
在本教程中,你将:
- 生成 ASP.NET 核心 Web API
- 配置 Web API 以使用其 Microsoft Entra 应用注册信息
- 保护 Web API 端点
- 运行 Web API,确保它侦听 HTTP 请求
先决条件
- 完成快速入门:调用受 Microsoft 标识平台保护的 Web API 中的步骤(如果尚未完成)。 无需克隆并运行代码示例,但请确保具有以下各项:
- 来自 Microsoft Entra 管理中心的 Web API 应用注册详细信息,包括客户端 ID 和租户 ID。
- ToDoList.Read 和 ToDoList.ReadWrite 作为 Web API 公开的委托权限(范围)
- ToDoList.Read.All 和 ToDoList.ReadWrite.All 作为 Web API 公开的应用程序权限(应用角色)
- .NET 8.0 SDK 或更高版本。
- Visual Studio Code 或其他代码编辑器。
创建新的 ASP.NET Core Web API 项目
若要创建最小 ASP.NET 核心 Web API 项目,请执行以下步骤:
在 Visual Studio Code 或任何其他代码编辑器上打开终端,并导航到要在其中创建项目的目录。
在 .NET CLI 或任何其他命令行工具上运行以下命令。
dotnet new web -o TodoListApi cd TodoListApi
当对话框询问是否要信任作者时,请选择 “是 ”。
当对话框询问是否要将所需资产添加到项目时,请选择 “是 ”。
安装所需程序包
若要生成、保护和测试 ASP.NET Core Web API,需要安装以下包:
Microsoft.EntityFrameworkCore.InMemory
- 允许将 Entity Framework Core 与内存中数据库配合使用的包。 它适用于测试目的,但并非专为生产用途而设计。Microsoft.Identity.Web
- 一组 ASP.NET 核心库,用于简化向与Microsoft标识平台集成的 Web 应用和 Web API 添加身份验证和授权支持。
若要安装包,请使用:
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web
配置应用注册详细信息
在应用文件夹中打开 appsettings.json 文件,并添加注册 Web API 后记录的应用注册详细信息。
{
"AzureAd": {
"Instance": "Enter_the_Authority_URL_Here",
"TenantId": "Enter_the_Tenant_Id_Here",
"ClientId": "Enter_the_Application_Id_Here"
},
"Logging": {...},
"AllowedHosts": "*"
}
如下所示,替换以下占位符:
- 将
Enter_the_Application_Id_Here
替换为应用程序(客户端)ID。 - 将
Enter_the_Tenant_Id_Here
替换为目录(租户)ID。 - 将
Enter_the_Authority_URL_Here
替换为你的颁发机构 URL,具体说明请参见下一部分。
应用的授权 URL
颁发机构 URL 指定 Microsoft 身份验证库 (MSAL) 可从中请求令牌的目录。
//Instance for workforce tenant
Instance: "https://login.partner.microsoftonline.cn/"
使用自定义 URL 域(可选)
员工租户不支持自定义 URL 域。
添加权限
所有 API 必须至少发布一个范围(也称为委托的权限),以便客户端应用成功获取用户的访问令牌。 API 还应发布至少一个应用角色(也称为应用程序权限),使客户端应用能够自行获取访问令牌,也就是说,当他们未登录用户时。
我们会在 appsettings.json 文件中指定这些权限。 在本教程中,你注册了以下委派权限和应用程序权限:
- 委托的权限:ToDoList.Read 和 ToDoList.ReadWrite。
- 应用程序权限:ToDoList.Read.All 和 ToDoList.ReadWrite.All。
当用户或客户端应用程序调用 Web API 时,只有具有这些作用域或权限的客户端才有权访问受保护的终结点。
{
"AzureAd": {
"Instance": "Enter_the_Authority_URL_Here",
"TenantId": "Enter_the_Tenant_Id_Here",
"ClientId": "Enter_the_Application_Id_Here",
"Scopes": {
"Read": ["ToDoList.Read", "ToDoList.ReadWrite"],
"Write": ["ToDoList.ReadWrite"]
},
"AppPermissions": {
"Read": ["ToDoList.Read.All", "ToDoList.ReadWrite.All"],
"Write": ["ToDoList.ReadWrite.All"]
}
},
"Logging": {...},
"AllowedHosts": "*"
}
在 API 中实现身份验证和授权
若要配置身份验证和授权,请打开 program.cs
该文件并将其内容替换为以下代码片段:
添加身份验证方案
在此 API 中,我们将 JSON Web 令牌 (JWT) 持有者方案用作默认身份验证机制。 使用AddAuthentication
方法注册 JWT 凭证持有者方案。
// Add required packages to your imports
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Add an authentication scheme
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration);
创建应用的模型
在项目的根文件夹中,创建名为 Models 的文件夹。 导航到 Models 文件夹并创建一个名为 ToDo.cs
然后添加以下代码的文件。
using System;
namespace ToDoListAPI.Models;
public class ToDo
{
public int Id { get; set; }
public Guid Owner { get; set; }
public string Description { get; set; } = string.Empty;
}
前面的代码创建名为 ToDo 的模型。 此模型表示应用管理的数据。
添加数据库上下文
接下来,我们定义一个数据库上下文类,该类协调数据模型的 Entity Framework 功能。 此类继承自 Microsoft.EntityFrameworkCore.DbContext 类,该类负责管理应用程序与数据库之间的交互。 若要添加数据库上下文,请执行以下步骤:
在项目的根文件夹中创建名为 DbContext 的文件夹。
导航到 DbContext 文件夹并创建一个名为
ToDoContext.cs
然后添加以下代码的文件:using Microsoft.EntityFrameworkCore; using ToDoListAPI.Models; namespace ToDoListAPI.Context; public class ToDoContext : DbContext { public ToDoContext(DbContextOptions<ToDoContext> options) : base(options) { } public DbSet<ToDo> ToDos { get; set; } }
在项目的根文件夹中打开 Program.cs 文件,并使用以下代码对其进行更新:
// Add the following to your imports using ToDoListAPI.Context; using Microsoft.EntityFrameworkCore; //Register ToDoContext as a service in the application builder.Services.AddDbContext<ToDoContext>(opt => opt.UseInMemoryDatabase("ToDos"));
在前面的代码片段中,我们在 ASP.NET Core 应用程序服务提供程序(也称为依赖项注入容器)中将 DB 上下文注册为作用域服务。 还可以将 ToDoContext
类配置为使用 ToDo 列表 API 的内存中数据库。
设置控制器
控制器通常实现创建、读取、更新和删除(CRUD)作来管理资源。 由于本教程更侧重于保护 API 终结点,因此我们只在控制器中实现两个作项。 进行“全部读取”操作以检索所有待办事项,并进行“创建”操作以添加新的待办事项。 按照以下步骤将控制器添加到项目:
导航到项目的根文件夹,并创建名为 Controllers 的文件夹。
在
ToDoListController.cs
文件夹中创建一个名为的文件,并添加以下锅炉板代码:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;
using ToDoListAPI.Models;
using ToDoListAPI.Context;
namespace ToDoListAPI.Controllers;
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ToDoListController : ControllerBase
{
private readonly ToDoContext _toDoContext;
public ToDoListController(ToDoContext toDoContext)
{
_toDoContext = toDoContext;
}
[HttpGet()]
[RequiredScopeOrAppPermission()]
public async Task<IActionResult> GetAsync(){...}
[HttpPost]
[RequiredScopeOrAppPermission()]
public async Task<IActionResult> PostAsync([FromBody] ToDo toDo){...}
private bool RequestCanAccessToDo(Guid userId){...}
private Guid GetUserId(){...}
private bool IsAppMakingRequest(){...}
}
将代码添加到控制器
本部分介绍如何将代码添加到上一部分中搭建的控制器基架。 此处的重点是保护 API,而不是生成 API。
导入必要的包: 包
Microsoft.Identity.Web
是 MSAL.NET 的包装器,可帮助我们轻松处理身份验证逻辑,例如处理令牌验证。 为了确保我们的端点需要授权,我们使用内置Microsoft.AspNetCore.Authorization
包。由于我们授予的权限,允许此 API 使用代表用户的委托的权限,或使用客户端自身调用的应用程序权限(而不是代表用户的应用程序权限)进行调用,因此重要的是需要确认调用是否由应用本身触发。 执行此作的最简单方法是查找访问令牌是否包含
idtyp
可选声明。 此idtyp
声明是 API 确定令牌是应用令牌还是应用 + 用户令牌的最简便方法。 我们建议启用idtyp
可选声明。如果未启用声明
idtyp
,则可以使用roles
和scp
声明来确定访问令牌是应用令牌还是应用 + 用户令牌。 由 Microsoft Entra ID 颁发的访问令牌至少包含两个声明中的一个。 颁发给用户的访问令牌具有scp
声明。 颁发给应用程序的访问令牌具有roles
声明。 同时包含两个声明的访问令牌仅颁发给用户,其中scp
声明用于指定委托的权限,而roles
声明用于指定用户的角色。 两个声明均未包含的访问令牌将不会得到遵循。private bool IsAppMakingRequest() { if (HttpContext.User.Claims.Any(c => c.Type == "idtyp")) { return HttpContext.User.Claims.Any(c => c.Type == "idtyp" && c.Value == "app"); } else { return HttpContext.User.Claims.Any(c => c.Type == "roles") && !HttpContext.User.Claims.Any(c => c.Type == "scp"); } }
添加一个帮助程序函数,用于确定所发出的请求是否包含执行预期操作所需的足够权限。 请检查应用是代表自己发出请求,还是代表拥有给定资源的用户通过验证用户 ID 来执行调用。
private bool RequestCanAccessToDo(Guid userId) { return IsAppMakingRequest() || (userId == GetUserId()); } private Guid GetUserId() { Guid userId; if (!Guid.TryParse(HttpContext.User.GetObjectId(), out userId)) { throw new Exception("User ID is not valid."); } return userId; }
插入权限定义以保护路由。 通过将
[Authorize]
属性添加到控制器类来保护 API。 此行为可确保仅当使用已授权的标识调用 API 时,才能调用控制器操作。 权限定义定义执行这些操作所需的权限类型。[Authorize] [Route("api/[controller]")] [ApiController] public class ToDoListController: ControllerBase{...}
向 GET 和 POST 终结点添加权限。 为此,请使用属于 Microsoft.Identity.Web.Resource 命名空间的 RequiredScopeOrAppPermission 方法。 然后,通过 RequiredScopesConfigurationKey 和 RequiredAppPermissionsConfigurationKey 属性将范围和权限传递给此方法。
[HttpGet] [RequiredScopeOrAppPermission( RequiredScopesConfigurationKey = "AzureAD:Scopes:Read", RequiredAppPermissionsConfigurationKey = "AzureAD:AppPermissions:Read" )] public async Task<IActionResult> GetAsync() { var toDos = await _toDoContext.ToDos! .Where(td => RequestCanAccessToDo(td.Owner)) .ToListAsync(); return Ok(toDos); } [HttpPost] [RequiredScopeOrAppPermission( RequiredScopesConfigurationKey = "AzureAD:Scopes:Write", RequiredAppPermissionsConfigurationKey = "AzureAD:AppPermissions:Write" )] public async Task<IActionResult> PostAsync([FromBody] ToDo toDo) { // Only let applications with global to-do access set the user ID or to-do's var ownerIdOfTodo = IsAppMakingRequest() ? toDo.Owner : GetUserId(); var newToDo = new ToDo() { Owner = ownerIdOfTodo, Description = toDo.Description }; await _toDoContext.ToDos!.AddAsync(newToDo); await _toDoContext.SaveChangesAsync(); return Created($"/todo/{newToDo!.Id}", newToDo); }
将应用程序接口中介软件配置为使用控制器
接下来,我们将应用程序配置为识别和使用控制器来处理 HTTP 请求。 program.cs
打开该文件并添加以下代码,以在依赖项注入容器中注册控制器服务。
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
在前面的代码片段中, AddControllers()
该方法通过注册必要的服务来准备应用程序以使用控制器,同时 MapControllers()
映射控制器路由来处理传入的 HTTP 请求。
运行 API
使用 dotnet run
命令运行 API,以确保其正常运行而不会出现任何错误。 如果要即使在测试期间也使用 HTTPS 协议,则需要 信任。NET 的开发证书。
通过在终端中键入以下命令启动应用程序:
dotnet run
应在终端中显示类似于以下内容的输出,该输出确认应用程序正在运行
http://localhost:{port}
并侦听请求。Building... info: Microsoft.Hosting.Lifetime[0] Now listening on: http://localhost:{port} info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. ...
网页 http://localhost:{host}
显示类似于下图的输出。 这是因为在没有身份验证的情况下调用了 API。 若要进行授权调用,请参阅 后续步骤 ,了解如何访问受保护的 Web API。
有关此 API 代码的完整示例,请参阅示例文件。