通过授权保护的用户数据创建 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
创建:
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 用户 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
是 Identity 数据库中 AspNetUser
表中的用户 ID。 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、控制器或操作方法除外。 例如,具有 [AllowAnonymous]
或 [Authorize(PolicyName="MyPolicy")]
的 Razor Pages、控制器或操作方法使用应用的授权属性,而不是回退授权策略。
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;
}
向联系人添加管理员用户 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。 授权处理程序通常:
- 满足要求时调用
context.Succeed
。 - 未满足要求时返回
Task.CompletedTask
。 在未事先调用context.Success
或context.Fail
的情况下返回Task.CompletedTask
不是成功或失败,它允许运行其他授权处理程序。
如果需要显式失败,请调用 context.Fail。
该应用使联系人所有者可编辑/删除/创建自己的数据。 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 Identity,其基于 Entity Framework Core。 将处理程序注册到服务集合,以便通过依赖注入对 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 页面并添加操作要求类。
查看联系人操作要求类
查看 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
方法:- 将用户 ID 添加到
Contact
模型。 - 调用授权处理程序以验证用户是否有权创建联系人。
- 将用户 ID 添加到
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 Page 或控制器必须强制进行访问检查以确保数据的安全。
更新详细信息
更新详细信息视图,以便经理可以批准或拒绝联系人:
@*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 | 否 | 编辑并删除其数据。 |
manager@contoso.com | 是 | 编辑并删除其数据。 |
admin@contoso.com | 是 | 编辑并删除所有数据。 |
在管理员的浏览器中创建联系人。 复制管理员联系人的“删除”和“编辑”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; } } }
搭建
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 类添加到“数据”文件夹:
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();
测试应用是否为数据库设定种子。 如果联系人数据库中存在任何行,则 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 Identity 用户 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
是 Identity 数据库中 AspNetUser
表中的用户 ID。 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、控制器或操作方法除外。 例如,具有 [AllowAnonymous]
或 [Authorize(PolicyName="MyPolicy")]
的 Razor Pages、控制器或操作方法使用应用的身份验证属性,而不是回退身份验证策略。
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;
}
向联系人添加管理员用户 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。 授权处理程序通常:
- 满足要求时调用
context.Succeed
。 - 未满足要求时返回
Task.CompletedTask
。 在未事先调用context.Success
或context.Fail
的情况下返回Task.CompletedTask
不是成功或失败,它允许运行其他授权处理程序。
如果需要显式失败,请调用 context.Fail。
该应用使联系人所有者可编辑/删除/创建自己的数据。 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 Identity,其基于 Entity Framework 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.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 页面并添加操作要求类。
查看联系人操作要求类
查看 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
方法以执行以下操作:
- 将用户 ID 添加到
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 Page 或控制器必须强制进行访问检查以确保数据的安全。
更新详细信息
更新详细信息视图,以便经理可以批准或拒绝联系人:
@*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 | 否 | 编辑/删除自己的数据。 |
manager@contoso.com | 是 | 批准/拒绝和编辑/删除自己的数据。 |
admin@contoso.com | 是 | 批准/拒绝和编辑/删除所有数据。 |
在管理员的浏览器中创建联系人。 复制管理员联系人的“删除”和“编辑”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; } }
搭建
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 类添加到“数据”文件夹:
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>();
});
}
}
测试应用是否为数据库设定种子。 如果联系人数据库中存在任何行,则 seed 方法不会运行。
其他资源
- 教程:在 Azure 应用服务中生成 ASP.NET Core 和 Azure SQL 数据库应用
- ASP.NET Core 授权实验室。 此实验室更详细地介绍了本教程中所介绍的安全功能。
- ASP.NET Core 中的授权简介
- 基于自定义策略的授权