使用受授權保護的使用者資料建立 ASP.NET Core web 應用程式Create an ASP.NET Core web app with user data protected by authorization

作者:Rick AndersonJoe AudetteBy Rick Anderson and Joe Audette

請參閱 此 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

本教學課程是 advanced。This tutorial is advanced. 您應該熟悉:You should be familiar with:

入門和完成的應用程式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. 若要建立入門應用程式,請參閱 建立入門應用程式To create the starter app, see Create the starter app.

保護使用者資料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 Identity 使用者識別碼,以確保使用者可以編輯其資料,而不是其他使用者資料。Use the ASP.NET Identity user ID to ensure users can edit their data, but not other users data. OwnerID 和加入 ContactStatusContact 模型: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 這是 AspNetUser 資料庫中資料表的使用者識別碼 IdentityOwnerID 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

將角色服務新增至 IdentityAdd 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 fallback 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.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

上述反白顯示的程式碼會設定回溯 驗證原則The preceding highlighted code sets the fallback authentication policy. 除了具有驗證屬性的頁面、控制器或動作方法之外,fallback 驗證原則需要驗證 *all _ 使用者 Razor 。The fallback authentication policy requires *all _ users to be authenticated, except for Razor Pages, controllers, or action methods with an authentication attribute. 例如,使用 Razor 或的頁面、控制器或動作方法,會 [AllowAnonymous] [Authorize(PolicyName="MyPolicy")] 使用套用的驗證屬性,而不是回溯驗證原則。For example, Razor Pages, controllers, or action methods with [AllowAnonymous] or [Authorize(PolicyName="MyPolicy")] use the applied authentication attribute rather than the fallback authentication policy.

RequireAuthenticatedUser 加入 DenyAnonymousAuthorizationRequirement 目前的實例,這會強制驗證目前的使用者。RequireAuthenticatedUser adds DenyAnonymousAuthorizationRequirement to the current instance, which enforces that the current user is authenticated.

回退驗證原則:The fallback authentication policy:

_ 會套用至未明確指定驗證原則的所有要求。_ Is applied to all requests that do not explicitly specify an authentication policy. 針對端點路由所提供的要求,這會包含未指定授權屬性的任何端點。For requests served by endpoint routing, this would include any endpoint that does not specify an authorization attribute. 針對其他中介軟體在授權中介軟體之後所提供的要求(例如 靜態檔案),這會將原則套用至所有要求。For requests served by other middleware after the authorization middleware, such as static files, this would apply the policy to all requests.

將回溯驗證原則設定為要求使用者進行驗證,可保護新新增 Razor 的頁面和控制器。Setting the fallback 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.

AuthorizationOptions類別也包含 AuthorizationOptions.DefaultPolicyThe AuthorizationOptions class also contains AuthorizationOptions.DefaultPolicy. DefaultPolicy[Authorize] 未指定任何原則時,與屬性搭配使用的原則。The DefaultPolicy is the policy used with the [Authorize] attribute when no policy is specified. [Authorize] 不包含命名原則,與不同 [Authorize(PolicyName="MyPolicy")][Authorize] doesn't contain a named policy, unlike [Authorize(PolicyName="MyPolicy")].

如需原則的詳細資訊,請參閱 ASP.NET Core 中以原則為基礎的授權For more information on policies, see ASP.NET Core 中以原則為基礎的授權.

MVC 控制器和 Razor 頁面需要驗證所有使用者的另一種方法是新增授權篩選準則:An alternative way for MVC controllers and Razor Pages to require all users be authenticated is adding an authorization filter:

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

上述程式碼會使用授權篩選準則,設定 fallback 原則會使用端點路由。The preceding code uses an authorization filter, setting the fallback policy uses endpoint routing. 設定回復原則是要求所有使用者進行驗證的慣用方式。Setting the fallback policy is the preferred way to require all users be authenticated.

AllowAnonymous 新增至 IndexPrivacy 頁面,讓匿名使用者在註冊之前可以取得網站的相關資訊: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. 使用 Secret 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.InitializeIf 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;
}

將系統管理員使用者識別碼和新增 ContactStatus 至連絡人。Add the administrator user ID and ContactStatus to the contacts. 讓其中一個連絡人成為「已提交」,另一個「已拒絕」。Make one of the contacts "Submitted" and one "Rejected". 將使用者識別碼和狀態新增至所有連絡人。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 Identity ,以 Entity Framework Core 為基礎。The ContactIsOwnerAuthorizationHandler uses ASP.NET Core Identity, which is built on Entity Framework Core. 使用服務集合註冊處理常式,以便透過相依性插入來使用它們 ContactsControllerRegister the handlers with the service collection so they're available to the ContactsController through dependency injection. 將下列程式碼加入至結尾 ConfigureServicesAdd 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.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

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

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

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

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler 會新增為 singleton。ContactAdministratorsAuthorizationHandler and ContactManagerAuthorizationHandler are added as singletons. 它們是 singleton 的,因為它們不會使用 EF,而且所需的所有資訊都在 Context 方法的參數中 HandleRequirementAsyncThey'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.

複習 contact 作業需求課程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";
    }
}

建立連絡人頁面的基類 RazorCreate 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.
  • 加入 Identity 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:

  • 將使用者識別碼加入至 Contact 模型。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

更新 [刪除] 頁面模型,以使用授權處理常式來確認使用者擁有連絡人的 [刪除] 許可權。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");
    }
}

將授權服務插入至 viewsInject 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 中的 [編輯] 和 [刪除] 連結,以便只針對具有適當許可權的使用者轉譯這些連結: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. 使用者可以攻擊產生的 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:

  • 當使用者 未通過驗證 時, ChallengeResult 就會傳回。When the user is not authenticated, a ChallengeResult is returned. ChallengeResult 傳回時,會將使用者重新導向至登入頁面。When a ChallengeResult is returned, the user is redirected to the sign-in page.
  • 當使用者經過驗證但未獲授權時, ForbidResult 會傳回。When 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

如果您尚未設定植入使用者帳戶的密碼,請使用 Secret Manager 工具 來設定密碼: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:

  • 刪除資料表中的所有記錄 ContactDelete 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 YesYes 核准/拒絕和編輯/刪除自己的資料。Approve/reject and edit/delete own data.
admin@contoso.com YesYes 核准/拒絕和編輯/刪除所有資料。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

  • 建立 Razor 名為 "ContactManager" 的頁面應用程式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
    
  • 新增 模型/連絡人 .csAdd 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; }
    }
    
  • Scaffold 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 ,請參閱 此 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.cs 類別新增至 [ 資料 ] 資料夾: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.Initialize 來源 MainCall 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. 如果 contact DB 中有任何資料列,種子方法就不會執行。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

本教學課程是 advanced。This tutorial is advanced. 您應該熟悉:You should be familiar with:

入門和完成的應用程式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 Identity 使用者識別碼,以確保使用者可以編輯其資料,而不是其他使用者資料。Use the ASP.NET Identity user ID to ensure users can edit their data, but not other users data. OwnerID 和加入 ContactStatusContact 模型: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 這是 AspNetUser 資料庫中資料表的使用者識別碼 IdentityOwnerID 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

將角色服務新增至 IdentityAdd 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 加入至 Index、About 和 Contact 頁面,讓匿名使用者在註冊之前,可以取得網站的相關資訊。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. 使用 Secret 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.InitializeIf 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;
}

將系統管理員使用者識別碼和新增 ContactStatus 至連絡人。Add the administrator user ID and ContactStatus to the contacts. 讓其中一個連絡人成為「已提交」,另一個「已拒絕」。Make one of the contacts "Submitted" and one "Rejected". 將使用者識別碼和狀態新增至所有連絡人。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 Identity ,以 Entity Framework Core 為基礎。The ContactIsOwnerAuthorizationHandler uses ASP.NET Core Identity, which is built on Entity Framework Core. 使用服務集合註冊處理常式,以便透過相依性插入來使用它們 ContactsControllerRegister the handlers with the service collection so they're available to the ContactsController through dependency injection. 將下列程式碼加入至結尾 ConfigureServicesAdd 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 會新增為 singleton。ContactAdministratorsAuthorizationHandler and ContactManagerAuthorizationHandler are added as singletons. 它們是 singleton 的,因為它們不會使用 EF,而且所需的所有資訊都在 Context 方法的參數中 HandleRequirementAsyncThey'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.

複習 contact 作業需求課程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";
    }
}

建立連絡人頁面的基類 RazorCreate 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.
  • 加入 Identity 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:

  • 將使用者識別碼加入至 Contact 模型。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

更新 [刪除] 頁面模型,以使用授權處理常式來確認使用者擁有連絡人的 [刪除] 許可權。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");
    }
}

將授權服務插入至 viewsInject 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 中的 [編輯] 和 [刪除] 連結,以便只針對具有適當許可權的使用者轉譯這些連結: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. 使用者可以攻擊產生的 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

如果您尚未設定植入使用者帳戶的密碼,請使用 Secret Manager 工具 來設定密碼: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 YesYes 核准/拒絕和編輯/刪除自己的資料。Approve/reject and edit/delete own data.
admin@contoso.com YesYes 核准/拒絕和編輯/刪除所有資料。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

  • 建立 Razor 名為 "ContactManager" 的頁面應用程式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
    
  • 新增 模型/連絡人 .csAdd 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; }
    }
    
  • Scaffold 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
    
  • 更新 Pages/_Layout cshtml 檔案中的 ContactManager 錨點: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.cs 類別加入至 [ 資料 ] 資料夾。Add the SeedData class to the Data folder.

呼叫 SeedData.Initialize 來源 MainCall 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. 如果 contact DB 中有任何資料列,種子方法就不會執行。If there are any rows in the contact DB, the seed method doesn't run.

其他資源Additional resources