練習 - 搭配原則型授權使用宣告

已完成

在前一個單元中,您已了解驗證與授權之間的差異。 還了解原則如何使用宣告來授權。 在本單元中,您將使用身分識別來儲存宣告,並套用原則來進行條件式存取。

保護披薩清單

您收到新的需求,要求應該只有已驗證的使用者才能看到 [披薩清單] 頁面。 此外,只允許系統管理員建立和刪除披薩。 讓我們將其鎖定。

  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 宣告。 程式碼會從父 PageModel 類別中的 HttpContext 取得已驗證使用者的相關資訊。 此評估的結果會透過名為 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 頁面路由。 已驗證但未滿足原則需求的使用者會看到 [拒絕存取] 訊息。

      提示

      或者,您可以改為修改 AdminsOnly.cshtml.cs。 在此情況下,您會將 [Authorize(Policy = "Admin")] 新增為 AdminsOnlyModel 類別上的屬性。 上面所示 AuthorizePage 方法的優點是,要保護的 Razor 頁面不需要修改。 在 Program.cs 中,會改為管理授權層面。

  2. Pages/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>
    

    如果使用者不是系統管理員,上述變更會有條件地隱藏標頭中的 Admin 連結。 它會使用 RazorPage 類別的 Context 屬性來存取包含已驗證使用者相關資訊的 HttpContext

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();
      }
      

      在上述程式碼中:

      • AdminEmail 字串是從 Configuration 屬性讀取的,並指派給 adminEmail
      • Null 聯合運算子 ?? 用來確保若 appsettings.json 中沒有對應值時,adminEmail 會設定為 string.Empty
      • 如果使用者的電子郵件已成功確認:
        • 使用者的位址會與 adminEmail 進行比較。 string.Compare() 用於不區分大小寫的比較。
        • 系統會叫用 UserManager 類別的 AddClaimAsync 方法,以便將 IsAdmin 宣告儲存在 AspNetUserClaims 資料表中。
    3. 將下列程式碼新增至檔案開頭處。 其會解析 OnGetAsync 方法中的 Claim 類別參考:

      using System.Security.Claims;
      

測試管理宣告

讓我們執行最後一項測試來驗證新的系統管理員功能。

  1. 確定您已儲存所有的變更。

  2. 使用 dotnet run 執行應用程式。

  3. 如果您尚未登入,請瀏覽至您的應用程式,並使用現有的使用者登入。 從標頭中選取 [披薩清單]。 請注意,不會為使用者呈現 UI 元素,以刪除或建立披薩。

  4. 標頭中沒有 Admins 連結。 在瀏覽器的網址列中,直接瀏覽至 [AdminsOnly] 頁面。 將 URL 中的 /Pizza 取代為 /AdminsOnly

    禁止使用者瀏覽至該頁面。 [存取遭拒] 訊息隨即顯示。

  5. 選取 [登出]

  6. 使用位址 admin@contosopizza.com 註冊新的使用者。

  7. 如前所述,確認新使用者的電子郵件地址並登入。

  8. 一旦使用新的系統管理使用者登入,請選取標頭中的 [披薩清單] 連結。

    系統管理使用者可以建立和刪除披薩。

  9. 選取標頭中的 Admins 連結。

    [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 True

IsAdmin 宣告會以機碼值組的形式儲存在 AspNetUserClaims 資料表中。 AspNetUserClaims 記錄會與 AspNetUsers 資料表中的使用者記錄產生關聯。

摘要

在本單元中,您已修改應用程式來儲存宣告,並套用原則進行條件式存取。