教程:使用 Microsoft 标识平台生成和保护 ASP.NET Core Web API

本教程系列演示如何使用 Microsoft 标识平台保护 ASP.NET Core Web API,以限制其仅对授权用户和客户端应用的访问权限。 生成的 Web API 使用委派的权限(范围)和应用程序权限(应用角色)。

在本教程中,你将:

  • 生成 ASP.NET 核心 Web API
  • 配置 Web API 以使用其 Microsoft Entra 应用注册信息
  • 保护 Web API 端点
  • 运行 Web API,确保它侦听 HTTP 请求

先决条件

创建新的 ASP.NET Core Web API 项目

若要创建最小 ASP.NET 核心 Web API 项目,请执行以下步骤:

  1. 在 Visual Studio Code 或任何其他代码编辑器上打开终端,并导航到要在其中创建项目的目录。

  2. 在 .NET CLI 或任何其他命令行工具上运行以下命令。

    dotnet new web -o TodoListApi
    cd TodoListApi
    
  3. 当对话框询问是否要信任作者时,请选择 “是 ”。

  4. 当对话框询问是否要将所需资产添加到项目时,请选择 “是 ”。

安装所需程序包

若要生成、保护和测试 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.ReadToDoList.ReadWrite
  • 应用程序权限:ToDoList.Read.AllToDoList.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 类,该类负责管理应用程序与数据库之间的交互。 若要添加数据库上下文,请执行以下步骤:

  1. 在项目的根文件夹中创建名为 DbContext 的文件夹。

  2. 导航到 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; }
    }
    
  3. 在项目的根文件夹中打开 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 终结点,因此我们只在控制器中实现两个作项。 进行“全部读取”操作以检索所有待办事项,并进行“创建”操作以添加新的待办事项。 按照以下步骤将控制器添加到项目:

  1. 导航到项目的根文件夹,并创建名为 Controllers 的文件夹。

  2. 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。

  1. 导入必要的包:Microsoft.Identity.Web 是 MSAL.NET 的包装器,可帮助我们轻松处理身份验证逻辑,例如处理令牌验证。 为了确保我们的端点需要授权,我们使用内置 Microsoft.AspNetCore.Authorization 包。

  2. 由于我们授予的权限,允许此 API 使用代表用户的委托的权限,或使用客户端自身调用的应用程序权限(而不是代表用户的应用程序权限)进行调用,因此重要的是需要确认调用是否由应用本身触发。 执行此作的最简单方法是查找访问令牌是否包含 idtyp 可选声明。 此 idtyp 声明是 API 确定令牌是应用令牌还是应用 + 用户令牌的最简便方法。 我们建议启用 idtyp 可选声明。

    如果未启用声明 idtyp,则可以使用 rolesscp 声明来确定访问令牌是应用令牌还是应用 + 用户令牌。 由 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");
        }
    }
    
  3. 添加一个帮助程序函数,用于确定所发出的请求是否包含执行预期操作所需的足够权限。 请检查应用是代表自己发出请求,还是代表拥有给定资源的用户通过验证用户 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;
        }
    
  4. 插入权限定义以保护路由。 通过将 [Authorize] 属性添加到控制器类来保护 API。 此行为可确保仅当使用已授权的标识调用 API 时,才能调用控制器操作。 权限定义定义执行这些操作所需的权限类型。

    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class ToDoListController: ControllerBase{...}
    

    向 GET 和 POST 终结点添加权限。 为此,请使用属于 Microsoft.Identity.Web.Resource 命名空间的 RequiredScopeOrAppPermission 方法。 然后,通过 RequiredScopesConfigurationKeyRequiredAppPermissionsConfigurationKey 属性将范围和权限传递给此方法。

    [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 的开发证书

  1. 通过在终端中键入以下命令启动应用程序:

    dotnet run
    
  2. 应在终端中显示类似于以下内容的输出,该输出确认应用程序正在运行 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。

显示网页启动时出现 401 错误的屏幕截图。

有关此 API 代码的完整示例,请参阅示例文件

后续步骤