作者:Rick Anderson 和 Joe Audette
本教程演示如何使用受授权保护的用户数据创建 ASP.NET Core Web 应用。 它显示由经过身份验证的(已注册)用户创建的联系人列表。 该应用支持三个安全组:
- 已注册的用户 可以查看所有 已批准 的数据,并且可以编辑/删除其自己的数据。
- 经理可以批准或拒绝联系人数据。 只有标记为 “已批准 ”的联系人对用户可见。
- 管理员可以批准/拒绝和编辑/删除任何数据。
Note
本文中的图像与最新模板不完全匹配。
在下图中,用户 rick@contoso.com 登录到 Web 应用。 此用户只能查看 已批准 联系人,以及这些联系人的 编辑/删除/新建联系人 链接。 在此视图中,只有最后一条记录(由此用户创建)显示 “编辑 和 删除 ”链接。 在经理或管理员批准记录之前,其他用户不会看到最后一条记录。
在下一个映像中, 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:允许管理员批准或拒绝联系人以及编辑/删除联系人。
Prerequisites
本教程是高级教程。 你应该熟悉以下内容:
起始应用和完成的应用
Tip
你可以使用 git sparse-checkout 命令仅下载示例子文件夹。
例如:
git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse
cd AspNetCore.Docs
git sparse-checkout init --cone
git sparse-checkout set aspnetcore/security/authorization/secure-data/samples
入门应用
运行应用,点击 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 是 AspNetUser 数据库中 Identity 表中的用户 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、控制器或操作方法除外。 例如,具有 Razor 或 [AllowAnonymous] 的 [Authorize(PolicyName="MyPolicy")] 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);
}
创建测试帐户并更新联系人
通过更新 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)
{
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.Succeed或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使用基于 Entity Framework Core 构建的 ASP.NET Core Identity。 将这些处理程序注册到服务集合中,以便 ContactsController 可通过 依赖注入 使用。 将以下代码添加到 ConfigureServices末尾:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
// requires using Microsoft.Extensions.Configuration;
// Set password with the Secret Manager tool.
// dotnet user-secrets set SeedUserPW <pw>
var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");
await SeedData.Initialize(services, testUserPw);
}
ContactAdministratorsAuthorizationHandler 和 ContactManagerAuthorizationHandler 作为单例添加。 它们是单例,因为它们不使用 Entity Framework,所需的所有信息都在 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 页面创建基类
创建一个包含在联系人 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。
更新创建模型
更新创建页面模型。
- 定义使用基类的
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>
Warning
隐藏不具有更改数据权限的用户的链接不会保护应用的安全。 隐藏链接通过仅显示有效链接使应用更加用户友好。 用户可以通过攻击生成的 URL 来对其不拥有的数据调用编辑和删除操作。 Razor 页或控制器必须强制实施访问检查以保护数据。
更新详细信息
更新详细信息视图,以便经理可以批准或拒绝联系人:
@*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");
}
}
添加或删除用户角色
通过更新用户的角色分配,可以控制用户可用的权限。 从角色中删除用户并降低更改数据(例如编辑或删除联系人)的能力。 将用户添加到角色并增加其进行全局更改的权限。 还可以使用角色分配来限制用户参与,例如在聊天对话中将用户静音。
有关更多信息,请参阅 GitHub dotnet/aspnetcore 问题 #8502 - 禁言或移除用户的权限。管理员更改。
质询与禁止之间的区别
此应用将默认策略设置为需要经过身份验证的用户。 以下代码允许匿名用户。 允许匿名用户显示挑战与 Forbid 之间的差异。
[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后,会将用户重定向到登录页。 - 如果用户已经过身份验证,但未授权,则返回
ForbidResult。 返回ForbidResult时,用户将被重定向到访问被拒绝页面。
测试已完成的应用
如果尚未为种子用户帐户设置密码,请使用机密管理器工具设置密码:
选择一个强密码:
- 长度至少为 12 个字符,但 14 个字符以上更好。
- 大写字母、小写字母、数字和符号的组合。
- 不是字典中能找到的词,也不是人物、角色、产品或组织的名称。
- 与以前的密码明显不同。
- 你很容易记住,但别人很难猜到。 请考虑使用一个令人难忘的短语,例如
6MonkeysRLooking^。
从project的文件夹执行以下命令,其中
<PW>是密码:dotnet user-secrets set SeedUserPW <PW>
如果应用有联系人:
- 删除
Contact表中的所有记录。 - 重启应用以初始化数据库。
测试已完成应用的一种简单方法是启动三个不同的浏览器(或 incognito/InPrivate 会话)。 在一个浏览器中,注册新用户(例如 test@contoso.com)。 使用不同用户登录每个浏览器。 验证以下操作:
- 已注册的用户可以查看所有 已批准的 联系人数据。
- 已注册的用户可以编辑/删除他们自己的数据。
- 经理可以批准/拒绝联系人数据。
Details视图显示“批准”和“拒绝”按钮。 - 管理员可以批准/拒绝和编辑/删除任何数据。
| User | 批准/拒绝联系人 | 选项 |
|---|---|---|
test@contoso.com |
No | 编辑并删除其数据。 |
manager@contoso.com |
Yes | 编辑并删除其数据。 |
admin@contoso.com |
Yes | 编辑并删除所有数据。 |
在管理员的浏览器中创建联系人。 请从管理员联系人中复制用于“删除”和“编辑”的URL。 将这些链接粘贴到测试用户的浏览器中,并验证测试用户无法执行这些操作。
创建初学者应用
创建 Razor Pages 应用:
- 使用 个人帐户创建应用。
- 将应用 命名为 ContactManager,因此命名空间与示例中使用的命名空间匹配。
- 使用
-uld标志指定 LocalDB 而不是 SQLite。
dotnet new webapp -o ContactManager -au Individual -uld添加 Models/Contact.cs 文件:
using 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 updateNote
默认情况下,要安装的.NET二进制文件的体系结构表示当前运行的操作系统体系结构。 若要指定其他架构,请参阅如何使用
dotnet tool install命令并配合 '--arch' 选项。 有关详细信息,请参阅 GitHub dotnet/aspnetcore.docs 议题 #29262 - 在 Apple Silicon 上添加“-a arm64”。更新 Pages/Shared/_Layout.cshtml 文件中的 ContactManager 锚点:
<a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>通过创建、编辑和删除联系人来测试应用
初始化数据库数据
将 SeedData 类添加到 Data 文件夹中:
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries
namespace ContactManager.Data
{
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw="")
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
SeedDB(context, testUserPw);
}
}
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com"
},
new Contact
{
Name = "Thorsten Weinrich",
Address = "5678 1st Ave W",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "thorsten@example.com"
},
new Contact
{
Name = "Yuhong Li",
Address = "9012 State st",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "yuhong@example.com"
},
new Contact
{
Name = "Jon Orton",
Address = "3456 Maple St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "jon@example.com"
},
new Contact
{
Name = "Diliana Alexieva-Bosseva",
Address = "7890 2nd Ave E",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "diliana@example.com"
}
);
context.SaveChanges();
}
}
}
从Program.cs文件中调用SeedData.Initialize方法:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
await SeedData.Initialize(services);
}
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
测试应用并确认数据库已填充初始数据。 如果联系人数据库中有任何行,则种子方法不会运行。
本教程演示如何使用受授权保护的用户数据创建 ASP.NET Core Web 应用。 它显示经过身份验证(注册)的用户创建的联系人列表。 有三个安全组:
- 已注册的用户可以查看所有已批准的数据,并可以编辑/删除自己的数据。
- 经理可以批准或拒绝联系人数据。 只有已批准的联系人才对用户可见。
- 管理员可以批准/拒绝和编辑/删除任何数据。
本文档中的图像与最新的模板不完全匹配。
在下图中,用户 Rick (rick@example.com) 已登录。 Rick 只能查看已批准的联系人,并为其联系人编辑删除/创建新链接。 只有 Rick 创建的最后一条记录才会显示编辑和删除链接。 在经理或管理员将状态更改为“已批准”后,其他用户才能看到最后一条记录。
在下图中,manager@contoso.com 已登录并扮演经理的角色:
下图显示了经理的联系人详细信息视图:
“批准”和“拒绝”按钮仅为经理和管理员显示。
在下图中,admin@contoso.com 已登录并扮演管理员的角色:
管理员拥有所有权限。 她可以读取/编辑/删除任何联系人并更改联系人的状态。
该应用是通过基于以下模型搭建脚手架创建的:Contact
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:允许管理员执行以下操作:- 批准或拒绝联系人
- 编辑并删除联系人
Prerequisites
本教程是高级教程。 你应该熟悉以下内容:
起始应用和完成的应用
入门应用
运行应用,点击 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 是 AspNetUser 数据库中 Identity 表中的用户 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 类创建两个帐户:管理员和经理。 使用机密管理器工具为这些帐户设置密码。 从project目录设置密码(包含 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)
{
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。 在未事先调用Task.CompletedTask或context.Success的情况下返回context.Fail不是成功或失败,它允许运行其他授权处理程序。
如果需要显式地将其标记为失败,请调用 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使用基于 Entity Framework Core 构建的 ASP.NET Core Identity。 将这些处理程序注册到服务集合中,以便 ContactsController 可通过 依赖注入 使用。 将以下代码添加到 ConfigureServices末尾:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// Authorization handlers.
services.AddScoped<IAuthorizationHandler,
ContactIsOwnerAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler,
ContactAdministratorsAuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler,
ContactManagerAuthorizationHandler>();
}
ContactAdministratorsAuthorizationHandler 和 ContactManagerAuthorizationHandler 作为单例添加。 它们是单一实例,因为不使用 EF,并且所需的全部信息都位于 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 页面创建基类
创建一个包含在联系人 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。
更新创建模型
更新“创建”页模型构造函数以使用 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 语句。
更新 中的编辑和删除链接,以便仅为具有相应权限的用户呈现它们:
@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>
Warning
隐藏不具有更改数据权限的用户的链接不会保护应用的安全。 隐藏链接通过仅显示有效链接使应用更加用户友好。 用户可以通过攻击生成的 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后,会将用户重定向到登录页。 - 如果用户已经过身份验证,但未授权,则返回
ForbidResult。 返回ForbidResult时,用户将被重定向到访问被拒绝页面。
测试已完成的应用
如果尚未为种子用户帐户设置密码,请使用机密管理器工具设置密码:
选择强密码:使用八个或更多字符,并且至少使用一个大写字符、数字和符号。 例如,
Passw0rd!满足强密码要求。从project的文件夹执行以下命令,其中
<PW>是密码:dotnet user-secrets set SeedUserPW <PW>
如果应用有联系人:
- 删除
Contact表中的所有记录。 - 重启应用以初始化数据库。
测试已完成应用的一种简单方法是启动三个不同的浏览器(或 incognito/InPrivate 会话)。 在一个浏览器中,注册新用户(例如 test@example.com)。 使用不同用户登录每个浏览器。 验证以下操作:
- 已注册的用户可以查看所有已批准的联系人数据。
- 已注册的用户可以编辑/删除他们自己的数据。
- 经理可以批准/拒绝联系人数据。
Details视图显示“批准”和“拒绝”按钮。 - 管理员可以批准/拒绝和编辑/删除任何数据。
| User | 由应用设定种子 | 选项 |
|---|---|---|
| test@example.com | No | 编辑/删除自己的数据。 |
| manager@contoso.com | Yes | 批准/拒绝和编辑/删除自己的数据。 |
| admin@contoso.com | Yes | 批准/拒绝和编辑/删除所有数据。 |
在管理员的浏览器中创建联系人。 请从管理员联系人中复制用于“删除”和“编辑”的URL。 将这些链接粘贴到测试用户的浏览器中,以验证测试用户是否无法执行这些操作。
创建初学者应用
创建名为“ContactManager”的 Razor Pages 应用
- 使用 个人帐户创建应用。
- 将其命名为“ContactManager”,使命名空间与该示例中使用的命名空间匹配。
-
-uld指定 LocalDB,而不是 SQLite
dotnet new webapp -o ContactManager -au Individual -uld添加
Models/Contact.cs:public class Contact { public int ContactId { get; set; } public string Name { get; set; } public string Address { get; set; } public string City { get; set; } public string State { get; set; } public string Zip { get; set; } [DataType(DataType.EmailAddress)] public string Email { get; set; } }搭建
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
Note
默认情况下,要安装的.NET二进制文件的体系结构表示当前运行的操作系统体系结构。
若要指定其他架构,请参阅如何使用 dotnet tool install 命令并配合 '--arch' 选项。
有关详细信息,请参阅 GitHub dotnet/aspnetcore.docs 议题 #29262 - 在 Apple Silicon 上添加“-a arm64”。
如果使用 dotnet aspnet-codegenerator razorpage 命令遇到 bug,请参阅 GitHub问题。
- 更新
Pages/Shared/_Layout.cshtml文件中的 ContactManager 锚点:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
- 通过创建、编辑和删除联系人来测试应用
初始化数据库数据
将 SeedData 类添加到 Data 文件夹中:
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries
namespace ContactManager.Data
{
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
SeedDB(context, "0");
}
}
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com"
},
new Contact
{
Name = "Thorsten Weinrich",
Address = "5678 1st Ave W",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "thorsten@example.com"
},
new Contact
{
Name = "Yuhong Li",
Address = "9012 State st",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "yuhong@example.com"
},
new Contact
{
Name = "Jon Orton",
Address = "3456 Maple St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "jon@example.com"
},
new Contact
{
Name = "Diliana Alexieva-Bosseva",
Address = "7890 2nd Ave E",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "diliana@example.com"
}
);
context.SaveChanges();
}
}
}
从 Main 调用 SeedData.Initialize:
using ContactManager.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
namespace ContactManager
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ApplicationDbContext>();
context.Database.Migrate();
SeedData.Initialize(services, "not used");
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
测试应用是否已对数据库进行初始化。 如果联系人数据库中存在任何行,则 seed 方法不会运行。