教程:保护外部租户中注册的 ASP.NET Core Web API
本教程系列演示如何保护在外部租户中注册的 Web API。 在本教程中,你将生成一个 ASP.NET Core Web API,用于发布委托的权限(作用域)和应用程序权限(应用角色)。
在本教程中;
- 配置 Web API 以使用它的应用注册详细信息
- 将 Web API 配置为使用在应用注册中注册的委托权限和应用程序权限
- 保护 Web API 终结点
先决条件
公开至少一个范围(委托权限)和一个应用角色(应用程序权限)(例如 ToDoList.Read)的 API 注册。 如果尚未注册,请按照注册步骤在 Microsoft Entra 管理中心注册 API。 确保具有以下项:
- Web API 的应用程序(客户端)ID
- 已注册 Web API 的目录(租户)ID
- 注册 Web API 的目录(租户)子域。 例如,如果主域为 contoso.onmicrosoft.com,则目录(租户)子域为 contoso。
- 作为由 Web API 公开的委托权限(作用域)的 ToDoList.Read 和 ToDoList.ReadWrite。
- 作为由 Web API 公开的应用程序权限(应用角色)的 ToDoList.Read.All 和 ToDoList.ReadWrite.All。
.NET 7.0 SDK 或更高版本。
Visual Studio Code 或其他代码编辑器。
创建 ASP.NET Core Web API
打开终端,然后导航到希望存放项目的文件夹。
运行以下命令:
dotnet new webapi -o ToDoListAPI cd ToDoListAPI
当对话框询问是否要将所需资产添加到项目时,选择“是”。
安装包
安装以下包:
Microsoft.EntityFrameworkCore.InMemory
允许将 Entity Framework Core 和内存数据库一起使用。 它不适合用于生产环境。Microsoft.Identity.Web
可简化向与 Microsoft 标识平台集成的 Web 应用和 Web API 添加身份验证和授权支持的过程。
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web
配置应用注册详细信息
打开应用文件夹中的 appsettings.json 文件,并在注册 Web API 后添加到所记录的应用注册详细信息中。
{
"AzureAd": {
"Instance": "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/",
"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_Tenant_Subdomain_Here
替换为目录(租户)子域。
使用自定义 URL 域(可选)
使用自定义域可完全标记身份验证 URL。 从用户的角度来看,用户在身份验证过程中仍留在你的域中,而不是重定向到 ciamlogin.com 域名。
请按照以下步骤使用自定义域:
使用为外部租户中的应用启用自定义 URL 域中的步骤为外部租户启用自定义 URL 域。
打开 appsettings.json 文件:
- 将
Instance
属性的值更新为 https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here。 将Enter_the_Custom_Domain_Here
替换为你的自定义 URL 域,并将Enter_the_Tenant_ID_Here
替换为你的租户 ID。 如果没有租户 ID,请了解如何读取租户详细信息。 - 添加值为 [Enter_the_Custom_Domain_Here] 的
knownAuthorities
属性。
- 将
对 appsettings.json 文件进行更改后,如果自定义 URL 域为 login.contoso.com 且租户 ID 为 aaaabbbb-0000-cccc-1111-dddd2222eeee,则文件应类似于以下代码片段:
{
"AzureAd": {
"Instance": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
"TenantId": "Enter_the_Tenant_Id_Here",
"ClientId": "Enter_the_Application_Id_Here",
"KnownAuthorities": ["login.contoso.com"]
},
"Logging": {...},
"AllowedHosts": "*"
}
添加应用角色和范围
所有 API 必须至少发布一个范围(也称为委托的权限),以便客户端应用成功获取用户的访问令牌。 API 还应为应用程序至少发布一个应用角色(也称为应用程序权限),以便客户端应用以自己的身份获取访问令牌(即在它们未登录用户时)。
我们会在 appsettings.json 文件中指定这些权限。 在本教程中,我们注册了四个权限。 ToDoList.ReadWrite、ToDoList.Read、ToDoList.ReadWrite.All 和 ToDoList.Read.All,前两个为委托的权限,后两个为应用程序权限。
{
"AzureAd": {
"Instance": "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/",
"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": "*"
}
添加身份验证方案
身份验证方案是在身份验证期间配置身份验证服务时命名的。 在本文中,我们使用 JWT 持有者身份验证方案。 在 Programs.cs 文件中添加以下代码以添加身份验证方案。
// Add the following to your imports
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
// Add authentication scheme
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration);
创建模型
在项目的根文件夹中创建名为“Models”的文件夹。 导航到该文件夹并创建名为 ToDo.cs 的文件,然后添加以下代码。 此代码创建一个名为 ToDo 的模型。
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;
}
添加数据库上下文
数据库上下文是为数据模型协调 Entity Framework 功能的主类。 此类可通过从 Microsoft.EntityFrameworkCore.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 文件,然后在文件中添加以下代码。 此代码会将名为
ToDoContext
的DbContext
子类注册为 ASP.NET Core 应用程序服务提供程序(也称为依赖项注入容器)中已限定范围的服务。 配置上下文,以使用内存中数据库。// Add the following to your imports using ToDoListAPI.Context; using Microsoft.EntityFrameworkCore; builder.Services.AddDbContext<ToDoContext>(opt => opt.UseInMemoryDatabase("ToDos"));
添加控制器
在大多数情况下,控制器可以执行多个操作。 通常包括创建、读取、更新和删除 (CRUD) 操作。 在本教程中,我们仅创建两个操作项。 它们分别是读取所有操作项和创建操作项,用于演示如何保护终结点。
导航到项目根文件夹中的 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 包装器,可帮助我们轻松处理身份验证逻辑,例如,通过处理令牌验证。 为了确保终结点需要授权,我们使用内置的 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); }
运行 API
使用 dotnet run
命令运行 API,确保其运行良好,不会出现任何错误。 如果打算在测试期间使用 HTTPS 协议,则需要信任 .NET 的开发证书。
有关此 API 代码的完整示例,请参阅示例文件。