練習 - 搭配原則型授權使用宣告
在前一個單元中,您已了解驗證與授權之間的差異。 還了解原則如何使用宣告來授權。 在本單元中,您將使用身分識別來儲存宣告,並套用原則來進行條件式存取。
保護披薩清單
您收到新的需求,要求應該只有已驗證的使用者才能看到 [披薩清單] 頁面。 此外,只允許系統管理員建立和刪除披薩。 讓我們將其鎖定。
在 Pages/Pizza.cshtml.cs 中,套用下列變更:
將
[Authorize]
屬性新增至PizzaModel
類別。[Authorize] public class PizzaModel : PageModel
此屬性描述頁面的使用者授權需求。 在此案例中,除了對使用者進行驗證之外,沒有其他任何需求。 不允許匿名使用者檢視該頁面,而且系統會將其重新導向至登入頁面。
將下列這一行新增至檔案頂端的
using
指示詞,以解析Authorize
的參考:using Microsoft.AspNetCore.Authorization;
將下列屬性加入至
PizzaModel
類別:[Authorize] public class PizzaModel : PageModel { public bool IsAdmin => HttpContext.User.HasClaim("IsAdmin", bool.TrueString); public List<Pizza> pizzas = new();
上述程式碼會判斷已驗證的使用者是否具有值為
True
的IsAdmin
宣告。 程式碼會從父PageModel
類別中的HttpContext
取得已驗證使用者的相關資訊。 此評估的結果會透過名為IsAdmin
的唯讀屬性來存取。將
if (!IsAdmin) return Forbid();
同時新增至OnPost
和OnPostDelete
方法的開頭: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 狀態碼。
在 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
宣告。
在 Program.cs中,進行下列變更:
併入下列反白顯示的程式碼:
// 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
。修改
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 中,會改為管理授權層面。
在 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
宣告,您的應用程式將依賴已確認的電子郵件地址來識別系統管理員。
在 appsettings.json 中,新增醒目提示的屬性:
{ "AdminEmail" : "admin@contosopizza.com", "Logging": {
這是取得所指派宣告的已確認電子郵件地址。
在 Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs 中,進行下列變更:
併入下列反白顯示的程式碼:
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 容器接收
IConfiguration
。IConfiguration
包含來自 appsettings.json 的值,並指派給名為Configuration
的唯讀屬性。將反白顯示的變更套用至
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
資料表中。
- 使用者的位址會與
將下列程式碼新增至檔案開頭處。 其會解析
OnGetAsync
方法中的Claim
類別參考:using System.Security.Claims;
測試管理宣告
讓我們執行最後一項測試來驗證新的系統管理員功能。
確定您已儲存所有的變更。
使用
dotnet run
執行應用程式。如果您尚未登入,請瀏覽至您的應用程式,並使用現有的使用者登入。 從標頭中選取 [披薩清單]。 請注意,不會為使用者呈現 UI 元素,以刪除或建立披薩。
標頭中沒有 Admins 連結。 在瀏覽器的網址列中,直接瀏覽至 [AdminsOnly] 頁面。 將 URL 中的
/Pizza
取代為/AdminsOnly
。禁止使用者瀏覽至該頁面。 [存取遭拒] 訊息隨即顯示。
選取 [登出]。
使用位址
admin@contosopizza.com
註冊新的使用者。如前所述,確認新使用者的電子郵件地址並登入。
一旦使用新的系統管理使用者登入,請選取標頭中的 [披薩清單] 連結。
系統管理使用者可以建立和刪除披薩。
選取標頭中的 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
資料表中的使用者記錄產生關聯。
摘要
在本單元中,您已修改應用程式來儲存宣告,並套用原則進行條件式存取。