通过授权保护的用户数据创建 ASP.NET Core Web 应用

作者:Rick AndersonJoe Audette

本教程介绍如何通过授权保护的用户数据创建 ASP.NET Core Web 应用。 它显示经过身份验证(注册)的用户创建的联系人列表。 有三个安全组:

  • 已注册的用户可以查看所有已批准的数据,并可以编辑/删除自己的数据。
  • 经理可以批准或拒绝联系人数据。 只有已批准的联系人才对用户可见。
  • 管理员可以批准/拒绝和编辑/删除任何数据。

本文档中的图像与最新的模板不完全匹配。

在下图中,用户 Rick (rick@example.com) 已登录。 Rick 只能查看已批准的联系人,并为其联系人编辑/删除/创建新链接。 只有 Rick 创建的最后一条记录才会显示编辑和删除链接。 在经理或管理员将状态更改为“已批准”后,其他用户才能看到最后一条记录。

Screenshot showing Rick signed in

在下图中,manager@contoso.com 已登录并扮演经理的角色:

Screenshot showing manager@contoso.com signed in

下图显示了经理的联系人详细信息视图:

Manager's view of a contact

“批准”和“拒绝”按钮仅为经理和管理员显示。

在下图中,admin@contoso.com 已登录并扮演管理员的角色:

Screenshot showing admin@contoso.com signed in

管理员拥有所有权限。 她可以读取、编辑或删除任何联系人并更改联系人的状态。

应用通过搭建以下 模型的基架Contact创建:

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

该示例包含以下授权处理程序:

  • ContactIsOwnerAuthorizationHandler:确保用户只能编辑其数据。
  • ContactManagerAuthorizationHandler:允许经理批准或拒绝联系人。
  • ContactAdministratorsAuthorizationHandler:允许管理员批准或拒绝联系人以及编辑/删除联系人。

先决条件

本教程是高级教程。 你应该熟悉以下内容:

初学者和已完成应用

下载已完成的应用。 测试已完成应用,以便熟悉其安全功能。

初学者应用

下载初学者应用。

运行应用,点击 ContactManager 链接,并验证你是否可以创建、编辑和删除联系人。 若要创建初学者应用,请参阅创建初学者应用

保护用户数据

以下部分包含创建安全用户数据应用的所有主要步骤。 你可能会发现引用已完成的项目很有帮助。

将联系人数据绑定到用户

使用 ASP.NET Identity 用户 ID 确保用户可以编辑其数据,但不能编辑其他用户数据。 将 OwnerIDContactStatus 添加到 Contact 模型:

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

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

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

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerIDIdentity 数据库中 AspNetUser 表中的用户 ID。 Status 字段确定一般用户是否可以查看联系人。

创建新的迁移并更新数据库:

dotnet ef migrations add userID_Status
dotnet ef database update

将角色服务添加到 Identity

附加 AddRoles 以添加角色服务:

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

需要经过身份验证的用户

设置回退授权策略以要求用户进行身份验证:

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

前面突出显示的代码设置回退授权策略。 回退授权策略要求所有用户进行身份验证,但具有授权属性的 Razor Pages、控制器或操作方法除外。 例如,具有 [AllowAnonymous][Authorize(PolicyName="MyPolicy")] 的 Razor Pages、控制器或操作方法使用应用的授权属性,而不是回退授权策略。

RequireAuthenticatedUserDenyAnonymousAuthorizationRequirement 添加到当前实例,这将强制对当前用户进行身份验证。

回退授权策略:

  • 应用于未显式指定授权策略的所有请求。 对于终结点路由提供的请求,这包括未指定授权属性的任何终结点。 对于在授权中间件之后由其他中间件提供的请求(如静态文件),这会将策略应用于所有请求。

将回退授权策略设置为要求用户进行身份验证可保护新添加的 Razor Pages 和控制器。 默认情况下,要求进行授权比依赖新控制器和 Razor Pages 来包含 [Authorize] 属性更安全。

AuthorizationOptions 类还包含 AuthorizationOptions.DefaultPolicy。 未指定策略时,DefaultPolicy 是与 [Authorize] 属性一起使用的策略。 [Authorize] 不包含命名策略,与 [Authorize(PolicyName="MyPolicy")] 不同。

有关策略详细信息,请参阅 ASP.NET Core 中基于策略的授权

MVC 控制器和 Razor Pages 要求所有用户进行身份验证的另一种方法是添加授权筛选器:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddControllers(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

var app = builder.Build();

前面的代码使用授权筛选器,设置回退策略使用终结点路由。 设置回退策略是要求所有用户进行身份验证的首选方法。

AllowAnonymous 添加到 IndexPrivacy 页,以便匿名用户在注册之前可以获取有关站点的信息:

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

namespace ContactManager.Pages;

[AllowAnonymous]
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {

    }
}

配置测试帐户

SeedData 类创建两个帐户:管理员和经理。 使用机密管理器工具为这些帐户设置密码。 从项目目录(目录包含 Program.cs)中设置密码:

dotnet user-secrets set SeedUserPW <PW>

如果未指定强密码,则调用 SeedData.Initialize 时会引发异常。

更新应用以使用测试密码:

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

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

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}

创建测试帐户并更新联系人

更新 SeedData 类中的 Initialize 方法,以创建测试帐户:

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

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

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

        SeedDB(context, adminID);
    }
}

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

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

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

    return user.Id;
}

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

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

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

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

    //if (userManager == null)
    //{
    //    throw new Exception("userManager is null");
    //}

    var user = await userManager.FindByIdAsync(uid);

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

    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

向联系人添加管理员用户 ID 和 ContactStatus。 使其中一个联系人为“已提交”,一个为“已拒绝”。 向所有联系人添加用户 ID 和状态。 只显示一个联系人:

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

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

创建所有者、经理和管理员授权处理程序

在“授权”文件夹中创建 ContactIsOwnerAuthorizationHandler 类。 ContactIsOwnerAuthorizationHandler 验证对资源进行操作的用户是否拥有该资源。

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

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

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

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

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

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

            return Task.CompletedTask;
        }
    }
}

如果当前经过身份验证的用户是联系人所有者,则 ContactIsOwnerAuthorizationHandler 会调用 context.Succeed。 授权处理程序通常:

  • 满足要求时调用 context.Succeed
  • 未满足要求时返回 Task.CompletedTask。 在未事先调用 context.Successcontext.Fail 的情况下返回 Task.CompletedTask 不是成功或失败,它允许运行其他授权处理程序。

如果需要显式失败,请调用 context.Fail

该应用使联系人所有者可编辑/删除/创建自己的数据。 ContactIsOwnerAuthorizationHandler 不需要检查在要求参数中传递的操作。

创建经理授权处理程序

在“授权”文件夹中创建 ContactManagerAuthorizationHandler 类。 ContactManagerAuthorizationHandler 验证对资源进行操作的用户是否是经理。 只有经理才能批准或拒绝内容更改(新的或已更改的)。

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

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

创建管理员授权处理程序

在“授权”文件夹中创建 ContactAdministratorsAuthorizationHandler 类。 ContactAdministratorsAuthorizationHandler 验证对资源进行操作的用户是否是管理员。 管理员可以执行所有操作。

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

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

注册授权处理程序

使用 Entity Framework Core 的服务必须使用 AddScoped 注册依赖项注入ContactIsOwnerAuthorizationHandler 使用 ASP.NET Core Identity,其基于 Entity Framework Core。 将处理程序注册到服务集合,以便通过依赖注入ContactsController 可用。 将以下代码添加到 ConfigureServices 的末尾:

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

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

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler 作为单一实例添加。 它们是单一实例,因为不使用 EF,并且所需的全部信息都位于 HandleRequirementAsync 方法的 Context 参数中。

支持授权

在本部分中,将更新 Razor 页面并添加操作要求类。

查看联系人操作要求类

查看 ContactOperations 类。 此类包含应用支持的要求:

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

为联系人 Razor Pages 创建基类

创建一个包含在联系人 Razor Pages 中使用的服务的基类。 基类将初始化代码放在一个位置:

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}

前面的代码:

  • 添加 IAuthorizationService 服务以访问授权处理程序。
  • 添加 IdentityUserManager 服务。
  • 添加 ApplicationDbContext

更新 CreateModel

更新“创建”页模型:

  • 使用 DI_BasePageModel 基类的构造函数。
  • 执行以下操作的 OnPostAsync 方法:
    • 将用户 ID 添加到 Contact 模型。
    • 调用授权处理程序以验证用户是否有权创建联系人。
using ContactManager.Authorization;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

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

        public IActionResult OnGet()
        {
            return Page();
        }

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

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

            Contact.OwnerID = UserManager.GetUserId(User);

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

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

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

更新 IndexModel

更新 OnGetAsync 方法以便仅向一般用户显示已批准的联系人:

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

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

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

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}

更新 EditModel

添加授权处理程序来验证用户是否拥有该联系人。 由于正在验证资源授权,因此 [Authorize] 属性不够。 评估属性时,应用无法访问资源。 基于资源的授权必须是必需的。 如果应用有权访问该资源,则必须执行检查,方法是将其加载到页面模型中,或在处理程序本身中加载它。 通过传入资源键,可以频繁地访问资源。

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

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

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? contact = await Context.Contact.FirstOrDefaultAsync(
                                                         m => m.ContactId == id);
        if (contact == null)
        {
            return NotFound();
        }

        Contact = contact;

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

        return Page();
    }

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

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

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

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

        Contact.OwnerID = contact.OwnerID;

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

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

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

        await Context.SaveChangesAsync();

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

更新 DeleteModel

更新“删除”页模型,以使用授权处理程序来验证用户是否具有对联系人的“删除”权限。

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

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

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

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

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

        return Page();
    }

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

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

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

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

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

将授权服务注入视图

目前,UI 会显示用户不能修改的联系人的编辑和删除链接。

将授权服务注入 Pages/_ViewImports.cshtml 文件,以便它可用于所有视图:

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

前面的标记添加多个 using 语句。

更新 Pages/Contacts/Index.cshtml 中的编辑和删除链接,以便仅为具有相应权限的用户呈现它们:

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
             <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Contact) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Address)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.City)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.State)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Zip)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Email)
            </td>
                           <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

警告

隐藏不具有更改数据权限的用户的链接不会保护应用的安全。 隐藏链接通过仅显示有效链接使应用更加用户友好。 用户可以通过攻击生成的 URL 来对其不拥有的数据调用编辑和删除操作。 Razor Page 或控制器必须强制进行访问检查以确保数据的安全。

更新详细信息

更新详细信息视图,以便经理可以批准或拒绝联系人:

        @*Preceding markup omitted for brevity.*@
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Contact.Email)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Contact.Email)
        </dd>
    <dt>
            @Html.DisplayNameFor(model => model.Contact.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Contact.Status)
        </dd>
    </dl>
</div>

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

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

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

更新“详细信息”页模型

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

    public Contact Contact { get; set; }

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

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

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

        var currentUserId = UserManager.GetUserId(User);

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

        return Page();
    }

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

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

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

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

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

向角色添加或删除用户

有关以下内容的信息,请参阅此问题

  • 正在删除用户的权限。 例如,在聊天应用中对用户静音。
  • 正在向用户添加权限。

质询与禁止之间的区别

此应用将默认策略设置为需要经过身份验证的用户。 以下代码允许匿名用户。 允许匿名用户显示质询与禁止之间的区别。

[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

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

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

        if (!User.Identity!.IsAuthenticated)
        {
            return Challenge();
        }

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

        var currentUserId = UserManager.GetUserId(User);

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

        return Page();
    }
}

在上述代码中:

  • 如果用户未经过身份验证,则返回 ChallengeResult。 返回 ChallengeResult 后,会将用户重定向到登录页。
  • 如果用户已经过身份验证,但未授权,则返回 ForbidResult。 返回 ForbidResult 后,会将用户重定向到拒绝访问页。

测试已完成的应用

如果尚未为种子用户帐户设置密码,请使用机密管理器工具设置密码:

  • 选择强密码:使用八个或更多字符,并且至少使用一个大写字符、数字和符号。 例如,Passw0rd! 满足强密码要求。

  • 从项目的文件夹中执行以下命令,其中 <PW> 为密码:

    dotnet user-secrets set SeedUserPW <PW>
    

如果应用有联系人:

  • 删除 Contact 表中的所有记录。
  • 重启应用以设定数据库种子。

测试已完成应用的一种简单方法是启动三个不同的浏览器(或 incognito/InPrivate 会话)。 在一个浏览器中,注册新用户(例如 test@example.com)。 使用其他用户登录到每个浏览器。 验证以下操作:

  • 已注册的用户可以查看所有已批准的联系人数据。
  • 已注册的用户可以编辑/删除他们自己的数据。
  • 经理可以批准/拒绝联系人数据。 Details 视图显示“批准”和“拒绝”按钮。
  • 管理员可以批准/拒绝和编辑/删除任何数据。
用户 批准或拒绝联系人 选项
test@example.com 编辑并删除其数据。
manager@contoso.com 编辑并删除其数据。
admin@contoso.com 编辑并删除所有数据。

在管理员的浏览器中创建联系人。 复制管理员联系人的“删除”和“编辑”URL。 将这些链接粘贴到测试用户的浏览器中,以验证测试用户是否无法执行这些操作。

创建初学者应用

  • 创建名为“ContactManager”的 Razor Pages 应用

    • 通过个人用户帐户创建应用。
    • 将其命名为“ContactManager”,使命名空间与该示例中使用的命名空间匹配。
    • -uld 指定 LocalDB,而不是 SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • 添加 Models/Contact.cs:secure-data\samples\starter6\ContactManager\Models\Contact.cs

    using System.ComponentModel.DataAnnotations;
    
    namespace ContactManager.Models
    {
        public class Contact
        {
            public int ContactId { get; set; }
            public string? Name { get; set; }
            public string? Address { get; set; }
            public string? City { get; set; }
            public string? State { get; set; }
            public string? Zip { get; set; }
            [DataType(DataType.EmailAddress)]
            public string? Email { get; set; }
        }
    }
    
  • 搭建 Contact 模型的基架。

  • 创建初始迁移并更新数据库:

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

注意

默认情况下,要安装的 .NET 二进制文件的体系结构表示当前运行的 OS 体系结构。 若要指定不同的 OS 体系结构,请参阅 dotnet tool install, --arch option。 有关详细信息,请参阅 GitHub 问题 dotnet/AspNetCore.Docs #29262

  • 更新 Pages/Shared/_Layout.cshtml 文件中的 ContactManager 定位点:

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • 通过创建、编辑和删除联系人来测试应用

设定数据库种子

SeedData 类添加到“数据”文件夹:

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;

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

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

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

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

    }
}

Program.cs 调用 SeedData.Initialize

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ContactManager.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

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

    await SeedData.Initialize(services);
}

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

测试应用是否为数据库设定种子。 如果联系人数据库中存在任何行,则 seed 方法不会运行。

本教程介绍如何通过授权保护的用户数据创建 ASP.NET Core Web 应用。 它显示经过身份验证(注册)的用户创建的联系人列表。 有三个安全组:

  • 已注册的用户可以查看所有已批准的数据,并可以编辑/删除自己的数据。
  • 经理可以批准或拒绝联系人数据。 只有已批准的联系人才对用户可见。
  • 管理员可以批准/拒绝和编辑/删除任何数据。

本文档中的图像与最新的模板不完全匹配。

在下图中,用户 Rick (rick@example.com) 已登录。 Rick 只能查看已批准的联系人,并为其联系人编辑/删除/创建新链接。 只有 Rick 创建的最后一条记录才会显示编辑和删除链接。 在经理或管理员将状态更改为“已批准”后,其他用户才能看到最后一条记录。

Screenshot showing Rick signed in

在下图中,manager@contoso.com 已登录并扮演经理的角色:

Screenshot showing manager@contoso.com signed in

下图显示了经理的联系人详细信息视图:

Manager's view of a contact

“批准”和“拒绝”按钮仅为经理和管理员显示。

在下图中,admin@contoso.com 已登录并扮演管理员的角色:

Screenshot showing admin@contoso.com signed in

管理员拥有所有权限。 她可以读取/编辑/删除任何联系人并更改联系人的状态。

应用通过搭建以下 模型的基架Contact创建:

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

该示例包含以下授权处理程序:

  • ContactIsOwnerAuthorizationHandler:确保用户只能编辑其数据。
  • ContactManagerAuthorizationHandler:允许经理批准或拒绝联系人。
  • ContactAdministratorsAuthorizationHandler:允许管理员执行以下操作:
    • 批准或拒绝联系人
    • 编辑并删除联系人

先决条件

本教程是高级教程。 你应该熟悉以下内容:

初学者和已完成应用

下载已完成的应用。 测试已完成应用,以便熟悉其安全功能。

初学者应用

下载初学者应用。

运行应用,点击 ContactManager 链接,并验证你是否可以创建、编辑和删除联系人。 若要创建初学者应用,请参阅创建初学者应用

保护用户数据

以下部分包含创建安全用户数据应用的所有主要步骤。 你可能会发现引用已完成的项目很有帮助。

将联系人数据绑定到用户

使用 ASP.NET Identity 用户 ID 确保用户可以编辑其数据,但不能编辑其他用户数据。 将 OwnerIDContactStatus 添加到 Contact 模型:

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

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

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

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerIDIdentity 数据库中 AspNetUser 表中的用户 ID。 Status 字段确定一般用户是否可以查看联系人。

创建新的迁移并更新数据库:

dotnet ef migrations add userID_Status
dotnet ef database update

将角色服务添加到 Identity

附加 AddRoles 以添加角色服务:

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

需要经过身份验证的用户

设置回退身份验证策略以要求用户进行身份验证:

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

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

前面突出显示的代码设置回退身份验证策略。 回退身份验证策略要求所有用户进行身份验证,但具有身份验证属性的 Razor Pages、控制器或操作方法除外。 例如,具有 [AllowAnonymous][Authorize(PolicyName="MyPolicy")] 的 Razor Pages、控制器或操作方法使用应用的身份验证属性,而不是回退身份验证策略。

RequireAuthenticatedUserDenyAnonymousAuthorizationRequirement 添加到当前实例,这将强制对当前用户进行身份验证。

回退身份验证策略:

  • 应用于未显式指定身份验证策略的所有请求。 对于终结点路由提供的请求,这将包括未指定授权属性的任何终结点。 对于在授权中间件之后由其他中间件提供的请求(如静态文件),这会将策略应用于所有请求。

将回退身份验证策略设置为要求用户进行身份验证可保护新添加的 Razor Pages 和控制器。 默认情况下,要求进行身份验证比依赖新控制器和 Razor Pages 来包含 [Authorize] 属性更安全。

AuthorizationOptions 类还包含 AuthorizationOptions.DefaultPolicy。 未指定策略时,DefaultPolicy 是与 [Authorize] 属性一起使用的策略。 [Authorize] 不包含命名策略,与 [Authorize(PolicyName="MyPolicy")] 不同。

有关策略详细信息,请参阅 ASP.NET Core 中基于策略的授权

MVC 控制器和 Razor Pages 要求所有用户进行身份验证的另一种方法是添加授权筛选器:

public void ConfigureServices(IServiceCollection services)
{

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

    services.AddRazorPages();

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

前面的代码使用授权筛选器,设置回退策略使用终结点路由。 设置回退策略是要求所有用户进行身份验证的首选方法。

AllowAnonymous 添加到 IndexPrivacy 页,以便匿名用户在注册之前可以获取有关站点的信息:

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

namespace ContactManager.Pages
{
    [AllowAnonymous]
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {

        }
    }
}

配置测试帐户

SeedData 类创建两个帐户:管理员和经理。 使用机密管理器工具为这些帐户设置密码。 从项目目录(目录包含 Program.cs)中设置密码:

dotnet user-secrets set SeedUserPW <PW>

如果未指定强密码,则调用 SeedData.Initialize 时会引发异常。

更新 Main 以使用测试密码:

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

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

            try
            {
                var context = services.GetRequiredService<ApplicationDbContext>();
                context.Database.Migrate();

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

                var testUserPw = config["SeedUserPW"];

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

        host.Run();
    }

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

创建测试帐户并更新联系人

更新 SeedData 类中的 Initialize 方法,以创建测试帐户:

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

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

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

        SeedDB(context, adminID);
    }
}

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

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

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

    return user.Id;
}

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

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

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

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

    //if (userManager == null)
    //{
    //    throw new Exception("userManager is null");
    //}

    var user = await userManager.FindByIdAsync(uid);

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

    IR = await userManager.AddToRoleAsync(user, role);

    return IR;
}

向联系人添加管理员用户 ID 和 ContactStatus。 使其中一个联系人为“已提交”,一个为“已拒绝”。 向所有联系人添加用户 ID 和状态。 只显示一个联系人:

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

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

创建所有者、经理和管理员授权处理程序

在“授权”文件夹中创建 ContactIsOwnerAuthorizationHandler 类。 ContactIsOwnerAuthorizationHandler 验证对资源进行操作的用户是否拥有该资源。

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

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

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

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for CRUD permission, return.

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

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

            return Task.CompletedTask;
        }
    }
}

如果当前经过身份验证的用户是联系人所有者,则 ContactIsOwnerAuthorizationHandler 会调用 context.Succeed。 授权处理程序通常:

  • 满足要求时调用 context.Succeed
  • 未满足要求时返回 Task.CompletedTask。 在未事先调用 context.Successcontext.Fail 的情况下返回 Task.CompletedTask 不是成功或失败,它允许运行其他授权处理程序。

如果需要显式失败,请调用 context.Fail

该应用使联系人所有者可编辑/删除/创建自己的数据。 ContactIsOwnerAuthorizationHandler 不需要检查在要求参数中传递的操作。

创建经理授权处理程序

在“授权”文件夹中创建 ContactManagerAuthorizationHandler 类。 ContactManagerAuthorizationHandler 验证对资源进行操作的用户是否是经理。 只有经理才能批准或拒绝内容更改(新的或已更改的)。

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

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.CompletedTask;
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.CompletedTask;
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

创建管理员授权处理程序

在“授权”文件夹中创建 ContactAdministratorsAuthorizationHandler 类。 ContactAdministratorsAuthorizationHandler 验证对资源进行操作的用户是否是管理员。 管理员可以执行所有操作。

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

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.CompletedTask;
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

注册授权处理程序

使用 Entity Framework Core 的服务必须使用 AddScoped 注册依赖项注入ContactIsOwnerAuthorizationHandler 使用 ASP.NET Core Identity,其基于 Entity Framework Core。 将处理程序注册到服务集合,以便通过依赖注入ContactsController 可用。 将以下代码添加到 ConfigureServices 的末尾:

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

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

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

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

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

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler 作为单一实例添加。 它们是单一实例,因为不使用 EF,并且所需的全部信息都位于 HandleRequirementAsync 方法的 Context 参数中。

支持授权

在本部分中,将更新 Razor 页面并添加操作要求类。

查看联系人操作要求类

查看 ContactOperations 类。 此类包含应用支持的要求:

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

为联系人 Razor Pages 创建基类

创建一个包含在联系人 Razor Pages 中使用的服务的基类。 基类将初始化代码放在一个位置:

using ContactManager.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace ContactManager.Pages.Contacts
{
    public class DI_BasePageModel : PageModel
    {
        protected ApplicationDbContext Context { get; }
        protected IAuthorizationService AuthorizationService { get; }
        protected UserManager<IdentityUser> UserManager { get; }

        public DI_BasePageModel(
            ApplicationDbContext context,
            IAuthorizationService authorizationService,
            UserManager<IdentityUser> userManager) : base()
        {
            Context = context;
            UserManager = userManager;
            AuthorizationService = authorizationService;
        } 
    }
}

前面的代码:

  • 添加 IAuthorizationService 服务以访问授权处理程序。
  • 添加 IdentityUserManager 服务。
  • 添加 ApplicationDbContext

更新 CreateModel

更新“创建”页模型构造函数以使用 DI_BasePageModel 基类:

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

更新 CreateModel.OnPostAsync 方法以执行以下操作:

  • 将用户 ID 添加到 Contact 模型。
  • 调用授权处理程序以验证用户是否有权创建联系人。
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Contact.OwnerID = UserManager.GetUserId(User);

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

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

    return RedirectToPage("./Index");
}

更新 IndexModel

更新 OnGetAsync 方法以便仅向一般用户显示已批准的联系人:

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

    public IList<Contact> Contact { get; set; }

    public async Task OnGetAsync()
    {
        var contacts = from c in Context.Contact
                       select c;

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

        var currentUserId = UserManager.GetUserId(User);

        // Only approved contacts are shown UNLESS you're authorized to see them
        // or you are the owner.
        if (!isAuthorized)
        {
            contacts = contacts.Where(c => c.Status == ContactStatus.Approved
                                        || c.OwnerID == currentUserId);
        }

        Contact = await contacts.ToListAsync();
    }
}

更新 EditModel

添加授权处理程序来验证用户是否拥有该联系人。 由于正在验证资源授权,因此 [Authorize] 属性不够。 评估属性时,应用无法访问资源。 基于资源的授权必须是必需的。 如果应用有权访问该资源,则必须执行检查,方法是将其加载到页面模型中,或在处理程序本身中加载它。 通过传入资源键,可以频繁地访问资源。

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

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

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

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

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

        return Page();
    }

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

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

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

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

        Contact.OwnerID = contact.OwnerID;

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

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

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

        await Context.SaveChangesAsync();

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

更新 DeleteModel

更新“删除”页模型,以使用授权处理程序来验证用户是否具有对联系人的“删除”权限。

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

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

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

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

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

        return Page();
    }

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

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

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

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

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

将授权服务注入视图

目前,UI 会显示用户不能修改的联系人的编辑和删除链接。

将授权服务注入 Pages/_ViewImports.cshtml 文件,以便它可用于所有视图:

@using Microsoft.AspNetCore.Identity
@using ContactManager
@using ContactManager.Data
@namespace ContactManager.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using ContactManager.Authorization;
@using Microsoft.AspNetCore.Authorization
@using ContactManager.Models
@inject IAuthorizationService AuthorizationService

前面的标记添加多个 using 语句。

更新 Pages/Contacts/Index.cshtml 中的编辑和删除链接,以便仅为具有相应权限的用户呈现它们:

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Contact)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Address)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.City)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.State)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Zip)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Email)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

警告

隐藏不具有更改数据权限的用户的链接不会保护应用的安全。 隐藏链接通过仅显示有效链接使应用更加用户友好。 用户可以通过攻击生成的 URL 来对其不拥有的数据调用编辑和删除操作。 Razor Page 或控制器必须强制进行访问检查以确保数据的安全。

更新详细信息

更新详细信息视图,以便经理可以批准或拒绝联系人:

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

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

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

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

更新“详细信息”页模型:

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

    public Contact Contact { get; set; }

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

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

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

        var currentUserId = UserManager.GetUserId(User);

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

        return Page();
    }

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

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

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

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

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

向角色添加或删除用户

有关以下内容的信息,请参阅此问题

  • 正在删除用户的权限。 例如,在聊天应用中对用户静音。
  • 正在向用户添加权限。

质询与禁止之间的区别

此应用将默认策略设置为需要经过身份验证的用户。 以下代码允许匿名用户。 允许匿名用户显示质询与禁止之间的区别。

[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
    public Details2Model(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<IdentityUser> userManager)
        : base(context, authorizationService, userManager)
    {
    }

    public Contact Contact { get; set; }

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

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

        if (!User.Identity.IsAuthenticated)
        {
            return Challenge();
        }

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

        var currentUserId = UserManager.GetUserId(User);

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

        return Page();
    }
}

在上述代码中:

  • 如果用户未经过身份验证,则返回 ChallengeResult。 返回 ChallengeResult 后,会将用户重定向到登录页。
  • 如果用户已经过身份验证,但未授权,则返回 ForbidResult。 返回 ForbidResult 后,会将用户重定向到拒绝访问页。

测试已完成的应用

如果尚未为种子用户帐户设置密码,请使用机密管理器工具设置密码:

  • 选择强密码:使用八个或更多字符,并且至少使用一个大写字符、数字和符号。 例如,Passw0rd! 满足强密码要求。

  • 从项目的文件夹中执行以下命令,其中 <PW> 为密码:

    dotnet user-secrets set SeedUserPW <PW>
    

如果应用有联系人:

  • 删除 Contact 表中的所有记录。
  • 重启应用以设定数据库种子。

测试已完成应用的一种简单方法是启动三个不同的浏览器(或 incognito/InPrivate 会话)。 在一个浏览器中,注册新用户(例如 test@example.com)。 使用其他用户登录到每个浏览器。 验证以下操作:

  • 已注册的用户可以查看所有已批准的联系人数据。
  • 已注册的用户可以编辑/删除他们自己的数据。
  • 经理可以批准/拒绝联系人数据。 Details 视图显示“批准”和“拒绝”按钮。
  • 管理员可以批准/拒绝和编辑/删除任何数据。
用户 由应用设定种子 选项
test@example.com 编辑/删除自己的数据。
manager@contoso.com 批准/拒绝和编辑/删除自己的数据。
admin@contoso.com 批准/拒绝和编辑/删除所有数据。

在管理员的浏览器中创建联系人。 复制管理员联系人的“删除”和“编辑”URL。 将这些链接粘贴到测试用户的浏览器中,以验证测试用户是否无法执行这些操作。

创建初学者应用

  • 创建名为“ContactManager”的 Razor Pages 应用

    • 通过个人用户帐户创建应用。
    • 将其命名为“ContactManager”,使命名空间与该示例中使用的命名空间匹配。
    • -uld 指定 LocalDB,而不是 SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • 添加 Models/Contact.cs

    public class Contact
    {
        public int ContactId { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
    }
    
  • 搭建 Contact 模型的基架。

  • 创建初始迁移并更新数据库:

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

注意

默认情况下,要安装的 .NET 二进制文件的体系结构表示当前运行的 OS 体系结构。 若要指定不同的 OS 体系结构,请参阅 dotnet tool install, --arch option。 有关详细信息,请参阅 GitHub 问题 dotnet/AspNetCore.Docs #29262

如果在使用 dotnet aspnet-codegenerator razorpage 命令时遇到 bug,请参阅此 GitHub 问题

  • 更新 Pages/Shared/_Layout.cshtml 文件中的 ContactManager 定位点:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • 通过创建、编辑和删除联系人来测试应用

设定数据库种子

SeedData 类添加到“数据”文件夹:

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

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

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

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

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

    }
}

Main 调用 SeedData.Initialize

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

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

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

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

            host.Run();
        }

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

测试应用是否为数据库设定种子。 如果联系人数据库中存在任何行,则 seed 方法不会运行。

其他资源