练习 - 将声明与基于策略的授权结合使用

已完成

在上一单元中,你了解了身份验证和授权之间的区别。 你还了解了策略如何使用声明进行授权。 在本单元中,你使用标识来存储声明并应用策略进行条件访问。

保护“披萨列表”

你收到了一项新要求,即“披萨列表”页应仅对经过身份验证的用户可见。 此外,只有管理员可以创建和删除披萨。 让我们将其锁定。

  1. 在 Pages/Pizza.cshtml.cs 中,应用以下更改:

    1. [Authorize] 特性添加到 PizzaModel 类中。

      [Authorize]
      public class PizzaModel : PageModel
      

      该特性描述了该页的用户身份验证要求。 在此例中,除进行用户身份验证之外没有任何要求。 匿名用户无法查看该页面并将重定向到登录页面。

    2. 通过将以下行添加到文件顶部的 using 指令来解析对 Authorize 的引用:

      using Microsoft.AspNetCore.Authorization;
      
    3. 将以下属性添加到 PizzaModel 类:

      [Authorize]
      public class PizzaModel : PageModel
      {
          public bool IsAdmin => HttpContext.User.HasClaim("IsAdmin", bool.TrueString);
      
          public List<Pizza> pizzas = new();
      

      上述代码确定经过身份验证的用户是否具有值为 TrueIsAdmin 声明。 可以通过名为 IsAdmin 的只读属性访问此评估的结果。

    4. if (!IsAdmin) return Forbid(); 添加到 OnPostOnPostDelete 方法的开头:

      public IActionResult OnPost()
      {
          if (!IsAdmin) return Forbid();
          if (!ModelState.IsValid)
          {
              return Page();
          }
          PizzaService.Add(NewPizza);
          return RedirectToAction("Get");
      }
      
      public IActionResult OnPostDelete(int id)
      {
          if (!IsAdmin) return Forbid();
          PizzaService.Delete(id);
          return RedirectToAction("Get");
      }
      

      在下一步中,你将隐藏非管理员的创建/删除 UI 元素。 这并不能阻止使用 HttpRepl 或 curl 等工具的对手直接访问这些终结点。 添加此检查可确保如果有人尝试此操作,系统将返回一个 HTTP 403 状态代码。

  2. 在 Pages/Pizza.cshtml 中,添加检查以隐藏非管理员的管理员 UI 元素:

    隐藏“新披萨”表单

    <h1>Pizza List 🍕</h1>
    @if (Model.IsAdmin)
    {
    <form method="post" class="card p-3">
        <div class="row">
            <div asp-validation-summary="All"></div>
        </div>
        <div class="form-group mb-0 align-middle">
            <label asp-for="NewPizza.Name">Name</label>
            <input type="text" asp-for="NewPizza.Name" class="mr-5">
            <label asp-for="NewPizza.Size">Size</label>
            <select asp-for="NewPizza.Size" asp-items="Html.GetEnumSelectList<PizzaSize>()" class="mr-5"></select>
            <label asp-for="NewPizza.Price"></label>
            <input asp-for="NewPizza.Price" class="mr-5" />
            <label asp-for="NewPizza.IsGlutenFree">Gluten Free</label>
            <input type="checkbox" asp-for="NewPizza.IsGlutenFree" class="mr-5">
            <button class="btn btn-primary">Add</button>
        </div>
    </form>
    }
    

    隐藏“删除披萨”按钮

    <table class="table mt-5">
        <thead>
            <tr>
                <th scope="col">Name</th>
                <th scope="col">Price</th>
                <th scope="col">Size</th>
                <th scope="col">Gluten Free</th>
                @if (Model.IsAdmin)
                {
                <th scope="col">Delete</th>
                }
            </tr>
        </thead>
        @foreach (var pizza in Model.pizzas)
        {
            <tr>
                <td>@pizza.Name</td>
                <td>@($"{pizza.Price:C}")</td>
                <td>@pizza.Size</td>
                <td>@Model.GlutenFreeText(pizza)</td>
                @if (Model.IsAdmin)
                {
                <td>
                    <form method="post" asp-page-handler="Delete" asp-route-id="@pizza.Id">
                        <button class="btn btn-danger">Delete</button>
                    </form>
                </td>
                }
            </tr>
        }
    </table>
    

    上述更改导致仅当经过身份验证的用户是管理员时才能呈现只有管理员能访问的 UI 元素。

应用授权策略

还有一件事你应记住。 有一个页面应该只允许管理员访问,出于方便命名为 Pages/AdminsOnly.cshtml。 让我们创建一个策略来检查 IsAdmin=True 声明。

  1. 在 Program.cs 中,进行以下更改:

    1. 纳入以下突出显示的代码:

      // Add services to the container.
      builder.Services.AddRazorPages();
      builder.Services.AddTransient<IEmailSender, EmailSender>();
      builder.Services.AddSingleton(new QRCodeService(new QRCodeGenerator()));
      builder.Services.AddAuthorization(options =>
          options.AddPolicy("Admin", policy =>
              policy.RequireAuthenticatedUser()
                  .RequireClaim("IsAdmin", bool.TrueString)));
      
      var app = builder.Build();
      

      上述代码定义了名为 Admin 的授权策略。 该策略要求对用户进行身份验证并将 IsAdmin 声明设置为 True

    2. 修改对 AddRazorPages 的调用,如下所示:

      builder.Services.AddRazorPages(options =>
          options.Conventions.AuthorizePage("/AdminsOnly", "Admin"));
      

      AuthorizePage 方法调用通过应用 Admin 策略来保护 /AdminsOnly Razor Page 路由。 经过身份验证但不满足策略要求的用户将收到“拒绝访问”消息。

      提示

      或者,可以改为修改 AdminsOnly.cshtml.cs。 在这种情况下,可以将 [Authorize(Policy = "Admin")] 作为 AdminsOnlyModel 类上的属性添加。 上面显示的 AuthorizePage 方法的优点是,受保护的 Razor Page 无需修改。 授权方面改为在 Program.cs 中进行管理。

  2. 在 PPages/Shared/_Layout.cshtml 中,合并以下更改:

    <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Pizza">Pizza List</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
        </li>
        @if (Context.User.HasClaim("IsAdmin", bool.TrueString))
        {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/AdminsOnly">Admins</a>
        </li>
        }
    </ul>
    

    如果用户不是管理员,上述更改会有条件地隐藏标头中的“管理员”链接。

为用户添加 IsAdmin 声明

为了确定哪些用户应获得 IsAdmin=True 声明,你的应用将依赖于已确认的电子邮件地址来标识管理员。

  1. 在 appsettings.json 中,添加突出显示的属性:

    {
      "AdminEmail" : "admin@contosopizza.com",
      "Logging": {
    

    这是分配声明的已确认的电子邮件地址。

  2. 在 Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs 中,进行以下更改:

    1. 纳入以下突出显示的代码:

      public class ConfirmEmailModel : PageModel
      {
          private readonly UserManager<RazorPagesPizzaUser> _userManager;
          private readonly IConfiguration Configuration;
      
          public ConfirmEmailModel(UserManager<RazorPagesPizzaUser> userManager,
                                      IConfiguration configuration)
          {
              _userManager = userManager;
              Configuration = configuration;
          }
      
      

      上述更改将修改构造函数以接收来自 IoC 容器的 IConfigurationIConfiguration 包含来自 appsettings.json 的值,并分配给只读属性 Configuration

    2. 将突出显示的更改应用于 OnGetAsync 方法:

      public async Task<IActionResult> OnGetAsync(string userId, string code)
      {
          if (userId == null || code == null)
          {
              return RedirectToPage("/Index");
          }
      
          var user = await _userManager.FindByIdAsync(userId);
          if (user == null)
          {
              return NotFound($"Unable to load user with ID '{userId}'.");
          }
      
          code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
          var result = await _userManager.ConfirmEmailAsync(user, code);
          StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
      
          var adminEmail = Configuration["AdminEmail"] ?? string.Empty;
          if(result.Succeeded)
          {
              var isAdmin = string.Compare(user.Email, adminEmail, true) == 0 ? true : false;
              await _userManager.AddClaimAsync(user, 
                  new Claim("IsAdmin", isAdmin.ToString()));
          }
      
          return Page();
      }
      

      在上述代码中:

      • Configuration 属性读取 AdminEmail 字符串,并将其分配给 adminEmail
      • 如果 appsettings.json 中没有相应的值,则使用 Null 合并操作符 ?? 确保 adminEmail 设置为 string.Empty
      • 如果用户的电子邮件确认成功:
        • 将用户的地址与 adminEmail 进行比较。 string.Compare() 用于不区分大小写的比较。
        • 调用 UserManager 类的 AddClaimAsync 方法,以在 AspNetUserClaims 表中保存 IsAdmin 声明。
    3. 在文件顶部添加以下代码。 它可解析 OnGetAsync 方法中的 Claim 类引用:

      using System.Security.Claims;
      

测试管理员声明

让我们进行最后一次测试来验证新的管理员功能。

  1. 确保已保存所有更改。

  2. 使用 dotnet run 运行应用。

  3. 导航到应用并以现有用户身份登录(如果尚未登录)。 从标头中选择“披萨列表”。 请注意,未向用户显示删除或创建披萨的 UI 元素。

  4. 标头中没有“管理员”链接。 在浏览器的地址栏中,直接导航到“AdminsOnly”页面。 将 URL 中的 /Pizza 替换为 /AdminsOnly

    禁止用户导航到该页面。 将显示“拒绝访问”消息。

  5. 选择“注销”。

  6. 使用地址 admin@contosopizza.com 注册新用户。

  7. 和前面一样,请确认新用户的电子邮件地址并登录。

  8. 以新管理用户身份登录后,选择标头中的“披萨列表”链接。

    管理用户可以创建和删除披萨。

  9. 选择标头中的“管理员”链接。

    系统随即显示“AdminsOnly”页。

检查 AspNetUserClaims 表

在 VS Code 中使用 SQL Server 扩展,运行以下查询:

SELECT u.Email, c.ClaimType, c.ClaimValue
FROM dbo.AspNetUserClaims AS c
    INNER JOIN dbo.AspNetUsers AS u
    ON c.UserId = u.Id

随即显示包含类似以下结果的选项卡:

电子邮件 ClaimType ClaimValue
admin@contosopizza.com IsAdmin 正确

IsAdmin 声明作为键值对存储在 AspNetUserClaims 表中。 AspNetUserClaims 记录与 AspNetUsers 表中的用户记录相关联。

摘要

在本单元中,你通过修改应用来存储声明并应用策略进行条件访问。