使用受授权的用户数据创建 ASP.NET Core 应用Create an ASP.NET Core app with user data protected by authorization

作者:Rick AndersonJoe AudetteBy Rick Anderson and Joe Audette

请参阅此 PDF ASP.NET Core MVC 版本。See this PDF for the ASP.NET Core MVC version. 本教程的 ASP.NET Core 1.1 版本是文件夹。The ASP.NET Core 1.1 version of this tutorial is in this folder. ASP.NET Core 示例是在 1.1示例The 1.1 ASP.NET Core sample is in the samples.

请参阅此 pdfSee this pdf

本教程演示如何使用受授权的用户数据创建 ASP.NET Core web 应用。This tutorial shows how to create an ASP.NET Core web app with user data protected by authorization. 它显示的身份验证 (注册) 的用户的联系人列表已创建。It displays a list of contacts that authenticated (registered) users have created. 有三个安全组:There are three security groups:

  • 注册用户可以查看所有已批准的数据还可以编辑/删除其自己的数据。Registered users can view all the approved data and can edit/delete their own data.
  • 管理器可以批准或拒绝的联系人数据。Managers can approve or reject contact data. 仅已批准的联系人是对用户可见。Only approved contacts are visible to users.
  • 管理员可以批准/拒绝和编辑/删除的任何数据。Administrators can approve/reject and edit/delete any data.

此文档中的图像与最新模板并不完全匹配。The images in this document don't exactly match the latest templates.

在下图中,用户 Rick (rick@example.com) 登录。In the following image, user Rick (rick@example.com) is signed in. Rick 只能查看允许的联系人和编辑/删除/新建其联系人的链接。Rick can only view approved contacts and Edit/Delete/Create New links for his contacts. 只有最后一个记录,创建由 Rick,显示编辑删除链接。Only the last record, created by Rick, displays Edit and Delete links. 其他用户不会看到的最后一个记录,直到经理或管理员的状态更改为"已批准"。Other users won't see the last record until a manager or administrator changes the status to "Approved".

屏幕截图显示 Rick 登录

在下图中,manager@contoso.com 已登录,并在管理器的角色中:In the following image, manager@contoso.com is signed in and in the manager's role:

屏幕截图显示manager@contoso.com登录

下图显示在管理器的联系人的详细信息视图:The following image shows the managers details view of a contact:

联系人的经理的视图

批准拒绝按钮仅显示经理和管理员。The Approve and Reject buttons are only displayed for managers and administrators.

在下图中,admin@contoso.com 以管理员角色登录:In the following image, admin@contoso.com is signed in and in the administrator's role:

屏幕截图显示admin@contoso.com登录

管理员具有所有权限。The administrator has all privileges. 她可以读取、 编辑或删除任何联系,并更改联系人的状态。She can read/edit/delete any contact and change the status of contacts.

通过创建该应用基架以下Contact模型:The app was created by scaffolding the following Contact model:

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; }
}

此示例包含以下授权处理程序:The sample contains the following authorization handlers:

  • ContactIsOwnerAuthorizationHandler:确保用户只能编辑其数据。ContactIsOwnerAuthorizationHandler: Ensures that a user can only edit their data.
  • ContactManagerAuthorizationHandler:允许经理批准或拒绝联系人。ContactManagerAuthorizationHandler: Allows managers to approve or reject contacts.
  • ContactAdministratorsAuthorizationHandler:允许管理员批准或拒绝联系人以及编辑/删除联系人。ContactAdministratorsAuthorizationHandler: Allows administrators to approve or reject contacts and to edit/delete contacts.

系统必备Prerequisites

本教程被高级。This tutorial is advanced. 您应熟悉:You should be familiar with:

Starter 和已完成应用程序The starter and completed app

下载完成应用。Download the completed app. 测试已完成的应用,使你熟悉其安全功能。Test the completed app so you become familiar with its security features.

入门级应用The starter app

下载初学者应用。Download the starter app.

运行应用,点击ContactManager链接,并验证是否可以创建、 编辑和删除联系人。Run the app, tap the ContactManager link, and verify you can create, edit, and delete a contact.

保护用户数据Secure user data

以下部分介绍了所有主要的步骤以创建安全的用户数据应用程序。The following sections have all the major steps to create the secure user data app. 您可能会发现引用的已完成项目很有帮助。You may find it helpful to refer to the completed project.

将绑定到用户的联系人数据Tie the contact data to the user

使用 ASP.NET标识用户 ID,以确保用户可以编辑其数据,而不是其他用户数据。Use the ASP.NET Identity user ID to ensure users can edit their data, but not other users data. 添加OwnerIDContactStatusContact模型:Add OwnerID and ContactStatus to the Contact model:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string OwnerID { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID 是从用户的 IDAspNetUser表中标识数据库。OwnerID is the user's ID from the AspNetUser table in the Identity database. Status字段确定是否可由普通用户查看联系人。The Status field determines if a contact is viewable by general users.

创建新的迁移并更新数据库:Create a new migration and update the database:

dotnet ef migrations add userID_Status
dotnet ef database update

将角色服务添加到标识Add Role services to Identity

追加AddRoles添加角色服务:Append AddRoles to add Role services:

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>();

需要身份验证的用户Require authenticated users

设置默认身份验证策略以要求用户进行身份验证:Set the default authentication policy to require users to be authenticated:

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddControllers(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

可以选择在 Razor 页面、 控制器或操作方法级别使用的身份验证禁用[AllowAnonymous]属性。You can opt out of authentication at the Razor Page, controller, or action method level with the [AllowAnonymous] attribute. 设置默认身份验证策略以要求用户进行身份验证来保护新添加的 Razor 页面和控制器。Setting the default authentication policy to require users to be authenticated protects newly added Razor Pages and controllers. 默认情况下所需的身份验证是比依赖于新的控制器和 Razor 页,以包含更安全[Authorize]属性。Having authentication required by default is more secure than relying on new controllers and Razor Pages to include the [Authorize] attribute.

AllowAnonymous添加到 "索引" 和 "隐私" 页,以便匿名用户在注册之前可以获取有关站点的信息。Add AllowAnonymous to the Index and Privacy pages so anonymous users can get information about the site before they register.

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()
        {

        }
    }
}

配置测试帐户Configure the test account

SeedData类创建两个帐户: 管理员和管理员。The SeedData class creates two accounts: administrator and manager. 使用机密管理器工具设置这些帐户的密码。Use the Secret Manager tool to set a password for these accounts. 从项目目录中设置密码 (目录包含Program.cs):Set the password from the project directory (the directory containing Program.cs):

dotnet user-secrets set SeedUserPW <PW>

如果未指定强密码,将引发异常时SeedData.Initialize调用。If a strong password is not specified, an exception is thrown when SeedData.Initialize is called.

更新Main使用测试密码:Update Main to use the test password:

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>();
            });
}

创建测试帐户和更新联系人Create the test accounts and update the contacts

更新Initialize中的方法SeedData类,以创建测试帐户:Update the Initialize method in the SeedData class to create the test accounts:

public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser {
            UserName = UserName,
            EmailConfirmed = true
        };
        await userManager.CreateAsync(user, testUserPw);
    }

    if (user == null)
    {
        throw new Exception("The password is probably not strong enough!");
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    IdentityResult IR = null;
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByIdAsync(uid);

    if(user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }
    
    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

添加管理员用户 ID 和ContactStatus到联系人。Add the administrator user ID and ContactStatus to the contacts. 先创建一个"已提交"和一个"已拒绝"的联系人。Make one of the contacts "Submitted" and one "Rejected". 将用户 ID 和状态添加到所有联系人。Add the user ID and status to all the contacts. 只能有一个联系人所示:Only one contact is shown:

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
        },

创建所有者、 经理和管理员授权处理程序Create owner, manager, and administrator authorization handlers

创建ContactIsOwnerAuthorizationHandler类中授权文件夹。Create a ContactIsOwnerAuthorizationHandler class in the Authorization folder. ContactIsOwnerAuthorizationHandler验证对资源进行操作的用户拥有的资源。The ContactIsOwnerAuthorizationHandler verifies that the user acting on a resource owns the resource.

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调用上下文。成功当前经过身份验证的用户是否联系所有者。The ContactIsOwnerAuthorizationHandler calls context.Succeed if the current authenticated user is the contact owner. 授权处理程序通常:Authorization handlers generally:

  • 返回context.Succeed满足的要求。Return context.Succeed when the requirements are met.
  • 返回Task.CompletedTask时不符合要求。Return Task.CompletedTask when requirements aren't met. Task.CompletedTask 不是成功或失败—它允许其他授权处理程序运行。Task.CompletedTask is not success or failure—it allows other authorization handlers to run.

如果你需要将显式失败,返回上下文。失败If you need to explicitly fail, return context.Fail.

应用程序允许联系所有者到编辑/删除/创建他们自己的数据。The app allows contact owners to edit/delete/create their own data. ContactIsOwnerAuthorizationHandler 不需要检查要求参数中传递该操作。ContactIsOwnerAuthorizationHandler doesn't need to check the operation passed in the requirement parameter.

创建管理器授权处理程序Create a manager authorization handler

创建ContactManagerAuthorizationHandler类中授权文件夹。Create a ContactManagerAuthorizationHandler class in the Authorization folder. ContactManagerAuthorizationHandler验证对资源进行操作的用户是管理员。The ContactManagerAuthorizationHandler verifies the user acting on the resource is a manager. 只有经理们才可以批准或拒绝内容的更改 (新的或已更改)。Only managers can approve or reject content changes (new or changed).

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;
        }
    }
}

创建管理员授权处理程序Create an administrator authorization handler

创建ContactAdministratorsAuthorizationHandler类中授权文件夹。Create a ContactAdministratorsAuthorizationHandler class in the Authorization folder. ContactAdministratorsAuthorizationHandler验证对资源进行操作的用户是管理员。The ContactAdministratorsAuthorizationHandler verifies the user acting on the resource is an administrator. 管理员可以执行所有操作。Administrator can do all operations.

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;
        }
    }
}

注册授权处理程序Register the authorization handlers

Entity Framework Core 使用 AddScoped 的服务必须使用注册以进行依赖关系注入Services using Entity Framework Core must be registered for dependency injection using AddScoped. ContactIsOwnerAuthorizationHandler使用 ASP.NET Core标识,这基于实体框架核心。The ContactIsOwnerAuthorizationHandler uses ASP.NET Core Identity, which is built on Entity Framework Core. 注册服务集合的处理程序,以便它们可供ContactsController通过依赖关系注入Register the handlers with the service collection so they're available to the ContactsController through dependency injection. 将以下代码添加到末尾ConfigureServices:Add the following code to the end of ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddControllers(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });
    // Authorization handlers.
    services.AddScoped<IAuthorizationHandler,
                          ContactIsOwnerAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactManagerAuthorizationHandler>();
}

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler添加为单一实例。ContactAdministratorsAuthorizationHandler and ContactManagerAuthorizationHandler are added as singletons. 它们是单一实例,因为它们不使用 EF 和所需的所有信息都位于Context参数的HandleRequirementAsync方法。They're singletons because they don't use EF and all the information needed is in the Context parameter of the HandleRequirementAsync method.

支持授权Support authorization

在本部分中,将更新 Razor 页面和添加操作要求类。In this section, you update the Razor Pages and add an operations requirements class.

查看联系人操作要求类Review the contact operations requirements class

查看ContactOperations类。Review the ContactOperations class. 此类包含要求应用支持:This class contains the requirements the app supports:

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 页面的基类Create a base class for the Contacts Razor Pages

创建一个包含在联系人 Razor 页面使用的服务的基类。Create a base class that contains the services used in the contacts Razor Pages. 基类将初始化代码放在一个位置:The base class puts the initialization code in one location:

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;
        } 
    }
}

前面的代码:The preceding code:

  • 添加IAuthorizationService服务对授权处理程序的访问权限。Adds the IAuthorizationService service to access to the authorization handlers.
  • 将标识添加UserManager服务。Adds the Identity UserManager service.
  • 添加 ApplicationDbContextAdd the ApplicationDbContext.

更新 CreateModelUpdate the CreateModel

更新创建页面模型构造函数以使用DI_BasePageModel基类:Update the create page model constructor to use the DI_BasePageModel base class:

public class CreateModel : DI_BasePageModel
{
    public CreateModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

更新CreateModel.OnPostAsync方法:Update the CreateModel.OnPostAsync method to:

  • 添加到的用户 IDContact模型。Add the user ID to the Contact model.
  • 调用授权处理程序,以验证用户有权创建联系人。Call the authorization handler to verify the user has permission to create contacts.
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");
}

更新 IndexModelUpdate the IndexModel

更新OnGetAsync方法,以便仅被批准的联系人显示为普通用户:Update the OnGetAsync method so only approved contacts are shown to general users:

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();
    }
}

更新 EditModelUpdate the EditModel

添加授权处理程序以验证的用户拥有联系人。Add an authorization handler to verify the user owns the contact. 正在验证资源授权,因为[Authorize]属性不能满足。Because resource authorization is being validated, the [Authorize] attribute is not enough. 评估属性时,应用程序不具有对资源的访问。The app doesn't have access to the resource when attributes are evaluated. 基于资源的授权必须是命令性。Resource-based authorization must be imperative. 应用程序页面模型中加载或加载处理程序本身内获得资源的访问权限,则必须执行检查。Checks must be performed once the app has access to the resource, either by loading it in the page model or by loading it within the handler itself. 您经常访问的资源,通过传入的资源键。You frequently access the resource by passing in the resource key.

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");
    }
}

更新 DeleteModelUpdate the DeleteModel

更新要使用授权处理程序来验证用户具有 delete 权限 contact 上的删除页面模型。Update the delete page model to use the authorization handler to verify the user has delete permission on the contact.

public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

将授权服务注入到视图Inject the authorization service into the views

目前,此 UI 显示编辑和删除的用户无法修改的联系人的链接。Currently, the UI shows edit and delete links for contacts the user can't modify.

将授权服务注入Pages/_ViewImports cshtml文件,使其可供所有视图使用:Inject the authorization service in the Pages/_ViewImports.cshtml file so it's available to all views:

@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语句。The preceding markup adds several using statements.

更新编辑删除中的链接Pages/Contacts/Index.cshtml以便仅在呈现具有适当权限的用户:Update the Edit and Delete links in Pages/Contacts/Index.cshtml so they're only rendered for users with the appropriate permissions:

@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>

警告

隐藏用户没有权限更改的数据中的链接不会保护应用。Hiding links from users that don't have permission to change data doesn't secure the app. 隐藏链接使应用更加友好的用户显示唯一有效的链接。Hiding links makes the app more user-friendly by displaying only valid links. 用户可以 hack 生成的 Url 以调用编辑和删除操作上不拥有的数据。Users can hack the generated URLs to invoke edit and delete operations on data they don't own. Razor 页面或控制器必须强制执行访问检查,以保护数据。The Razor Page or controller must enforce access checks to secure the data.

更新详细信息Update Details

更新的详细信息视图,使管理员可以批准或拒绝联系人:Update the details view so managers can approve or reject contacts:

        @*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>

更新详细信息页模型:Update the details page model:

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");
    }
}

添加或删除角色对用户的Add or remove a user to a role

请参阅本期有关的信息:See this issue for information on:

  • 从用户删除的特权。Removing privileges from a user. 例如,在聊天应用中对用户进行静音。For example, muting a user in a chat app.
  • 将权限添加到用户。Adding privileges to a user.

质询和禁止之间的差异Differences between Challenge and Forbid

此应用将默认策略设置为 "需要经过身份验证的用户"。This app sets the default policy to require authenticated users. 以下代码允许匿名用户。The following code allows anonymous users. 允许匿名用户显示质询与禁止之间的差异。Anonymous users are allowed to show the differences between Challenge vs 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 = 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();
    }
}

在上述代码中:In the preceding code:

  • 如果用户未通过身份验证,将返回 ChallengeResultWhen the user is not authenticated, a ChallengeResult is returned. 返回 ChallengeResult 后,用户将重定向到登录页。When a ChallengeResult is returned, the user is redirected to the sign-in page.
  • 如果用户已通过身份验证,但未获得授权,则返回 ForbidResultWhen the user is authenticated, but not authorized, a ForbidResult is returned. 返回 ForbidResult 后,用户将被重定向到 "拒绝访问" 页。When a ForbidResult is returned, the user is redirected to the access denied page.

测试已完成的应用程序Test the completed app

如果你尚未设置设定为种子的用户帐户的密码,使用机密管理器工具设置密码:If you haven't already set a password for seeded user accounts, use the Secret Manager tool to set a password:

  • 选择强密码:使用八个或更多字符,并且至少使用一个大写字符、数字和符号。Choose a strong password: Use eight or more characters and at least one upper-case character, number, and symbol. 例如,Passw0rd!符合强密码要求。For example, Passw0rd! meets the strong password requirements.

  • 执行以下命令从项目的文件夹,其中<PW>的密码:Execute the following command from the project's folder, where <PW> is the password:

    dotnet user-secrets set SeedUserPW <PW>
    

如果应用了联系人:If the app has contacts:

  • 删除所有记录中Contact表。Delete all of the records in the Contact table.
  • 重新启动应用以设定数据库种子。Restart the app to seed the database.

测试已完成的应用程序的简单方法是启动三个不同的浏览器 (或 incognito/InPrivate 会话)。An easy way to test the completed app is to launch three different browsers (or incognito/InPrivate sessions). 在一个浏览器中注册一个新用户 (例如, test@example.com)。In one browser, register a new user (for example, test@example.com). 登录到每个浏览器使用不同的用户。Sign in to each browser with a different user. 验证以下操作:Verify the following operations:

  • 已注册的用户可以查看所有已批准的联系人数据。Registered users can view all of the approved contact data.
  • 已注册的用户可以编辑/删除其自己的数据。Registered users can edit/delete their own data.
  • 经理可以批准/拒绝的联系人数据。Managers can approve/reject contact data. Details视图视图将显示批准拒绝按钮。The Details view shows Approve and Reject buttons.
  • 管理员可以批准/拒绝和编辑/删除所有数据。Administrators can approve/reject and edit/delete all data.
用户User 由应用程序进行种子设定Seeded by the app 选项Options
test@example.com NoNo 编辑/删除自己的数据。Edit/delete the own data.
manager@contoso.com Yes 批准/拒绝和编辑/删除拥有的数据。Approve/reject and edit/delete own data.
admin@contoso.com Yes 批准/拒绝和编辑/删除所有数据。Approve/reject and edit/delete all data.

在管理员的浏览器中创建联系人。Create a contact in the administrator's browser. 删除 URL 复制并编辑从管理员的联系信息。Copy the URL for delete and edit from the administrator contact. 将下面的链接粘贴到测试用户的浏览器以验证测试用户不能执行这些操作。Paste these links into the test user's browser to verify the test user can't perform these operations.

创建入门级应用Create the starter app

  • 创建名为"ContactManager"Razor 页面应用Create a Razor Pages app named "ContactManager"

    • 创建包含应用单个用户帐户Create the app with Individual User Accounts.
    • 它命名为"ContactManager"使命名空间匹配的示例中使用的命名空间。Name it "ContactManager" so the namespace matches the namespace used in the sample.
    • -uld 指定 LocalDB,而不是 SQLite-uld specifies LocalDB instead of SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • 添加模型/联系方式Add 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模型。Scaffold the Contact model.

  • 创建初始迁移并更新数据库:Create initial migration and update the database:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
dotnet ef database drop -f
dotnet ef migrations add initial
dotnet ef database update

如果使用 dotnet aspnet-codegenerator razorpage 命令时遇到 bug,请参阅此 GitHub 问题If you experience a bug with the dotnet aspnet-codegenerator razorpage command, see this GitHub issue.

  • 更新Pages/Shared/_Layout cshtml文件中的ContactManager定位点:Update the ContactManager anchor in the Pages/Shared/_Layout.cshtml file:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • 测试应用程序的创建、 编辑和删除联系人Test the app by creating, editing, and deleting a contact

设定数据库种子Seed the database

SeedData类添加到Data文件夹:Add the SeedData class to the Data folder:

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;

// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {              
                SeedDB(context, "0");
            }
        }        

        public static void SeedDB(ApplicationDbContext context, string adminID)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
                new Contact
                {
                    Name = "Thorsten Weinrich",
                    Address = "5678 1st Ave W",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "thorsten@example.com"
                },
             new Contact
             {
                 Name = "Yuhong Li",
                 Address = "9012 State st",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "yuhong@example.com"
             },
             new Contact
             {
                 Name = "Jon Orton",
                 Address = "3456 Maple St",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "jon@example.com"
             },
             new Contact
             {
                 Name = "Diliana Alexieva-Bosseva",
                 Address = "7890 2nd Ave E",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "diliana@example.com"
             }
             );
            context.SaveChanges();
        }

    }
}

调用SeedData.InitializeMain:Call SeedData.Initialize from Main:

using ContactManager.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContactManager
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<ApplicationDbContext>();
                    context.Database.Migrate();
                    SeedData.Initialize(services, "not used");
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

测试应用程序设定数据库种子。Test that the app seeded the database. 如果联系人 DB 中有任何行,则不会运行 seed 方法。If there are any rows in the contact DB, the seed method doesn't run.

本教程演示如何使用受授权的用户数据创建 ASP.NET Core web 应用。This tutorial shows how to create an ASP.NET Core web app with user data protected by authorization. 它显示的身份验证 (注册) 的用户的联系人列表已创建。It displays a list of contacts that authenticated (registered) users have created. 有三个安全组:There are three security groups:

  • 注册用户可以查看所有已批准的数据还可以编辑/删除其自己的数据。Registered users can view all the approved data and can edit/delete their own data.
  • 管理器可以批准或拒绝的联系人数据。Managers can approve or reject contact data. 仅已批准的联系人是对用户可见。Only approved contacts are visible to users.
  • 管理员可以批准/拒绝和编辑/删除的任何数据。Administrators can approve/reject and edit/delete any data.

在下图中,用户 Rick (rick@example.com) 登录。In the following image, user Rick (rick@example.com) is signed in. Rick 只能查看允许的联系人和编辑/删除/新建其联系人的链接。Rick can only view approved contacts and Edit/Delete/Create New links for his contacts. 只有最后一个记录,创建由 Rick,显示编辑删除链接。Only the last record, created by Rick, displays Edit and Delete links. 其他用户不会看到的最后一个记录,直到经理或管理员的状态更改为"已批准"。Other users won't see the last record until a manager or administrator changes the status to "Approved".

屏幕截图显示 Rick 登录

在下图中,manager@contoso.com 已登录,并在管理器的角色中:In the following image, manager@contoso.com is signed in and in the manager's role:

屏幕截图显示manager@contoso.com登录

下图显示在管理器的联系人的详细信息视图:The following image shows the managers details view of a contact:

联系人的经理的视图

批准拒绝按钮仅显示经理和管理员。The Approve and Reject buttons are only displayed for managers and administrators.

在下图中,admin@contoso.com 以管理员角色登录:In the following image, admin@contoso.com is signed in and in the administrator's role:

屏幕截图显示admin@contoso.com登录

管理员具有所有权限。The administrator has all privileges. 她可以读取、 编辑或删除任何联系,并更改联系人的状态。She can read/edit/delete any contact and change the status of contacts.

通过创建该应用基架以下Contact模型:The app was created by scaffolding the following Contact model:

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; }
}

此示例包含以下授权处理程序:The sample contains the following authorization handlers:

  • ContactIsOwnerAuthorizationHandler:确保用户只能编辑其数据。ContactIsOwnerAuthorizationHandler: Ensures that a user can only edit their data.
  • ContactManagerAuthorizationHandler:允许经理批准或拒绝联系人。ContactManagerAuthorizationHandler: Allows managers to approve or reject contacts.
  • ContactAdministratorsAuthorizationHandler:允许管理员批准或拒绝联系人以及编辑/删除联系人。ContactAdministratorsAuthorizationHandler: Allows administrators to approve or reject contacts and to edit/delete contacts.

系统必备Prerequisites

本教程被高级。This tutorial is advanced. 您应熟悉:You should be familiar with:

Starter 和已完成应用程序The starter and completed app

下载完成应用。Download the completed app. 测试已完成的应用,使你熟悉其安全功能。Test the completed app so you become familiar with its security features.

入门级应用The starter app

下载初学者应用。Download the starter app.

运行应用,点击ContactManager链接,并验证是否可以创建、 编辑和删除联系人。Run the app, tap the ContactManager link, and verify you can create, edit, and delete a contact.

保护用户数据Secure user data

以下部分介绍了所有主要的步骤以创建安全的用户数据应用程序。The following sections have all the major steps to create the secure user data app. 您可能会发现引用的已完成项目很有帮助。You may find it helpful to refer to the completed project.

将绑定到用户的联系人数据Tie the contact data to the user

使用 ASP.NET标识用户 ID,以确保用户可以编辑其数据,而不是其他用户数据。Use the ASP.NET Identity user ID to ensure users can edit their data, but not other users data. 添加OwnerIDContactStatusContact模型:Add OwnerID and ContactStatus to the Contact model:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string OwnerID { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID 是从用户的 IDAspNetUser表中标识数据库。OwnerID is the user's ID from the AspNetUser table in the Identity database. Status字段确定是否可由普通用户查看联系人。The Status field determines if a contact is viewable by general users.

创建新的迁移并更新数据库:Create a new migration and update the database:

dotnet ef migrations add userID_Status
dotnet ef database update

将角色服务添加到标识Add Role services to Identity

追加AddRoles添加角色服务:Append AddRoles to add Role services:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
         .AddEntityFrameworkStores<ApplicationDbContext>();

需要身份验证的用户Require authenticated users

设置默认身份验证策略以要求用户进行身份验证:Set the default authentication policy to require users to be authenticated:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
         .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddMvc(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    })                
       .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

可以选择在 Razor 页面、 控制器或操作方法级别使用的身份验证禁用[AllowAnonymous]属性。You can opt out of authentication at the Razor Page, controller, or action method level with the [AllowAnonymous] attribute. 设置默认身份验证策略以要求用户进行身份验证来保护新添加的 Razor 页面和控制器。Setting the default authentication policy to require users to be authenticated protects newly added Razor Pages and controllers. 默认情况下所需的身份验证是比依赖于新的控制器和 Razor 页,以包含更安全[Authorize]属性。Having authentication required by default is more secure than relying on new controllers and Razor Pages to include the [Authorize] attribute.

添加AllowAnonymous到索引中,因此匿名用户可以获取有关站点的信息注册有关,和联系人页面。Add AllowAnonymous to the Index, About, and Contact pages so anonymous users can get information about the site before they register.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages
{
    [AllowAnonymous]
    public class IndexModel : PageModel
    {
        public void OnGet()
        {

        }
    }
}

配置测试帐户Configure the test account

SeedData类创建两个帐户: 管理员和管理员。The SeedData class creates two accounts: administrator and manager. 使用机密管理器工具设置这些帐户的密码。Use the Secret Manager tool to set a password for these accounts. 从项目目录中设置密码 (目录包含Program.cs):Set the password from the project directory (the directory containing Program.cs):

dotnet user-secrets set SeedUserPW <PW>

如果未指定强密码,将引发异常时SeedData.Initialize调用。If a strong password is not specified, an exception is thrown when SeedData.Initialize is called.

更新Main使用测试密码:Update Main to use the test password:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;
            var context = services.GetRequiredService<ApplicationDbContext>();
            context.Database.Migrate();

            // requires using Microsoft.Extensions.Configuration;
            var config = host.Services.GetRequiredService<IConfiguration>();
            // Set password with the Secret Manager tool.
            // dotnet user-secrets set SeedUserPW <pw>

            var testUserPw = config["SeedUserPW"];
            try
            {
                SeedData.Initialize(services, testUserPw).Wait();
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex.Message, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

创建测试帐户和更新联系人Create the test accounts and update the contacts

更新Initialize中的方法SeedData类,以创建测试帐户:Update the Initialize method in the SeedData class to create the test accounts:

public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
    using (var context = new ApplicationDbContext(
        serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
    {
        // For sample purposes seed both with the same password.
        // Password is set with the following:
        // dotnet user-secrets set SeedUserPW <pw>
        // The admin user can do anything

        var adminID = await EnsureUser(serviceProvider, testUserPw, "admin@contoso.com");
        await EnsureRole(serviceProvider, adminID, Constants.ContactAdministratorsRole);

        // allowed user can create and edit contacts that they create
        var managerID = await EnsureUser(serviceProvider, testUserPw, "manager@contoso.com");
        await EnsureRole(serviceProvider, managerID, Constants.ContactManagersRole);

        SeedDB(context, adminID);
    }
}

private static async Task<string> EnsureUser(IServiceProvider serviceProvider,
                                            string testUserPw, string UserName)
{
    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByNameAsync(UserName);
    if (user == null)
    {
        user = new IdentityUser { UserName = UserName };
        await userManager.CreateAsync(user, testUserPw);
    }

    return user.Id;
}

private static async Task<IdentityResult> EnsureRole(IServiceProvider serviceProvider,
                                                              string uid, string role)
{
    IdentityResult IR = null;
    var roleManager = serviceProvider.GetService<RoleManager<IdentityRole>>();

    if (roleManager == null)
    {
        throw new Exception("roleManager null");
    }

    if (!await roleManager.RoleExistsAsync(role))
    {
        IR = await roleManager.CreateAsync(new IdentityRole(role));
    }

    var userManager = serviceProvider.GetService<UserManager<IdentityUser>>();

    var user = await userManager.FindByIdAsync(uid);

    if(user == null)
    {
        throw new Exception("The testUserPw password was probably not strong enough!");
    }
    
    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

添加管理员用户 ID 和ContactStatus到联系人。Add the administrator user ID and ContactStatus to the contacts. 先创建一个"已提交"和一个"已拒绝"的联系人。Make one of the contacts "Submitted" and one "Rejected". 将用户 ID 和状态添加到所有联系人。Add the user ID and status to all the contacts. 只能有一个联系人所示:Only one contact is shown:

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
        },

创建所有者、 经理和管理员授权处理程序Create owner, manager, and administrator authorization handlers

创建一个授权文件夹,并在其中创建一个 ContactIsOwnerAuthorizationHandler 类。Create an Authorization folder and create a ContactIsOwnerAuthorizationHandler class in it. ContactIsOwnerAuthorizationHandler验证对资源进行操作的用户拥有的资源。The ContactIsOwnerAuthorizationHandler verifies that the user acting on a resource owns the resource.

using System.Threading.Tasks;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<IdentityUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<IdentityUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                // Return Task.FromResult(0) if targeting a version of
                // .NET Framework older than 4.6:
                return Task.CompletedTask;
            }

            // If we're not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.CompletedTask;
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

ContactIsOwnerAuthorizationHandler调用上下文。成功当前经过身份验证的用户是否联系所有者。The ContactIsOwnerAuthorizationHandler calls context.Succeed if the current authenticated user is the contact owner. 授权处理程序通常:Authorization handlers generally:

  • 返回context.Succeed满足的要求。Return context.Succeed when the requirements are met.
  • 返回Task.CompletedTask时不符合要求。Return Task.CompletedTask when requirements aren't met. Task.CompletedTask 不是成功或失败—它允许其他授权处理程序运行。Task.CompletedTask is not success or failure—it allows other authorization handlers to run.

如果你需要将显式失败,返回上下文。失败If you need to explicitly fail, return context.Fail.

应用程序允许联系所有者到编辑/删除/创建他们自己的数据。The app allows contact owners to edit/delete/create their own data. ContactIsOwnerAuthorizationHandler 不需要检查要求参数中传递该操作。ContactIsOwnerAuthorizationHandler doesn't need to check the operation passed in the requirement parameter.

创建管理器授权处理程序Create a manager authorization handler

创建ContactManagerAuthorizationHandler类中授权文件夹。Create a ContactManagerAuthorizationHandler class in the Authorization folder. ContactManagerAuthorizationHandler验证对资源进行操作的用户是管理员。The ContactManagerAuthorizationHandler verifies the user acting on the resource is a manager. 只有经理们才可以批准或拒绝内容的更改 (新的或已更改)。Only managers can approve or reject content changes (new or changed).

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;
        }
    }
}

创建管理员授权处理程序Create an administrator authorization handler

创建ContactAdministratorsAuthorizationHandler类中授权文件夹。Create a ContactAdministratorsAuthorizationHandler class in the Authorization folder. ContactAdministratorsAuthorizationHandler验证对资源进行操作的用户是管理员。The ContactAdministratorsAuthorizationHandler verifies the user acting on the resource is an administrator. 管理员可以执行所有操作。Administrator can do all operations.

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;
        }
    }
}

注册授权处理程序Register the authorization handlers

Entity Framework Core 使用 AddScoped 的服务必须使用注册以进行依赖关系注入Services using Entity Framework Core must be registered for dependency injection using AddScoped. ContactIsOwnerAuthorizationHandler使用 ASP.NET Core标识,这基于实体框架核心。The ContactIsOwnerAuthorizationHandler uses ASP.NET Core Identity, which is built on Entity Framework Core. 注册服务集合的处理程序,以便它们可供ContactsController通过依赖关系注入Register the handlers with the service collection so they're available to the ContactsController through dependency injection. 将以下代码添加到末尾ConfigureServices:Add the following code to the end of ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>().AddRoles<IdentityRole>()
         .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddMvc(config =>
    {
        // using Microsoft.AspNetCore.Mvc.Authorization;
        // using Microsoft.AspNetCore.Authorization;
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    })                
       .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    // Authorization handlers.
    services.AddScoped<IAuthorizationHandler,
                          ContactIsOwnerAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactManagerAuthorizationHandler>();
}

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler添加为单一实例。ContactAdministratorsAuthorizationHandler and ContactManagerAuthorizationHandler are added as singletons. 它们是单一实例,因为它们不使用 EF 和所需的所有信息都位于Context参数的HandleRequirementAsync方法。They're singletons because they don't use EF and all the information needed is in the Context parameter of the HandleRequirementAsync method.

支持授权Support authorization

在本部分中,将更新 Razor 页面和添加操作要求类。In this section, you update the Razor Pages and add an operations requirements class.

查看联系人操作要求类Review the contact operations requirements class

查看ContactOperations类。Review the ContactOperations class. 此类包含要求应用支持:This class contains the requirements the app supports:

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 页面的基类Create a base class for the Contacts Razor Pages

创建一个包含在联系人 Razor 页面使用的服务的基类。Create a base class that contains the services used in the contacts Razor Pages. 基类将初始化代码放在一个位置:The base class puts the initialization code in one location:

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;
        } 
    }
}

前面的代码:The preceding code:

  • 添加IAuthorizationService服务对授权处理程序的访问权限。Adds the IAuthorizationService service to access to the authorization handlers.
  • 将标识添加UserManager服务。Adds the Identity UserManager service.
  • 添加 ApplicationDbContextAdd the ApplicationDbContext.

更新 CreateModelUpdate the CreateModel

更新创建页面模型构造函数以使用DI_BasePageModel基类:Update the create page model constructor to use the DI_BasePageModel base class:

public class CreateModel : DI_BasePageModel
{
    public CreateModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

更新CreateModel.OnPostAsync方法:Update the CreateModel.OnPostAsync method to:

  • 添加到的用户 IDContact模型。Add the user ID to the Contact model.
  • 调用授权处理程序,以验证用户有权创建联系人。Call the authorization handler to verify the user has permission to create contacts.
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Contact.OwnerID = UserManager.GetUserId(User);

    // requires using ContactManager.Authorization;
    var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                User, Contact,
                                                ContactOperations.Create);
    if (!isAuthorized.Succeeded)
    {
        return new ChallengeResult();
    }

    Context.Contact.Add(Contact);
    await Context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

更新 IndexModelUpdate the IndexModel

更新OnGetAsync方法,以便仅被批准的联系人显示为普通用户:Update the OnGetAsync method so only approved contacts are shown to general users:

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();
    }
}

更新 EditModelUpdate the EditModel

添加授权处理程序以验证的用户拥有联系人。Add an authorization handler to verify the user owns the contact. 正在验证资源授权,因为[Authorize]属性不能满足。Because resource authorization is being validated, the [Authorize] attribute is not enough. 评估属性时,应用程序不具有对资源的访问。The app doesn't have access to the resource when attributes are evaluated. 基于资源的授权必须是命令性。Resource-based authorization must be imperative. 应用程序页面模型中加载或加载处理程序本身内获得资源的访问权限,则必须执行检查。Checks must be performed once the app has access to the resource, either by loading it in the page model or by loading it within the handler itself. 您经常访问的资源,通过传入的资源键。You frequently access the resource by passing in the resource key.

public class EditModel : DI_BasePageModel
{
    public EditModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                  User, Contact,
                                                  ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Fetch Contact from DB to get OwnerID.
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Update);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }

        Contact.OwnerID = contact.OwnerID;

        Context.Attach(Contact).State = EntityState.Modified;

        if (contact.Status == ContactStatus.Approved)
        {
            // If the contact is updated after approval, 
            // and the user cannot approve,
            // set the status back to submitted so the update can be
            // checked and approved.
            var canApprove = await AuthorizationService.AuthorizeAsync(User,
                                    contact,
                                    ContactOperations.Approve);

            if (!canApprove.Succeeded)
            {
                contact.Status = ContactStatus.Submitted;
            }
        }

        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }

    private bool ContactExists(int id)
    {
        return Context.Contact.Any(e => e.ContactId == id);
    }
}

更新 DeleteModelUpdate the DeleteModel

更新要使用授权处理程序来验证用户具有 delete 权限 contact 上的删除页面模型。Update the delete page model to use the authorization handler to verify the user has delete permission on the contact.

public class DeleteModel : DI_BasePageModel
{
    public DeleteModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        Contact = await Context.Contact.FindAsync(id);

        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }

        Context.Contact.Remove(Contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

将授权服务注入到视图Inject the authorization service into the views

目前,此 UI 显示编辑和删除的用户无法修改的联系人的链接。Currently, the UI shows edit and delete links for contacts the user can't modify.

注入中的授权服务views/_viewimports.cshtml文件,以便它可供所有视图:Inject the authorization service in the Views/_ViewImports.cshtml file so it's available to all views:

@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语句。The preceding markup adds several using statements.

更新编辑删除中的链接Pages/Contacts/Index.cshtml以便仅在呈现具有适当权限的用户:Update the Edit and Delete links in Pages/Contacts/Index.cshtml so they're only rendered for users with the appropriate permissions:

@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>

警告

隐藏用户没有权限更改的数据中的链接不会保护应用。Hiding links from users that don't have permission to change data doesn't secure the app. 隐藏链接使应用更加友好的用户显示唯一有效的链接。Hiding links makes the app more user-friendly by displaying only valid links. 用户可以 hack 生成的 Url 以调用编辑和删除操作上不拥有的数据。Users can hack the generated URLs to invoke edit and delete operations on data they don't own. Razor 页面或控制器必须强制执行访问检查,以保护数据。The Razor Page or controller must enforce access checks to secure the data.

更新详细信息Update Details

更新的详细信息视图,使管理员可以批准或拒绝联系人:Update the details view so managers can approve or reject contacts:

        @*Precedng markup omitted for brevity.*@
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

@if (Model.Contact.Status != ContactStatus.Approved)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Approve)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }
}

@if (Model.Contact.Status != ContactStatus.Rejected)
{
    @if ((await AuthorizationService.AuthorizeAsync(
     User, Model.Contact, ContactOperations.Reject)).Succeeded)
    {
        <form style="display:inline;" method="post">
            <input type="hidden" name="id" value="@Model.Contact.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-success">Reject</button>
        </form>
    }
}

<div>
    @if ((await AuthorizationService.AuthorizeAsync(
         User, Model.Contact,
         ContactOperations.Update)).Succeeded)
    {
        <a asp-page="./Edit" asp-route-id="@Model.Contact.ContactId">Edit</a>
        <text> | </text>
    }
    <a asp-page="./Index">Back to List</a>
</div>

更新详细信息页模型:Update the details page model:

public class DetailsModel : DI_BasePageModel
{
    public DetailsModel(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);

        if (Contact == null)
        {
            return NotFound();
        }

        var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
                           User.IsInRole(Constants.ContactAdministratorsRole);

        var currentUserId = UserManager.GetUserId(User);

        if (!isAuthorized 
            &&  currentUserId != Contact.OwnerID
            && Contact.Status != ContactStatus.Approved) 
        {
            return new ChallengeResult();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, ContactStatus status)
    {
        var contact = await Context.Contact.FirstOrDefaultAsync(
                                                  m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var contactOperation = (status == ContactStatus.Approved)
                                                   ? ContactOperations.Approve
                                                   : ContactOperations.Reject;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(User, contact,
                                    contactOperation);
        if (!isAuthorized.Succeeded)
        {
            return new ChallengeResult();
        }
        contact.Status = status;
        Context.Contact.Update(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

添加或删除角色对用户的Add or remove a user to a role

请参阅本期有关的信息:See this issue for information on:

  • 从用户删除的特权。Removing privileges from a user. 例如,在聊天应用中对用户进行静音。For example, muting a user in a chat app.
  • 将权限添加到用户。Adding privileges to a user.

测试已完成的应用程序Test the completed app

如果你尚未设置设定为种子的用户帐户的密码,使用机密管理器工具设置密码:If you haven't already set a password for seeded user accounts, use the Secret Manager tool to set a password:

  • 选择强密码:使用八个或更多字符,并且至少使用一个大写字符、数字和符号。Choose a strong password: Use eight or more characters and at least one upper-case character, number, and symbol. 例如,Passw0rd!符合强密码要求。For example, Passw0rd! meets the strong password requirements.

  • 执行以下命令从项目的文件夹,其中<PW>的密码:Execute the following command from the project's folder, where <PW> is the password:

    dotnet user-secrets set SeedUserPW <PW>
    
  • 删除和更新数据库Drop and update the Database

    dotnet ef database drop -f
    dotnet ef database update  
    
  • 重新启动应用以设定数据库种子。Restart the app to seed the database.

测试已完成的应用程序的简单方法是启动三个不同的浏览器 (或 incognito/InPrivate 会话)。An easy way to test the completed app is to launch three different browsers (or incognito/InPrivate sessions). 在一个浏览器中注册一个新用户 (例如, test@example.com)。In one browser, register a new user (for example, test@example.com). 登录到每个浏览器使用不同的用户。Sign in to each browser with a different user. 验证以下操作:Verify the following operations:

  • 已注册的用户可以查看所有已批准的联系人数据。Registered users can view all of the approved contact data.
  • 已注册的用户可以编辑/删除其自己的数据。Registered users can edit/delete their own data.
  • 经理可以批准/拒绝的联系人数据。Managers can approve/reject contact data. Details视图视图将显示批准拒绝按钮。The Details view shows Approve and Reject buttons.
  • 管理员可以批准/拒绝和编辑/删除所有数据。Administrators can approve/reject and edit/delete all data.
用户User 由应用程序进行种子设定Seeded by the app 选项Options
test@example.com NoNo 编辑/删除自己的数据。Edit/delete the own data.
manager@contoso.com Yes 批准/拒绝和编辑/删除拥有的数据。Approve/reject and edit/delete own data.
admin@contoso.com Yes 批准/拒绝和编辑/删除所有数据。Approve/reject and edit/delete all data.

在管理员的浏览器中创建联系人。Create a contact in the administrator's browser. 删除 URL 复制并编辑从管理员的联系信息。Copy the URL for delete and edit from the administrator contact. 将下面的链接粘贴到测试用户的浏览器以验证测试用户不能执行这些操作。Paste these links into the test user's browser to verify the test user can't perform these operations.

创建入门级应用Create the starter app

  • 创建名为"ContactManager"Razor 页面应用Create a Razor Pages app named "ContactManager"

    • 创建包含应用单个用户帐户Create the app with Individual User Accounts.
    • 它命名为"ContactManager"使命名空间匹配的示例中使用的命名空间。Name it "ContactManager" so the namespace matches the namespace used in the sample.
    • -uld 指定 LocalDB,而不是 SQLite-uld specifies LocalDB instead of SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • 添加模型/联系方式Add 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模型。Scaffold the Contact model.

  • 创建初始迁移并更新数据库:Create initial migration and update the database:

    dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries
    dotnet ef database drop -f
    dotnet ef migrations add initial
    dotnet ef database update
    
  • 更新ContactManager中的定位点pages/_layout.cshtml文件:Update the ContactManager anchor in the Pages/_Layout.cshtml file:

    <a asp-page="/Contacts/Index" class="navbar-brand">ContactManager</a>
    
  • 测试应用程序的创建、 编辑和删除联系人Test the app by creating, editing, and deleting a contact

设定数据库种子Seed the database

添加SeedData类来数据文件夹。Add the SeedData class to the Data folder.

调用SeedData.InitializeMain:Call SeedData.Initialize from Main:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            try
            {
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();
                SeedData.Initialize(services, "not used");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred seeding the DB.");
            }
        }

        host.Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

测试应用程序设定数据库种子。Test that the app seeded the database. 如果联系人 DB 中有任何行,则不会运行 seed 方法。If there are any rows in the contact DB, the seed method doesn't run.

其他资源Additional resources