作者:Rick Anderson 和 Joe Audette
请参阅此 pdf
本教程演示如何使用受授权的用户数据创建 ASP.NET Core web 应用。 它显示的身份验证 (注册) 的用户的联系人列表已创建。 有三个安全组:
此文档中的图像与最新模板并不完全匹配。
在下图中,用户 Rick (rick@example.com
) 登录。 Rick 只能查看允许的联系人和编辑/删除/新建其联系人的链接。 只有最后一个记录,创建由 Rick,显示编辑并删除链接。 其他用户不会看到的最后一个记录,直到经理或管理员的状态更改为"已批准"。
在下图中,manager@contoso.com
已登录,并在管理器的角色中:
下图显示在管理器的联系人的详细信息视图:
批准并拒绝按钮仅显示经理和管理员。
在下图中,admin@contoso.com
以管理员角色登录:
管理员具有所有权限。 她可以读取、 编辑或删除任何联系,并更改联系人的状态。
通过创建该应用基架以下Contact
模型:
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标识用户 ID,以确保用户可以编辑其数据,而不是其他用户数据。 添加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
是从用户的 IDAspNetUser
表中标识数据库。 Status
字段确定是否可由普通用户查看联系人。
创建新的迁移并更新数据库:
dotnet ef migrations add userID_Status dotnet ef database update
追加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.AddControllers(config => { // using Microsoft.AspNetCore.Mvc.Authorization; // using Microsoft.AspNetCore.Authorization; var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); config.Filters.Add(new AuthorizeFilter(policy)); });
可以选择在 Razor 页面、 控制器或操作方法级别使用的身份验证禁用[AllowAnonymous]
属性。 设置默认身份验证策略以要求用户进行身份验证来保护新添加的 Razor 页面和控制器。 默认情况下所需的身份验证是比依赖于新的控制器和 Razor 页,以包含更安全[Authorize]
属性。
将AllowAnonymous添加到 "索引" 和 "隐私" 页,以便匿名用户在注册之前可以获取有关站点的信息。
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>(); }); }
更新Initialize
中的方法SeedData
类,以创建测试帐户:
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) { IdentityResult IR = null; var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>(); if (roleManager == null) { throw new Exception("roleManager null"); } if (!await roleManager.RoleExistsAsync(role)) { IR = await roleManager.CreateAsync(new IdentityRole(role)); } var userManager = serviceProvider.GetService<UserManager<IdentityUser>>(); 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; }
添加管理员用户 ID 和ContactStatus
到联系人。 先创建一个"已提交"和一个"已拒绝"的联系人。 将用户 ID 和状态添加到所有联系人。 只能有一个联系人所示:
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 },
创建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
满足的要求。Task.CompletedTask
时不符合要求。 Task.CompletedTask
不是成功或失败—它允许其他授权处理程序运行。如果你需要将显式失败,返回上下文。失败。
应用程序允许联系所有者到编辑/删除/创建他们自己的数据。 ContactIsOwnerAuthorizationHandler
不需要检查要求参数中传递该操作。
创建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; } } }
创建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
使用 ASP.NET Core标识,这基于实体框架核心。 注册服务集合的处理程序,以便它们可供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.AddControllers(config => { // using Microsoft.AspNetCore.Mvc.Authorization; // using Microsoft.AspNetCore.Authorization; var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); config.Filters.Add(new AuthorizeFilter(policy)); }); // Authorization handlers. services.AddScoped<IAuthorizationHandler, ContactIsOwnerAuthorizationHandler>(); services.AddSingleton<IAuthorizationHandler, ContactAdministratorsAuthorizationHandler>(); services.AddSingleton<IAuthorizationHandler, ContactManagerAuthorizationHandler>(); }
ContactAdministratorsAuthorizationHandler
和ContactManagerAuthorizationHandler
添加为单一实例。 它们是单一实例,因为它们不使用 EF 和所需的所有信息都位于Context
参数的HandleRequirementAsync
方法。
在本部分中,将更新 Razor 页面和添加操作要求类。
查看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 页面使用的服务的基类。 基类将初始化代码放在一个位置:
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
服务对授权处理程序的访问权限。UserManager
服务。ApplicationDbContext
。更新创建页面模型构造函数以使用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"); }
更新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(); } }
添加授权处理程序以验证的用户拥有联系人。 正在验证资源授权,因为[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"); } }
更新要使用授权处理程序来验证用户具有 delete 权限 contact 上的删除页面模型。
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>
警告
隐藏用户没有权限更改的数据中的链接不会保护应用。 隐藏链接使应用更加友好的用户显示唯一有效的链接。 用户可以 hack 生成的 Url 以调用编辑和删除操作上不拥有的数据。 Razor 页面或控制器必须强制执行访问检查,以保护数据。
更新的详细信息视图,使管理员可以批准或拒绝联系人:
@*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
后,用户将被重定向到 "拒绝访问" 页。如果你尚未设置设定为种子的用户帐户的密码,使用机密管理器工具设置密码:
选择强密码:使用八个或更多字符,并且至少使用一个大写字符、数字和符号。 例如,Passw0rd!
符合强密码要求。
执行以下命令从项目的文件夹,其中<PW>
的密码:
dotnet user-secrets set SeedUserPW <PW>
如果应用了联系人:
Contact
表。测试已完成的应用程序的简单方法是启动三个不同的浏览器 (或 incognito/InPrivate 会话)。 在一个浏览器中注册一个新用户 (例如, test@example.com
)。 登录到每个浏览器使用不同的用户。 验证以下操作:
Details
视图视图将显示批准并拒绝按钮。用户 | 由应用程序进行种子设定 | 选项 |
---|---|---|
test@example.com | No | 编辑/删除自己的数据。 |
manager@contoso.com | 是 | 批准/拒绝和编辑/删除拥有的数据。 |
admin@contoso.com | 是 | 批准/拒绝和编辑/删除所有数据。 |
在管理员的浏览器中创建联系人。 删除 URL 复制并编辑从管理员的联系信息。 将下面的链接粘贴到测试用户的浏览器以验证测试用户不能执行这些操作。
创建名为"ContactManager"Razor 页面应用
-uld
指定 LocalDB,而不是 SQLitedotnet new webapp -o ContactManager -au Individual -uld
添加模型/联系方式:
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; } }
基架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
如果使用 dotnet aspnet-codegenerator razorpage
命令时遇到 bug,请参阅此 GitHub 问题。
<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(); } } }
调用SeedData.Initialize
从Main
:
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>(); }); } }
测试应用程序设定数据库种子。 如果联系人 DB 中有任何行,则不会运行 seed 方法。
本教程演示如何使用受授权的用户数据创建 ASP.NET Core web 应用。 它显示的身份验证 (注册) 的用户的联系人列表已创建。 有三个安全组:
在下图中,用户 Rick (rick@example.com
) 登录。 Rick 只能查看允许的联系人和编辑/删除/新建其联系人的链接。 只有最后一个记录,创建由 Rick,显示编辑并删除链接。 其他用户不会看到的最后一个记录,直到经理或管理员的状态更改为"已批准"。
在下图中,manager@contoso.com
已登录,并在管理器的角色中:
下图显示在管理器的联系人的详细信息视图:
批准并拒绝按钮仅显示经理和管理员。
在下图中,admin@contoso.com
以管理员角色登录:
管理员具有所有权限。 她可以读取、 编辑或删除任何联系,并更改联系人的状态。
通过创建该应用基架以下Contact
模型:
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标识用户 ID,以确保用户可以编辑其数据,而不是其他用户数据。 添加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
是从用户的 IDAspNetUser
表中标识数据库。 Status
字段确定是否可由普通用户查看联系人。
创建新的迁移并更新数据库:
dotnet ef migrations add userID_Status dotnet ef database update
追加AddRoles添加角色服务:
public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>();
设置默认身份验证策略以要求用户进行身份验证:
public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddMvc(config => { // using Microsoft.AspNetCore.Mvc.Authorization; // using Microsoft.AspNetCore.Authorization; var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); config.Filters.Add(new AuthorizeFilter(policy)); }) .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
可以选择在 Razor 页面、 控制器或操作方法级别使用的身份验证禁用[AllowAnonymous]
属性。 设置默认身份验证策略以要求用户进行身份验证来保护新添加的 Razor 页面和控制器。 默认情况下所需的身份验证是比依赖于新的控制器和 Razor 页,以包含更安全[Authorize]
属性。
添加AllowAnonymous到索引中,因此匿名用户可以获取有关站点的信息注册有关,和联系人页面。
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; namespace ContactManager.Pages { [AllowAnonymous] public class IndexModel : PageModel { 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 = CreateWebHostBuilder(args).Build(); using (var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; 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"]; try { SeedData.Initialize(services, testUserPw).Wait(); } catch (Exception ex) { var logger = services.GetRequiredService<ILogger<Program>>(); logger.LogError(ex.Message, "An error occurred seeding the DB."); } } host.Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); }
更新Initialize
中的方法SeedData
类,以创建测试帐户:
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 }; await userManager.CreateAsync(user, testUserPw); } return user.Id; } private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider, string uid, string role) { IdentityResult IR = null; var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>(); if (roleManager == null) { throw new Exception("roleManager null"); } if (!await roleManager.RoleExistsAsync(role)) { IR = await roleManager.CreateAsync(new IdentityRole(role)); } var userManager = serviceProvider.GetService<UserManager<IdentityUser>>(); 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; }
添加管理员用户 ID 和ContactStatus
到联系人。 先创建一个"已提交"和一个"已拒绝"的联系人。 将用户 ID 和状态添加到所有联系人。 只能有一个联系人所示:
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 },
创建一个授权文件夹,并在其中创建一个 ContactIsOwnerAuthorizationHandler
类。 ContactIsOwnerAuthorizationHandler
验证对资源进行操作的用户拥有的资源。
using System.Threading.Tasks; using ContactManager.Data; using ContactManager.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.AspNetCore.Identity; 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.FromResult(0) if targeting a version of // .NET Framework older than 4.6: return Task.CompletedTask; } // If we're 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
满足的要求。Task.CompletedTask
时不符合要求。 Task.CompletedTask
不是成功或失败—它允许其他授权处理程序运行。如果你需要将显式失败,返回上下文。失败。
应用程序允许联系所有者到编辑/删除/创建他们自己的数据。 ContactIsOwnerAuthorizationHandler
不需要检查要求参数中传递该操作。
创建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; } } }
创建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
使用 ASP.NET Core标识,这基于实体框架核心。 注册服务集合的处理程序,以便它们可供ContactsController
通过依赖关系注入。 将以下代码添加到末尾ConfigureServices
:
public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddMvc(config => { // using Microsoft.AspNetCore.Mvc.Authorization; // using Microsoft.AspNetCore.Authorization; var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); config.Filters.Add(new AuthorizeFilter(policy)); }) .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); // Authorization handlers. services.AddScoped<IAuthorizationHandler, ContactIsOwnerAuthorizationHandler>(); services.AddSingleton<IAuthorizationHandler, ContactAdministratorsAuthorizationHandler>(); services.AddSingleton<IAuthorizationHandler, ContactManagerAuthorizationHandler>(); }
ContactAdministratorsAuthorizationHandler
和ContactManagerAuthorizationHandler
添加为单一实例。 它们是单一实例,因为它们不使用 EF 和所需的所有信息都位于Context
参数的HandleRequirementAsync
方法。
在本部分中,将更新 Razor 页面和添加操作要求类。
查看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 页面使用的服务的基类。 基类将初始化代码放在一个位置:
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
服务对授权处理程序的访问权限。UserManager
服务。ApplicationDbContext
。更新创建页面模型构造函数以使用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 new ChallengeResult(); } Context.Contact.Add(Contact); await Context.SaveChangesAsync(); return RedirectToPage("./Index"); }
更新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(); } }
添加授权处理程序以验证的用户拥有联系人。 正在验证资源授权,因为[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 new ChallengeResult(); } 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 new ChallengeResult(); } 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"); } private bool ContactExists(int id) { return Context.Contact.Any(e => e.ContactId == id); } }
更新要使用授权处理程序来验证用户具有 delete 权限 contact 上的删除页面模型。
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 new ChallengeResult(); } return Page(); } public async Task<IActionResult> OnPostAsync(int id) { Contact = await Context.Contact.FindAsync(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 new ChallengeResult(); } Context.Contact.Remove(Contact); await Context.SaveChangesAsync(); return RedirectToPage("./Index"); } }
目前,此 UI 显示编辑和删除的用户无法修改的联系人的链接。
注入中的授权服务views/_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>
警告
隐藏用户没有权限更改的数据中的链接不会保护应用。 隐藏链接使应用更加友好的用户显示唯一有效的链接。 用户可以 hack 生成的 Url 以调用编辑和删除操作上不拥有的数据。 Razor 页面或控制器必须强制执行访问检查,以保护数据。
更新的详细信息视图,使管理员可以批准或拒绝联系人:
@*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-success">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 new ChallengeResult(); } 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 new ChallengeResult(); } contact.Status = status; Context.Contact.Update(contact); await Context.SaveChangesAsync(); return RedirectToPage("./Index"); } }
请参阅本期有关的信息:
如果你尚未设置设定为种子的用户帐户的密码,使用机密管理器工具设置密码:
选择强密码:使用八个或更多字符,并且至少使用一个大写字符、数字和符号。 例如,Passw0rd!
符合强密码要求。
执行以下命令从项目的文件夹,其中<PW>
的密码:
dotnet user-secrets set SeedUserPW <PW>
删除和更新数据库
dotnet ef database drop -f dotnet ef database update
重新启动应用以设定数据库种子。
测试已完成的应用程序的简单方法是启动三个不同的浏览器 (或 incognito/InPrivate 会话)。 在一个浏览器中注册一个新用户 (例如, test@example.com
)。 登录到每个浏览器使用不同的用户。 验证以下操作:
Details
视图视图将显示批准并拒绝按钮。用户 | 由应用程序进行种子设定 | 选项 |
---|---|---|
test@example.com | No | 编辑/删除自己的数据。 |
manager@contoso.com | 是 | 批准/拒绝和编辑/删除拥有的数据。 |
admin@contoso.com | 是 | 批准/拒绝和编辑/删除所有数据。 |
在管理员的浏览器中创建联系人。 删除 URL 复制并编辑从管理员的联系信息。 将下面的链接粘贴到测试用户的浏览器以验证测试用户不能执行这些操作。
创建名为"ContactManager"Razor 页面应用
-uld
指定 LocalDB,而不是 SQLitedotnet new webapp -o ContactManager -au Individual -uld
添加模型/联系方式:
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; } }
基架Contact
模型。
创建初始迁移并更新数据库:
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
更新ContactManager中的定位点pages/_layout.cshtml文件:
<a asp-page="/Contacts/Index" class="navbar-brand">ContactManager</a>
测试应用程序的创建、 编辑和删除联系人
添加SeedData类来数据文件夹。
调用SeedData.Initialize
从Main
:
public class Program { public static void Main(string[] args) { var host = CreateWebHostBuilder(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 IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); }
测试应用程序设定数据库种子。 如果联系人 DB 中有任何行,则不会运行 seed 方法。