使用以授權機制保護的使用者資料來建立 ASP.NET Core Web 應用程式
作者:Rick Anderson 與 Joe Audette
本教學課程示範如何使用以授權機制保護的使用者資料來建立 ASP.NET Core Web 應用程式。 它會顯示已驗證 (已註冊) 使用者已建立的連絡人清單。 有三個安全性群組:
- 已註冊的使用者可以檢視所有已核准的資料,且能編輯/刪除自己的資料。
- 管理員可以核准或拒絕連絡人資料。 使用者只能看到核准的連絡人。
- 系統管理員可以核准/拒絕以及編輯/刪除任何資料。
本文件中的影像與最新的範本不完全相符。
在下圖中,使用者 Rick (rick@example.com
) 已登入。 Rick 只能檢視已核准的連絡人,以及 [編輯]/[刪除]/[新建] 連絡人的連結。 只有 Rick 建立的最後一筆記錄會顯示 [編輯] 和 [刪除] 連結。 在管理員或系統管理員將狀態變更為「已核准」之前,其他使用者不會看到最後一筆記錄。
在下圖中,manager@contoso.com
已登入且為管理員的角色:
下圖顯示連絡人的管理員詳細資料檢視:
只有管理員和系統管理員才會看到 [核准] 和 [拒絕] 按鈕。
在下圖中,admin@contoso.com
已登入且為系統管理員的角色:
系統管理員擁有所有權限。 她可以讀取、編輯或刪除任何連絡人,以及變更連絡人的狀態。
應用程式是透過對下列 Contact
模型執行 Scaffolding 所建立:
public class Contact
{
public int ContactId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}
此範例包含下列授權處理常式:
ContactIsOwnerAuthorizationHandler
:確保使用者只能編輯其資料。ContactManagerAuthorizationHandler
:允許管理員核准或拒絕連絡人。ContactAdministratorsAuthorizationHandler
:允許系統管理員核准或拒絕連絡人,以及編輯/刪除連絡人。
必要條件
此為進階教學課程。 您應熟悉:
入門和已完成的應用程式
下載完整應用程式。 測試已完成的應用程式,讓您熟悉其安全性功能。
入門應用程式
執行應用程式、點選 ContactManager 連結,並確認您可以建立、編輯和刪除連絡人。 若要建立入門應用程式,請參閱建立入門應用程式。
保護使用者資料
下列各節具有建立安全使用者資料應用程式的所有主要步驟。 您可能會發現參考已完成的專案會很有幫助。
將連絡人資料繫結至使用者
使用 ASP.NET Identity 使用者識別碼來確保使用者可以編輯其資料,但不能編輯其他使用者的資料。 將 OwnerID
和 ContactStatus
新增至 Contact
模型:
public class Contact
{
public int ContactId { get; set; }
// user ID from AspNetUser table.
public string? OwnerID { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string? Email { get; set; }
public ContactStatus Status { get; set; }
}
public enum ContactStatus
{
Submitted,
Approved,
Rejected
}
OwnerID
是 Identity 資料庫中 AspNetUser
資料表的使用者識別碼。 Status
欄位會決定一般使用者是否可以檢視連絡人。
建立新的移轉並更新資料庫:
dotnet ef migrations add userID_Status
dotnet ef database update
將角色服務新增至 Identity
附加 AddRoles 以新增角色服務:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
要求已驗證的使用者
將後援授權原則設定為要求使用者進行驗證:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
上述醒目提示的程式碼會設定後援授權原則。 後援授權原則需要驗證所有使用者,但 Razor Pages、控制器或具有授權屬性的動作方法除外。 例如,Razor Pages、控制器或搭配 [AllowAnonymous]
或 [Authorize(PolicyName="MyPolicy")]
的動作方法會使用套用的授權屬性,而不是後援授權原則。
RequireAuthenticatedUser 會將 DenyAnonymousAuthorizationRequirement 新增至目前的執行個體,這會強制執行目前的使用者已驗證。
後援授權原則:
- 會套用至未明確指定授權原則的所有要求。 對於端點路由所提供的要求,這包括未指定授權屬性的任何端點。 對於授權中介軟體之後由其他中介軟體提供的要求,例如靜態檔案,這會將原則套用至所有要求。
將後援授權原則設定為要求使用者通過驗證來保護新增的 Razor Pages和控制器。 根據預設設定為需要授權比依賴新的控制器和 Razor Pages 來包含 [Authorize]
屬性更安全。
AuthorizationOptions 類別也包含 AuthorizationOptions.DefaultPolicy。 DefaultPolicy
是未指定任何原則時,與 [Authorize]
屬性搭配使用的原則。 [Authorize]
不包含具名原則,與 [Authorize(PolicyName="MyPolicy")]
不同。
如需原則的詳細資訊,請參閱 ASP.NET Core 中的原則型授權。
MVC 控制器和 Razor Pages要求所有使用者通過驗證的替代方式是新增授權篩選:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllers(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
var app = builder.Build();
上述程式碼會使用授權篩選器,設定後援原則會使用端點路由。 設定後援原則是要求所有使用者通過驗證的慣用方式。
將 AllowAnonymous 新增至 Index
和 Privacy
頁面,讓匿名使用者可以在註冊之前取得網站的相關資訊:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ContactManager.Pages;
[AllowAnonymous]
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
設定測試帳戶
SeedData
類別會建立兩個帳戶:系統管理員和管理員。 使用祕密管理員工具來設定這些帳戶的密碼。 從專案目錄設定密碼 (包含 Program.cs
的目錄):
dotnet user-secrets set SeedUserPW <PW>
如果指定弱密碼,則會在呼叫 SeedData.Initialize
時擲回例外狀況。
更新應用程式以使用測試密碼:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
// requires using Microsoft.Extensions.Configuration;
// Set password with the Secret Manager tool.
// dotnet user-secrets set SeedUserPW <pw>
var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");
await SeedData.Initialize(services, testUserPw);
}
建立測試帳戶並更新連絡人
更新 SeedData
類別中的 Initialize
方法,以建立測試帳戶:
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
// For sample purposes seed both with the same password.
// Password is set with the following:
// dotnet user-secrets set SeedUserPW <pw>
// The admin user can do anything
var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);
// allowed user can create and edit contacts that they create
var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);
SeedDB(context, adminID);
}
}
private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
string testUserPw, string UserName)
{
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();
var user = await userManager.FindByNameAsync(UserName);
if (user == null)
{
user = new IdentityUser
{
UserName = UserName,
EmailConfirmed = true
};
await userManager.CreateAsync(user, testUserPw);
}
if (user == null)
{
throw new Exception("The password is probably not strong enough!");
}
return user.Id;
}
private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
string uid, string role)
{
var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();
if (roleManager == null)
{
throw new Exception("roleManager null");
}
IdentityResult IR;
if (!await roleManager.RoleExistsAsync(role))
{
IR = await roleManager.CreateAsync(new IdentityRole(role));
}
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();
//if (userManager == null)
//{
// throw new Exception("userManager is null");
//}
var user = await userManager.FindByIdAsync(uid);
if (user == null)
{
throw new Exception("The testUserPw password was probably not strong enough!");
}
IR = await userManager.AddToRoleAsync(user, role);
return IR;
}
將系統管理員使用者識別碼和 ContactStatus
新增至連絡人。 將其中一個連絡人設為「已提交」而另一個設為「已拒絕」。 將使用者識別碼和狀態新增至所有連絡人。 只會顯示一個連絡人:
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com",
Status = ContactStatus.Approved,
OwnerID = adminID
},
建立擁有者、管理員和系統管理員授權處理常式
在 Authorization 資料夾中建立 ContactIsOwnerAuthorizationHandler
類別。 ContactIsOwnerAuthorizationHandler
會確認對資源採取行動的使用者是否擁有資源。
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
namespace ContactManager.Authorization
{
public class ContactIsOwnerAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
UserManager<IdentityUser> _userManager;
public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser>
userManager)
{
_userManager = userManager;
}
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}
// If not asking for CRUD permission, return.
if (requirement.Name != Constants.CreateOperationName &&
requirement.Name != Constants.ReadOperationName &&
requirement.Name != Constants.UpdateOperationName &&
requirement.Name != Constants.DeleteOperationName )
{
return Task.CompletedTask;
}
if (resource.OwnerID == _userManager.GetUserId(context.User))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
如果目前已驗證的使用者是連絡人擁有者,則 ContactIsOwnerAuthorizationHandler
會呼叫 context.Succeed。 授權處理常式通常會:
- 符合需求時呼叫
context.Succeed
。 - 不符合需求時傳回
Task.CompletedTask
。 在沒有先前呼叫context.Success
或context.Fail
的情況下傳回Task.CompletedTask
不屬於成功或失敗,它可讓其他授權處理常式執行。
如果您需要明確失敗,請呼叫 context.Fail。
應用程式可讓連絡人擁有者編輯/刪除/建立自己的資料。 ContactIsOwnerAuthorizationHandler
不需要檢查在需求參數中傳遞的作業。
建立管理員授權處理常式
在 Authorization 資料夾中建立 ContactManagerAuthorizationHandler
類別。 ContactManagerAuthorizationHandler
會確認對資源採取行動的使用者是管理員。 只有管理員可以核准或拒絕內容變更 (新增或變更)。
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
namespace ContactManager.Authorization
{
public class ContactManagerAuthorizationHandler :
AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}
// If not asking for approval/reject, return.
if (requirement.Name != Constants.ApproveOperationName &&
requirement.Name != Constants.RejectOperationName)
{
return Task.CompletedTask;
}
// Managers can approve or reject.
if (context.User.IsInRole(Constants.ContactManagersRole))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
建立系統管理員授權處理常式
在 Authorization 資料夾中建立 ContactAdministratorsAuthorizationHandler
類別。 ContactAdministratorsAuthorizationHandler
會確認對資源採取行動的使用者是系統管理員。 系統管理員可以執行所有作業。
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace ContactManager.Authorization
{
public class ContactAdministratorsAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null)
{
return Task.CompletedTask;
}
// Administrators can do anything.
if (context.User.IsInRole(Constants.ContactAdministratorsRole))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
註冊授權處理常式
使用 Entity Framework Core 的服務必須註冊才能使用 AddScoped 進行相依性插入。 ContactIsOwnerAuthorizationHandler
會使用建置在 Entity Framework Core 上的 ASP.NET Core Identity。 向服務集合註冊處理常式,以便透過相依性插入提供給 ContactsController
。 在 ConfigureServices
的結尾新增下列程式碼:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
// requires using Microsoft.Extensions.Configuration;
// Set password with the Secret Manager tool.
// dotnet user-secrets set SeedUserPW <pw>
var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");
await SeedData.Initialize(services, testUserPw);
}
ContactAdministratorsAuthorizationHandler
和 ContactManagerAuthorizationHandler
會新增為單一資料庫。 其為單一資料庫,因為它們不會使用 EF,而所需的所有資訊都是在 HandleRequirementAsync
方法的 Context
參數中。
支援授權
在本節中,您會更新 Razor Pages 並新增作業需求類別。
檢閱連絡人作業需求類別
檢閱 ContactOperations
類別。 此類別包含應用程式支援的需求:
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace ContactManager.Authorization
{
public static class ContactOperations
{
public static OperationAuthorizationRequirement Create =
new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
public static OperationAuthorizationRequirement Read =
new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};
public static OperationAuthorizationRequirement Update =
new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName};
public static OperationAuthorizationRequirement Delete =
new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
public static OperationAuthorizationRequirement Approve =
new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
public static OperationAuthorizationRequirement Reject =
new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
}
public class Constants
{
public static readonly string CreateOperationName = "Create";
public static readonly string ReadOperationName = "Read";
public static readonly string UpdateOperationName = "Update";
public static readonly string DeleteOperationName = "Delete";
public static readonly string ApproveOperationName = "Approve";
public static readonly string RejectOperationName = "Reject";
public static readonly string ContactAdministratorsRole =
"ContactAdministrators";
public static readonly string ContactManagersRole = "ContactManagers";
}
}
建立連絡人 Razor Pages 的基底類別
建立基底類別,其中包含連絡人 Razor Pages 中使用的服務。 基底類別會將初始化程式碼放在單一位置:
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ContactManager.Pages.Contacts
{
public class DI_BasePageModel : PageModel
{
protected ApplicationDbContext Context { get; }
protected IAuthorizationService AuthorizationService { get; }
protected UserManager<IdentityUser> UserManager { get; }
public DI_BasePageModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager) : base()
{
Context = context;
UserManager = userManager;
AuthorizationService = authorizationService;
}
}
}
上述 程式碼:
- 將
IAuthorizationService
服務新增至授權處理常式的存取權。 - 新增 Identity
UserManager
服務。 - 加入
ApplicationDbContext
。
更新 CreateModel
更新建立頁面模型:
- 使用
DI_BasePageModel
基底類別的建構函式。 OnPostAsync
方法可:- 將使用者識別碼新增至
Contact
模型。 - 呼叫授權處理常式,以確認使用者具有建立連絡人的權限。
- 將使用者識別碼新增至
using ContactManager.Authorization;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace ContactManager.Pages.Contacts
{
public class CreateModel : DI_BasePageModel
{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public IActionResult OnGet()
{
return Page();
}
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
Contact.OwnerID = UserManager.GetUserId(User);
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Create);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Context.Contact.Add(Contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
更新 IndexModel
更新 OnGetAsync
方法,以僅向一般使用者顯示已核准的連絡人:
public class IndexModel : DI_BasePageModel
{
public IndexModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public IList<Contact> Contact { get; set; }
public async Task OnGetAsync()
{
var contacts = from c in Context.Contact
select c;
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
// Only approved contacts are shown UNLESS you're authorized to see them
// or you are the owner.
if (!isAuthorized)
{
contacts = contacts.Where(c => c.Status == ContactStatus.Approved
|| c.OwnerID == currentUserId);
}
Contact = await contacts.ToListAsync();
}
}
更新 EditModel
新增授權處理常式,以確認使用者擁有連絡人。 由於正在驗證資源授權,因此 [Authorize]
屬性是不夠的。 評估屬性時,應用程式無法存取資源。 資源型授權必須是命令式的。 一旦應用程式能夠存取資源,就必須執行檢查,方法是在頁面模型中載入資源,或在處理常式本身內載入它。 您經常透過傳入資源金鑰來存取資源。
public class EditModel : DI_BasePageModel
{
public EditModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
Contact = contact;
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch Contact from DB to get OwnerID.
var contact = await Context
.Contact.AsNoTracking()
.FirstOrDefaultAsync(m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Contact.OwnerID = contact.OwnerID;
Context.Attach(Contact).State = EntityState.Modified;
if (Contact.Status == ContactStatus.Approved)
{
// If the contact is updated after approval,
// and the user cannot approve,
// set the status back to submitted so the update can be
// checked and approved.
var canApprove = await AuthorizationService.AuthorizeAsync(User,
Contact,
ContactOperations.Approve);
if (!canApprove.Succeeded)
{
Contact.Status = ContactStatus.Submitted;
}
}
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
更新 DeleteModel
更新刪除頁面模型,以使用授權處理常式來確認使用者具有連絡人的刪除權限。
public class DeleteModel : DI_BasePageModel
{
public DeleteModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? _contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (_contact == null)
{
return NotFound();
}
Contact = _contact;
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Delete);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
var contact = await Context
.Contact.AsNoTracking()
.FirstOrDefaultAsync(m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, contact,
ContactOperations.Delete);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Context.Contact.Remove(contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
將授權服務插入檢視
目前,UI 會顯示使用者無法修改之連絡人的編輯和刪除連結。
將授權服務插入 Pages/_ViewImports.cshtml
檔案中,以便可供所有檢視使用:
@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService
上述標記會新增數個 using
陳述式。
更新 Pages/Contacts/Index.cshtml
中的 [編輯] 和 [刪除] 連結,使其僅針對具有適當權限的使用者進行轉譯:
@page
@model ContactManager.Pages.Contacts.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Address)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].City)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].State)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Zip)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Email)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Status)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Contact) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Html.DisplayFor(modelItem => item.City)
</td>
<td>
@Html.DisplayFor(modelItem => item.State)
</td>
<td>
@Html.DisplayFor(modelItem => item.Zip)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
@Html.DisplayFor(modelItem => item.Status)
</td>
<td>
@if ((await AuthorizationService.AuthorizeAsync(
User, item,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
<text> | </text>
}
<a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>
@if ((await AuthorizationService.AuthorizeAsync(
User, item,
ContactOperations.Delete)).Succeeded)
{
<text> | </text>
<a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
}
</td>
</tr>
}
</tbody>
</table>
警告
隱藏沒有變更資料權限之使用者的連結,並不會保護應用程式的安全。 隱藏連結可讓應用程式更易於使用,也就是只顯示有效的連結。 使用者可以對產生的 URL 進行駭客攻擊,以叫用他們未擁有資料的編輯和刪除作業。 Razor Pages 或控制器必須強制執行存取檢查才能保護資料的安全。
更新詳細資料
更新詳細資料檢視,讓管理員可以核准或拒絕連絡人:
@*Preceding markup omitted for brevity.*@
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Contact.Email)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Contact.Email)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Contact.Status)
</dt>
<dd>
@Html.DisplayFor(model => model.Contact.Status)
</dd>
</dl>
</div>
@if (Model.Contact.Status != ContactStatus.Approved)
{
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact, ContactOperations.Approve)).Succeeded)
{
<form style="display:inline;" method="post">
<input type="hidden" name="id" value="@Model.Contact.ContactId" />
<input type="hidden" name="status" value="@ContactStatus.Approved" />
<button type="submit" class="btn btn-xs btn-success">Approve</button>
</form>
}
}
@if (Model.Contact.Status != ContactStatus.Rejected)
{
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact, ContactOperations.Reject)).Succeeded)
{
<form style="display:inline;" method="post">
<input type="hidden" name="id" value="@Model.Contact.ContactId" />
<input type="hidden" name="status" value="@ContactStatus.Rejected" />
<button type="submit" class="btn btn-xs btn-danger">Reject</button>
</form>
}
}
<div>
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
<text> | </text>
}
<a asp-page="./Index">Back to List</a>
</div>
更新詳細資料頁面模型
public class DetailsModel : DI_BasePageModel
{
public DetailsModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);
if (_contact == null)
{
return NotFound();
}
Contact = _contact;
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
{
var contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var contactOperation = (status == ContactStatus.Approved)
? ContactOperations.Approve
: ContactOperations.Reject;
var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
contactOperation);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
contact.Status = status;
Context.Contact.Update(contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
將使用者新增至角色或移除使用者
如需下列資訊,請參閱這個問題:
- 從使用者移除權限。 例如,在聊天應用程式中將使用者靜音。
- 將權限新增至使用者。
挑戰與禁止之間的差異
此應用程式會將預設原則設定為要求已驗證的使用者。 下列程式碼允許匿名使用者。 允許匿名使用者顯示挑戰與禁止之間的差異。
[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
public Details2Model(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);
if (_contact == null)
{
return NotFound();
}
Contact = _contact;
if (!User.Identity!.IsAuthenticated)
{
return Challenge();
}
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return Forbid();
}
return Page();
}
}
在上述程式碼中:
- 當使用者未通過驗證時,即會傳回
ChallengeResult
。 傳回ChallengeResult
時,使用者會重新導向至登入頁面。 - 當使用者經過驗證但未獲得授權時,即會傳回
ForbidResult
。 傳回ForbidResult
時,使用者會重新導向至拒絕存取的頁面。
測試已完成的應用程式
警告
本文使用 祕密管理員工具 來儲存植入使用者帳戶的密碼。 祕密管理員工具會在區域開發期間儲存敏感性資料。 如需將應用程式部署至測試,或是生產環境時可使用到的驗證程序相關資訊,請參閱保護驗證流程那一章節。
如果您尚未為植入的使用者帳戶設定密碼,請使用祕密管理員工具來設定密碼:
選擇強式密碼:
- 長度至少為 12 個字元,但 14 個字元以上更好。
- 大寫字母、小寫字母、數字及符號的組合。
- 不可為能在字典或人名、字元、產品或組織名稱中找到的字組。
- 與先前的密碼明顯不同
- 容易讓您記住,但其他人難以猜到。 請考慮使用 "6MonkeysRLooking^" 等令人難忘的片語。
從專案的資料夾中執行下列命令,其中
<PW>
是密碼:dotnet user-secrets set SeedUserPW <PW>
如果應用程式具有連絡人:
- 刪除
Contact
資料表中的所有記錄。 - 重新啟動應用程式以植入資料庫。
測試已完成應用程式的簡單方式是啟動三種不同的瀏覽器 (或 incognito/InPrivate 工作階段)。 在一個瀏覽器中,註冊新的使用者 (例如 test@example.com
)。 以不同的使用者登入每個瀏覽器。 確認下列作業:
- 已註冊的使用者可以檢視所有已核准的連絡人資料。
- 已註冊的使用者可以編輯/刪除自己的資料。
- 管理員可以核准/拒絕連絡人資料。
Details
檢視會顯示 [核准] 和 [拒絕] 按鈕。 - 系統管理員可以核准/拒絕以及編輯/刪除所有資料。
使用者 | 核准或拒絕連絡人 | 選項。 |
---|---|---|
test@example.com | No | 編輯和刪除其資料。 |
manager@contoso.com | Yes | 編輯和刪除其資料。 |
admin@contoso.com | Yes | 編輯和刪除所有資料。 |
在系統管理員的瀏覽器中建立連絡人。 從系統管理員連絡人複製要刪除和編輯的 URL。 將這些連結貼到測試使用者的瀏覽器中,以確認測試使用者無法執行這些作業。
建立入門應用程式
建立名為「ContactManager」的 Razor Pages 應用程式
- 使用個別使用者帳戶建立應用程式。
- 將其命名為「ContactManager」,使命名空間符合範例中使用的命名空間。
-uld
會指定 LocalDB,而不是 SQLite
dotnet new webapp -o ContactManager -au Individual -uld
新增
Models/Contact.cs
:secure-data\samples\starter6\ContactManager\Models\Contact.csusing System.ComponentModel.DataAnnotations; namespace ContactManager.Models { public class Contact { public int ContactId { get; set; } public string? Name { get; set; } public string? Address { get; set; } public string? City { get; set; } public string? State { get; set; } public string? Zip { get; set; } [DataType(DataType.EmailAddress)] public string? Email { get; set; } } }
Scaffold
Contact
模型。建立初始移轉並更新資料庫:
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet-aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update
注意
根據預設,要安裝的 .NET 二進位檔架構代表目前執行的 OS 架構。 若要指定不同的 OS 架構,請參閱 dotnet tool install, --arch option。 如需詳細資訊,請參閱 GitHub 問題 dotnet/AspNetCore.Docs #29262。
更新
Pages/Shared/_Layout.cshtml
檔案中的 ContactManager 錨點:<a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
建立、編輯和刪除連絡人以測試應用程式
植入資料庫
將 SeedData 類別新增至 Data 資料夾:
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries
namespace ContactManager.Data
{
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw="")
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
SeedDB(context, testUserPw);
}
}
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com"
},
new Contact
{
Name = "Thorsten Weinrich",
Address = "5678 1st Ave W",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "thorsten@example.com"
},
new Contact
{
Name = "Yuhong Li",
Address = "9012 State st",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "yuhong@example.com"
},
new Contact
{
Name = "Jon Orton",
Address = "3456 Maple St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "jon@example.com"
},
new Contact
{
Name = "Diliana Alexieva-Bosseva",
Address = "7890 2nd Ave E",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "diliana@example.com"
}
);
context.SaveChanges();
}
}
}
從 Program.cs
呼叫 SeedData.Initialize
:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
await SeedData.Initialize(services);
}
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
測試應用程式已植入資料庫。 如果連絡人資料庫中有任何資料列,則植入方法不會執行。
本教學課程示範如何使用以授權機制保護的使用者資料來建立 ASP.NET Core Web 應用程式。 它會顯示已驗證 (已註冊) 使用者已建立的連絡人清單。 有三個安全性群組:
- 已註冊的使用者可以檢視所有已核准的資料,且能編輯/刪除自己的資料。
- 管理員可以核准或拒絕連絡人資料。 使用者只能看到核准的連絡人。
- 系統管理員可以核准/拒絕以及編輯/刪除任何資料。
本文件中的影像與最新的範本不完全相符。
在下圖中,使用者 Rick (rick@example.com
) 已登入。 Rick 只能檢視已核准的連絡人,以及 [編輯]/[刪除]/[新建] 連絡人的連結。 只有 Rick 建立的最後一筆記錄會顯示 [編輯] 和 [刪除] 連結。 在管理員或系統管理員將狀態變更為「已核准」之前,其他使用者不會看到最後一筆記錄。
在下圖中,manager@contoso.com
已登入且為管理員的角色:
下圖顯示連絡人的管理員詳細資料檢視:
只有管理員和系統管理員才會看到 [核准] 和 [拒絕] 按鈕。
在下圖中,admin@contoso.com
已登入且為系統管理員的角色:
系統管理員擁有所有權限。 她可以讀取/編輯/刪除任何連絡人,以及變更連絡人的狀態。
應用程式是透過對下列 Contact
模型執行 Scaffolding 所建立:
public class Contact
{
public int ContactId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}
此範例包含下列授權處理常式:
ContactIsOwnerAuthorizationHandler
:確保使用者只能編輯其資料。ContactManagerAuthorizationHandler
:允許管理員核准或拒絕連絡人。ContactAdministratorsAuthorizationHandler
:允許系統管理員:- 核准或拒絕連絡人
- 編輯和刪除連絡人
必要條件
此為進階教學課程。 您應熟悉:
入門和已完成的應用程式
下載完整應用程式。 測試已完成的應用程式,讓您熟悉其安全性功能。
入門應用程式
執行應用程式、點選 ContactManager 連結,並確認您可以建立、編輯和刪除連絡人。 若要建立入門應用程式,請參閱建立入門應用程式。
保護使用者資料
下列各節具有建立安全使用者資料應用程式的所有主要步驟。 您可能會發現參考已完成的專案會很有幫助。
將連絡人資料繫結至使用者
使用 ASP.NET Identity 使用者識別碼來確保使用者可以編輯其資料,但不能編輯其他使用者的資料。 將 OwnerID
和 ContactStatus
新增至 Contact
模型:
public class Contact
{
public int ContactId { get; set; }
// user ID from AspNetUser table.
public string OwnerID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
public ContactStatus Status { get; set; }
}
public enum ContactStatus
{
Submitted,
Approved,
Rejected
}
OwnerID
是 Identity 資料庫中 AspNetUser
資料表的使用者識別碼。 Status
欄位會決定一般使用者是否可以檢視連絡人。
建立新的移轉並更新資料庫:
dotnet ef migrations add userID_Status
dotnet ef database update
將角色服務新增至 Identity
附加 AddRoles 以新增角色服務:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
要求已驗證的使用者
將後援驗證原則設定為要求使用者通過驗證:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
上述醒目提示的程式碼會設定後援驗證原則。 後援驗證原則需要驗證所有使用者,但 Razor Pages、控制器或具有驗證屬性的動作方法除外。 例如,Razor Pages、控制器或搭配 [AllowAnonymous]
或 [Authorize(PolicyName="MyPolicy")]
的動作方法會使用套用的驗證屬性,而不是後援驗證原則。
RequireAuthenticatedUser 會將 DenyAnonymousAuthorizationRequirement 新增至目前的執行個體,這會強制執行目前的使用者已驗證。
後援驗證原則:
- 會套用至未明確指定驗證原則的所有要求。 對於端點路由所提供的要求,這將包括未指定授權屬性的任何端點。 對於授權中介軟體之後由其他中介軟體提供的要求,例如靜態檔案,這會將原則套用至所有要求。
將後援驗證原則設定為要求使用者通過驗證來保護新增的 Razor Pages和控制器。 根據預設設定為需要驗證比依賴新的控制器和 Razor Pages 來包含 [Authorize]
屬性更安全。
AuthorizationOptions 類別也包含 AuthorizationOptions.DefaultPolicy。 DefaultPolicy
是未指定任何原則時,與 [Authorize]
屬性搭配使用的原則。 [Authorize]
不包含具名原則,與 [Authorize(PolicyName="MyPolicy")]
不同。
如需原則的詳細資訊,請參閱 ASP.NET Core 中的原則型授權。
MVC 控制器和 Razor Pages要求所有使用者通過驗證的替代方式是新增授權篩選:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
services.AddControllers(config =>
{
// using Microsoft.AspNetCore.Mvc.Authorization;
// using Microsoft.AspNetCore.Authorization;
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
上述程式碼會使用授權篩選器,設定後援原則會使用端點路由。 設定後援原則是要求所有使用者通過驗證的慣用方式。
將 AllowAnonymous 新增至 Index
和 Privacy
頁面,讓匿名使用者可以在註冊之前取得網站的相關資訊:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace ContactManager.Pages
{
[AllowAnonymous]
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
}
設定測試帳戶
SeedData
類別會建立兩個帳戶:系統管理員和管理員。 使用祕密管理員工具來設定這些帳戶的密碼。 從專案目錄設定密碼 (包含 Program.cs
的目錄):
dotnet user-secrets set SeedUserPW <PW>
如果未指定強式密碼,則會在呼叫 SeedData.Initialize
時擲回例外狀況。
更新 Main
以使用測試密碼:
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
// requires using Microsoft.Extensions.Configuration;
var config = host.Services.GetRequiredService<IConfiguration>();
// Set password with the Secret Manager tool.
// dotnet user-secrets set SeedUserPW <pw>
var testUserPw = config["SeedUserPW"];
SeedData.Initialize(services, testUserPw).Wait();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
建立測試帳戶並更新連絡人
更新 SeedData
類別中的 Initialize
方法,以建立測試帳戶:
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
// For sample purposes seed both with the same password.
// Password is set with the following:
// dotnet user-secrets set SeedUserPW <pw>
// The admin user can do anything
var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);
// allowed user can create and edit contacts that they create
var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);
SeedDB(context, adminID);
}
}
private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
string testUserPw, string UserName)
{
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();
var user = await userManager.FindByNameAsync(UserName);
if (user == null)
{
user = new IdentityUser
{
UserName = UserName,
EmailConfirmed = true
};
await userManager.CreateAsync(user, testUserPw);
}
if (user == null)
{
throw new Exception("The password is probably not strong enough!");
}
return user.Id;
}
private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
string uid, string role)
{
var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();
if (roleManager == null)
{
throw new Exception("roleManager null");
}
IdentityResult IR;
if (!await roleManager.RoleExistsAsync(role))
{
IR = await roleManager.CreateAsync(new IdentityRole(role));
}
var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();
//if (userManager == null)
//{
// throw new Exception("userManager is null");
//}
var user = await userManager.FindByIdAsync(uid);
if (user == null)
{
throw new Exception("The testUserPw password was probably not strong enough!");
}
IR = await userManager.AddToRoleAsync(user, role);
return IR;
}
將系統管理員使用者識別碼和 ContactStatus
新增至連絡人。 將其中一個連絡人設為「已提交」而另一個設為「已拒絕」。 將使用者識別碼和狀態新增至所有連絡人。 只會顯示一個連絡人:
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com",
Status = ContactStatus.Approved,
OwnerID = adminID
},
建立擁有者、管理員和系統管理員授權處理常式
在 Authorization 資料夾中建立 ContactIsOwnerAuthorizationHandler
類別。 ContactIsOwnerAuthorizationHandler
會確認對資源採取行動的使用者是否擁有資源。
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
namespace ContactManager.Authorization
{
public class ContactIsOwnerAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
UserManager<IdentityUser> _userManager;
public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser>
userManager)
{
_userManager = userManager;
}
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}
// If not asking for CRUD permission, return.
if (requirement.Name != Constants.CreateOperationName &&
requirement.Name != Constants.ReadOperationName &&
requirement.Name != Constants.UpdateOperationName &&
requirement.Name != Constants.DeleteOperationName )
{
return Task.CompletedTask;
}
if (resource.OwnerID == _userManager.GetUserId(context.User))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
如果目前已驗證的使用者是連絡人擁有者,則 ContactIsOwnerAuthorizationHandler
會呼叫 context.Succeed。 授權處理常式通常會:
- 符合需求時呼叫
context.Succeed
。 - 不符合需求時傳回
Task.CompletedTask
。 在沒有先前呼叫context.Success
或context.Fail
的情況下傳回Task.CompletedTask
不屬於成功或失敗,它可讓其他授權處理常式執行。
如果您需要明確失敗,請呼叫 context.Fail。
應用程式可讓連絡人擁有者編輯/刪除/建立自己的資料。 ContactIsOwnerAuthorizationHandler
不需要檢查在需求參數中傳遞的作業。
建立管理員授權處理常式
在 Authorization 資料夾中建立 ContactManagerAuthorizationHandler
類別。 ContactManagerAuthorizationHandler
會確認對資源採取行動的使用者是管理員。 只有管理員可以核准或拒絕內容變更 (新增或變更)。
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;
namespace ContactManager.Authorization
{
public class ContactManagerAuthorizationHandler :
AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null || resource == null)
{
return Task.CompletedTask;
}
// If not asking for approval/reject, return.
if (requirement.Name != Constants.ApproveOperationName &&
requirement.Name != Constants.RejectOperationName)
{
return Task.CompletedTask;
}
// Managers can approve or reject.
if (context.User.IsInRole(Constants.ContactManagersRole))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
建立系統管理員授權處理常式
在 Authorization 資料夾中建立 ContactAdministratorsAuthorizationHandler
類別。 ContactAdministratorsAuthorizationHandler
會確認對資源採取行動的使用者是系統管理員。 系統管理員可以執行所有作業。
using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace ContactManager.Authorization
{
public class ContactAdministratorsAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Contact>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Contact resource)
{
if (context.User == null)
{
return Task.CompletedTask;
}
// Administrators can do anything.
if (context.User.IsInRole(Constants.ContactAdministratorsRole))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
註冊授權處理常式
使用 Entity Framework Core 的服務必須註冊才能使用 AddScoped 進行相依性插入。 ContactIsOwnerAuthorizationHandler
會使用建置在 Entity Framework Core 上的 ASP.NET Core Identity。 向服務集合註冊處理常式,以便透過相依性插入提供給 ContactsController
。 在 ConfigureServices
的結尾新增下列程式碼:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// Authorization handlers.
services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
}
ContactAdministratorsAuthorizationHandler
和 ContactManagerAuthorizationHandler
會新增為單一資料庫。 其為單一資料庫,因為它們不會使用 EF,而所需的所有資訊都是在 HandleRequirementAsync
方法的 Context
參數中。
支援授權
在本節中,您會更新 Razor Pages 並新增作業需求類別。
檢閱連絡人作業需求類別
檢閱 ContactOperations
類別。 此類別包含應用程式支援的需求:
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace ContactManager.Authorization
{
public static class ContactOperations
{
public static OperationAuthorizationRequirement Create =
new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
public static OperationAuthorizationRequirement Read =
new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};
public static OperationAuthorizationRequirement Update =
new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName};
public static OperationAuthorizationRequirement Delete =
new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
public static OperationAuthorizationRequirement Approve =
new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
public static OperationAuthorizationRequirement Reject =
new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
}
public class Constants
{
public static readonly string CreateOperationName = "Create";
public static readonly string ReadOperationName = "Read";
public static readonly string UpdateOperationName = "Update";
public static readonly string DeleteOperationName = "Delete";
public static readonly string ApproveOperationName = "Approve";
public static readonly string RejectOperationName = "Reject";
public static readonly string ContactAdministratorsRole =
"ContactAdministrators";
public static readonly string ContactManagersRole = "ContactManagers";
}
}
建立連絡人 Razor Pages 的基底類別
建立基底類別,其中包含連絡人 Razor Pages 中使用的服務。 基底類別會將初始化程式碼放在單一位置:
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace ContactManager.Pages.Contacts
{
public class DI_BasePageModel : PageModel
{
protected ApplicationDbContext Context { get; }
protected IAuthorizationService AuthorizationService { get; }
protected UserManager<IdentityUser> UserManager { get; }
public DI_BasePageModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager) : base()
{
Context = context;
UserManager = userManager;
AuthorizationService = authorizationService;
}
}
}
上述 程式碼:
- 將
IAuthorizationService
服務新增至授權處理常式的存取權。 - 新增 Identity
UserManager
服務。 - 加入
ApplicationDbContext
。
更新 CreateModel
更新建立頁面模型建構函式以使用 DI_BasePageModel
基底類別:
public class CreateModel : DI_BasePageModel
{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
更新 CreateModel.OnPostAsync
方法以:
- 將使用者識別碼新增至
Contact
模型。 - 呼叫授權處理常式,以確認使用者具有建立連絡人的權限。
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
Contact.OwnerID = UserManager.GetUserId(User);
// requires using ContactManager.Authorization;
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Create);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Context.Contact.Add(Contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
更新 IndexModel
更新 OnGetAsync
方法,以僅向一般使用者顯示已核准的連絡人:
public class IndexModel : DI_BasePageModel
{
public IndexModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public IList<Contact> Contact { get; set; }
public async Task OnGetAsync()
{
var contacts = from c in Context.Contact
select c;
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
// Only approved contacts are shown UNLESS you're authorized to see them
// or you are the owner.
if (!isAuthorized)
{
contacts = contacts.Where(c => c.Status == ContactStatus.Approved
|| c.OwnerID == currentUserId);
}
Contact = await contacts.ToListAsync();
}
}
更新 EditModel
新增授權處理常式,以確認使用者擁有連絡人。 由於正在驗證資源授權,因此 [Authorize]
屬性是不夠的。 評估屬性時,應用程式無法存取資源。 資源型授權必須是命令式的。 一旦應用程式能夠存取資源,就必須執行檢查,方法是在頁面模型中載入資源,或在處理常式本身內載入它。 您經常透過傳入資源金鑰來存取資源。
public class EditModel : DI_BasePageModel
{
public EditModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (Contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch Contact from DB to get OwnerID.
var contact = await Context
.Contact.AsNoTracking()
.FirstOrDefaultAsync(m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, contact,
ContactOperations.Update);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Contact.OwnerID = contact.OwnerID;
Context.Attach(Contact).State = EntityState.Modified;
if (Contact.Status == ContactStatus.Approved)
{
// If the contact is updated after approval,
// and the user cannot approve,
// set the status back to submitted so the update can be
// checked and approved.
var canApprove = await AuthorizationService.AuthorizeAsync(User,
Contact,
ContactOperations.Approve);
if (!canApprove.Succeeded)
{
Contact.Status = ContactStatus.Submitted;
}
}
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
更新 DeleteModel
更新刪除頁面模型,以使用授權處理常式來確認使用者具有連絡人的刪除權限。
public class DeleteModel : DI_BasePageModel
{
public DeleteModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (Contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, Contact,
ContactOperations.Delete);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
var contact = await Context
.Contact.AsNoTracking()
.FirstOrDefaultAsync(m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var isAuthorized = await AuthorizationService.AuthorizeAsync(
User, contact,
ContactOperations.Delete);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
Context.Contact.Remove(contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
將授權服務插入檢視
目前,UI 會顯示使用者無法修改之連絡人的編輯和刪除連結。
將授權服務插入 Pages/_ViewImports.cshtml
檔案中,以便可供所有檢視使用:
@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService
上述標記會新增數個 using
陳述式。
更新 Pages/Contacts/Index.cshtml
中的 [編輯] 和 [刪除] 連結,使其僅針對具有適當權限的使用者進行轉譯:
@page
@model ContactManager.Pages.Contacts.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Address)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].City)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].State)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Zip)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Email)
</th>
<th>
@Html.DisplayNameFor(model => model.Contact[0].Status)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Contact)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Html.DisplayFor(modelItem => item.City)
</td>
<td>
@Html.DisplayFor(modelItem => item.State)
</td>
<td>
@Html.DisplayFor(modelItem => item.Zip)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
@Html.DisplayFor(modelItem => item.Status)
</td>
<td>
@if ((await AuthorizationService.AuthorizeAsync(
User, item,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
<text> | </text>
}
<a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>
@if ((await AuthorizationService.AuthorizeAsync(
User, item,
ContactOperations.Delete)).Succeeded)
{
<text> | </text>
<a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
}
</td>
</tr>
}
</tbody>
</table>
警告
隱藏沒有變更資料權限之使用者的連結,並不會保護應用程式的安全。 隱藏連結可讓應用程式更易於使用,也就是只顯示有效的連結。 使用者可以對產生的 URL 進行駭客攻擊,以叫用他們未擁有資料的編輯和刪除作業。 Razor Pages 或控制器必須強制執行存取檢查才能保護資料的安全。
更新詳細資料
更新詳細資料檢視,讓管理員可以核准或拒絕連絡人:
@*Precedng markup omitted for brevity.*@
<dt>
@Html.DisplayNameFor(model => model.Contact.Email)
</dt>
<dd>
@Html.DisplayFor(model => model.Contact.Email)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Contact.Status)
</dt>
<dd>
@Html.DisplayFor(model => model.Contact.Status)
</dd>
</dl>
</div>
@if (Model.Contact.Status != ContactStatus.Approved)
{
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact, ContactOperations.Approve)).Succeeded)
{
<form style="display:inline;" method="post">
<input type="hidden" name="id" value="@Model.Contact.ContactId" />
<input type="hidden" name="status" value="@ContactStatus.Approved" />
<button type="submit" class="btn btn-xs btn-success">Approve</button>
</form>
}
}
@if (Model.Contact.Status != ContactStatus.Rejected)
{
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact, ContactOperations.Reject)).Succeeded)
{
<form style="display:inline;" method="post">
<input type="hidden" name="id" value="@Model.Contact.ContactId" />
<input type="hidden" name="status" value="@ContactStatus.Rejected" />
<button type="submit" class="btn btn-xs btn-danger">Reject</button>
</form>
}
}
<div>
@if ((await AuthorizationService.AuthorizeAsync(
User, Model.Contact,
ContactOperations.Update)).Succeeded)
{
<a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
<text> | </text>
}
<a asp-page="./Index">Back to List</a>
</div>
更新詳細資料頁面模型:
public class DetailsModel : DI_BasePageModel
{
public DetailsModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);
if (Contact == null)
{
return NotFound();
}
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return Forbid();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
{
var contact = await Context.Contact.FirstOrDefaultAsync(
m => m.ContactId == id);
if (contact == null)
{
return NotFound();
}
var contactOperation = (status == ContactStatus.Approved)
? ContactOperations.Approve
: ContactOperations.Reject;
var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
contactOperation);
if (!isAuthorized.Succeeded)
{
return Forbid();
}
contact.Status = status;
Context.Contact.Update(contact);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
將使用者新增至角色或移除使用者
如需下列資訊,請參閱這個問題:
- 從使用者移除權限。 例如,在聊天應用程式中將使用者靜音。
- 將權限新增至使用者。
挑戰與禁止之間的差異
此應用程式會將預設原則設定為要求已驗證的使用者。 下列程式碼允許匿名使用者。 允許匿名使用者顯示挑戰與禁止之間的差異。
[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
public Details2Model(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);
if (Contact == null)
{
return NotFound();
}
if (!User.Identity.IsAuthenticated)
{
return Challenge();
}
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return Forbid();
}
return Page();
}
}
在上述程式碼中:
- 當使用者未通過驗證時,即會傳回
ChallengeResult
。 傳回ChallengeResult
時,使用者會重新導向至登入頁面。 - 當使用者經過驗證但未獲得授權時,即會傳回
ForbidResult
。 傳回ForbidResult
時,使用者會重新導向至拒絕存取的頁面。
測試已完成的應用程式
如果您尚未為植入的使用者帳戶設定密碼,請使用祕密管理員工具來設定密碼:
選擇強式密碼:使用 8 個以上的字元,以及至少一個大寫字元、數字和符號。 例如,
Passw0rd!
符合強式密碼需求。從專案的資料夾中執行下列命令,其中
<PW>
是密碼:dotnet user-secrets set SeedUserPW <PW>
如果應用程式具有連絡人:
- 刪除
Contact
資料表中的所有記錄。 - 重新啟動應用程式以植入資料庫。
測試已完成應用程式的簡單方式是啟動三種不同的瀏覽器 (或 incognito/InPrivate 工作階段)。 在一個瀏覽器中,註冊新的使用者 (例如 test@example.com
)。 以不同的使用者登入每個瀏覽器。 確認下列作業:
- 已註冊的使用者可以檢視所有已核准的連絡人資料。
- 已註冊的使用者可以編輯/刪除自己的資料。
- 管理員可以核准/拒絕連絡人資料。
Details
檢視會顯示 [核准] 和 [拒絕] 按鈕。 - 系統管理員可以核准/拒絕以及編輯/刪除所有資料。
使用者 | 由應用程式植入 | 選項。 |
---|---|---|
test@example.com | No | 編輯/刪除自己的資料。 |
manager@contoso.com | Yes | 核准/拒絕以及編輯/刪除自己的資料。 |
admin@contoso.com | Yes | 核准/拒絕以及編輯/刪除所有資料。 |
在系統管理員的瀏覽器中建立連絡人。 從系統管理員連絡人複製要刪除和編輯的 URL。 將這些連結貼到測試使用者的瀏覽器中,以確認測試使用者無法執行這些作業。
建立入門應用程式
建立名為「ContactManager」的 Razor Pages 應用程式
- 使用個別使用者帳戶建立應用程式。
- 將其命名為「ContactManager」,使命名空間符合範例中使用的命名空間。
-uld
會指定 LocalDB,而不是 SQLite
dotnet new webapp -o ContactManager -au Individual -uld
新增
Models/Contact.cs
:public class Contact { public int ContactId { get; set; } public string Name { get; set; } public string Address { get; set; } public string City { get; set; } public string State { get; set; } public string Zip { get; set; } [DataType(DataType.EmailAddress)] public string Email { get; set; } }
Scaffold
Contact
模型。建立初始移轉並更新資料庫:
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update
注意
根據預設,要安裝的 .NET 二進位檔架構代表目前執行的 OS 架構。 若要指定不同的 OS 架構,請參閱 dotnet tool install, --arch option。 如需詳細資訊,請參閱 GitHub 問題 dotnet/AspNetCore.Docs #29262。
如果您遇到 dotnet aspnet-codegenerator razorpage
命令的 BUG,請參閱這個 GitHub 問題。
- 更新
Pages/Shared/_Layout.cshtml
檔案中的 ContactManager 錨點:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
- 建立、編輯和刪除連絡人以測試應用程式
植入資料庫
將 SeedData 類別新增至 Data 資料夾:
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries
namespace ContactManager.Data
{
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
SeedDB(context, "0");
}
}
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com"
},
new Contact
{
Name = "Thorsten Weinrich",
Address = "5678 1st Ave W",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "thorsten@example.com"
},
new Contact
{
Name = "Yuhong Li",
Address = "9012 State st",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "yuhong@example.com"
},
new Contact
{
Name = "Jon Orton",
Address = "3456 Maple St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "jon@example.com"
},
new Contact
{
Name = "Diliana Alexieva-Bosseva",
Address = "7890 2nd Ave E",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "diliana@example.com"
}
);
context.SaveChanges();
}
}
}
從 Main
呼叫 SeedData.Initialize
:
using ContactManager.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
namespace ContactManager
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
SeedData.Initialize(services, "not used");
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
測試應用程式已植入資料庫。 如果連絡人資料庫中有任何資料列,則植入方法不會執行。
其他資源
- 教學課程:在 Azure App Service 中建置 ASP.NET Core 和 Azure SQL Database 應用程式
- ASP.NET Core 授權實驗室。 本實驗室會更詳細地說明本教學課程中介紹的安全性功能。
- ASP.NET Core 中的授權簡介
- 自訂原則式授權