使用以授權機制保護的使用者資料來建立 ASP.NET Core Web 應用程式

作者:Rick AndersonJoe Audette

本教學課程示範如何使用以授權機制保護的使用者資料來建立 ASP.NET Core Web 應用程式。 它會顯示已驗證 (已註冊) 使用者已建立的連絡人清單。 有三個安全性群組:

  • 已註冊的使用者可以檢視所有已核准的資料,且能編輯/刪除自己的資料。
  • 管理員可以核准或拒絕連絡人資料。 使用者只能看到核准的連絡人。
  • 系統管理員可以核准/拒絕以及編輯/刪除任何資料。

本文件中的影像與最新的範本不完全相符。

在下圖中,使用者 Rick (rick@example.com) 已登入。 Rick 只能檢視已核准的連絡人,以及 [編輯]/[刪除]/[新建] 連絡人的連結。 只有 Rick 建立的最後一筆記錄會顯示 [編輯] 和 [刪除] 連結。 在管理員或系統管理員將狀態變更為「已核准」之前,其他使用者不會看到最後一筆記錄。

Screenshot showing Rick signed in

在下圖中,manager@contoso.com 已登入且為管理員的角色:

Screenshot showing manager@contoso.com signed in

下圖顯示連絡人的管理員詳細資料檢視:

Manager's view of a contact

只有管理員和系統管理員才會看到 [核准] 和 [拒絕] 按鈕。

在下圖中,admin@contoso.com 已登入且為系統管理員的角色:

Screenshot showing admin@contoso.com signed in

系統管理員擁有所有權限。 她可以讀取、編輯或刪除任何連絡人,以及變更連絡人的狀態。

應用程式是透過對下列 Contact 模型執行 Scaffolding 所建立:

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

此範例包含下列授權處理常式:

  • ContactIsOwnerAuthorizationHandler:確保使用者只能編輯其資料。
  • ContactManagerAuthorizationHandler:允許管理員核准或拒絕連絡人。
  • ContactAdministratorsAuthorizationHandler:允許系統管理員核准或拒絕連絡人,以及編輯/刪除連絡人。

必要條件

此為進階教學課程。 您應熟悉:

入門和已完成的應用程式

下載完整應用程式。 測試已完成的應用程式,讓您熟悉其安全性功能。

入門應用程式

下載入門應用程式。

執行應用程式、點選 ContactManager 連結,並確認您可以建立、編輯和刪除連絡人。 若要建立入門應用程式,請參閱建立入門應用程式

保護使用者資料

下列各節具有建立安全使用者資料應用程式的所有主要步驟。 您可能會發現參考已完成的專案會很有幫助。

將連絡人資料繫結至使用者

使用 ASP.NET Identity 使用者識別碼來確保使用者可以編輯其資料,但不能編輯其他使用者的資料。 將 OwnerIDContactStatus 新增至 Contact 模型:

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

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

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

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerIDIdentity 資料庫中 AspNetUser 資料表的使用者識別碼。 Status 欄位會決定一般使用者是否可以檢視連絡人。

建立新的移轉並更新資料庫:

dotnet ef migrations add userID_Status
dotnet ef database update

將角色服務新增至 Identity

附加 AddRoles 以新增角色服務:

var builder = WebApplication.CreateBuilder(args);

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

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

要求已驗證的使用者

將後援授權原則設定為要求使用者進行驗證:

var builder = WebApplication.CreateBuilder(args);

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

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

builder.Services.AddRazorPages();

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

上述醒目提示的程式碼會設定後援授權原則。 後援授權原則需要驗證所有使用者,但 Razor Pages、控制器或具有授權屬性的動作方法除外。 例如,Razor Pages、控制器或搭配 [AllowAnonymous][Authorize(PolicyName="MyPolicy")] 的動作方法會使用套用的授權屬性,而不是後援授權原則。

RequireAuthenticatedUser 會將 DenyAnonymousAuthorizationRequirement 新增至目前的執行個體,這會強制執行目前的使用者已驗證。

後援授權原則:

  • 會套用至未明確指定授權原則的所有要求。 對於端點路由所提供的要求,這包括未指定授權屬性的任何端點。 對於授權中介軟體之後由其他中介軟體提供的要求,例如靜態檔案,這會將原則套用至所有要求。

將後援授權原則設定為要求使用者通過驗證來保護新增的 Razor Pages和控制器。 根據預設設定為需要授權比依賴新的控制器和 Razor Pages 來包含 [Authorize] 屬性更安全。

AuthorizationOptions 類別也包含 AuthorizationOptions.DefaultPolicyDefaultPolicy 是未指定任何原則時,與 [Authorize] 屬性搭配使用的原則。 [Authorize] 不包含具名原則,與 [Authorize(PolicyName="MyPolicy")] 不同。

如需原則的詳細資訊,請參閱 ASP.NET Core 中的原則型授權

MVC 控制器和 Razor Pages要求所有使用者通過驗證的替代方式是新增授權篩選:

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

var builder = WebApplication.CreateBuilder(args);

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

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

builder.Services.AddRazorPages();

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

var app = builder.Build();

上述程式碼會使用授權篩選器,設定後援原則會使用端點路由。 設定後援原則是要求所有使用者通過驗證的慣用方式。

AllowAnonymous 新增至 IndexPrivacy 頁面,讓匿名使用者可以在註冊之前取得網站的相關資訊:

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

namespace ContactManager.Pages;

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

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

    public void OnGet()
    {

    }
}

設定測試帳戶

SeedData 類別會建立兩個帳戶:系統管理員和管理員。 使用祕密管理員工具來設定這些帳戶的密碼。 從專案目錄設定密碼 (包含 Program.cs 的目錄):

dotnet user-secrets set SeedUserPW <PW>

如果未指定強式密碼,則會在呼叫 SeedData.Initialize 時擲回例外狀況。

更新應用程式以使用測試密碼:

var builder = WebApplication.CreateBuilder(args);

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

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

builder.Services.AddRazorPages();

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

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

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

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

var app = builder.Build();

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

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

   await SeedData.Initialize(services, testUserPw);
}

建立測試帳戶並更新連絡人

更新 SeedData 類別中的 Initialize 方法,以建立測試帳戶:

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

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

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

        SeedDB(context, adminID);
    }
}

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

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

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

    return user.Id;
}

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

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

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

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

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

    var user = await userManager.FindByIdAsync(uid);

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

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

    return IR;
}

將系統管理員使用者識別碼和 ContactStatus 新增至連絡人。 將其中一個連絡人設為「已提交」而另一個設為「已拒絕」。 將使用者識別碼和狀態新增至所有連絡人。 只會顯示一個連絡人:

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

建立擁有者、管理員和系統管理員授權處理常式

Authorization 資料夾中建立 ContactIsOwnerAuthorizationHandler 類別。 ContactIsOwnerAuthorizationHandler 會確認對資源採取行動的使用者是否擁有資源。

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

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

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

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

            // If not asking for CRUD permission, return.

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

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

            return Task.CompletedTask;
        }
    }
}

如果目前已驗證的使用者是連絡人擁有者,則 ContactIsOwnerAuthorizationHandler 會呼叫 context.Succeed。 授權處理常式通常會:

  • 符合需求時呼叫 context.Succeed
  • 不符合需求時傳回 Task.CompletedTask。 在沒有先前呼叫 context.Successcontext.Fail 的情況下傳回 Task.CompletedTask 不屬於成功或失敗,它可讓其他授權處理常式執行。

如果您需要明確失敗,請呼叫 context.Fail

應用程式可讓連絡人擁有者編輯/刪除/建立自己的資料。 ContactIsOwnerAuthorizationHandler 不需要檢查在需求參數中傳遞的作業。

建立管理員授權處理常式

Authorization 資料夾中建立 ContactManagerAuthorizationHandler 類別。 ContactManagerAuthorizationHandler 會確認對資源採取行動的使用者是管理員。 只有管理員可以核准或拒絕內容變更 (新增或變更)。

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

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

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

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

            return Task.CompletedTask;
        }
    }
}

建立系統管理員授權處理常式

Authorization 資料夾中建立 ContactAdministratorsAuthorizationHandler 類別。 ContactAdministratorsAuthorizationHandler 會確認對資源採取行動的使用者是系統管理員。 系統管理員可以執行所有作業。

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

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

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

            return Task.CompletedTask;
        }
    }
}

註冊授權處理常式

使用 Entity Framework Core 的服務必須註冊才能使用 AddScoped 進行相依性插入ContactIsOwnerAuthorizationHandler 會使用建置在 Entity Framework Core 上的 ASP.NET Core Identity。 向服務集合註冊處理常式,以便透過相依性插入提供給 ContactsController。 在 ConfigureServices 的結尾新增下列程式碼:

var builder = WebApplication.CreateBuilder(args);

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

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

builder.Services.AddRazorPages();

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

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

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

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

var app = builder.Build();

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

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

   await SeedData.Initialize(services, testUserPw);
}

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler 會新增為單一資料庫。 其為單一資料庫,因為它們不會使用 EF,而所需的所有資訊都是在 HandleRequirementAsync 方法的 Context 參數中。

支援授權

在本節中,您會更新 Razor Pages 並新增作業需求類別。

檢閱連絡人作業需求類別

檢閱 ContactOperations 類別。 此類別包含應用程式支援的需求:

using Microsoft.AspNetCore.Authorization.Infrastructure;

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

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

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

建立連絡人 Razor Pages 的基底類別

建立基底類別,其中包含連絡人 Razor Pages 中使用的服務。 基底類別會將初始化程式碼放在單一位置:

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

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

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

上述 程式碼:

  • IAuthorizationService 服務新增至授權處理常式的存取權。
  • 新增 IdentityUserManager 服務。
  • 加入 ApplicationDbContext

更新 CreateModel

更新建立頁面模型:

  • 使用 DI_BasePageModel 基底類別的建構函式。
  • OnPostAsync 方法可:
    • 將使用者識別碼新增至 Contact 模型。
    • 呼叫授權處理常式,以確認使用者具有建立連絡人的權限。
using ContactManager.Authorization;
using ContactManager.Data;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

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

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

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

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

            Contact.OwnerID = UserManager.GetUserId(User);

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

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

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

更新 IndexModel

更新 OnGetAsync 方法,以僅向一般使用者顯示已核准的連絡人:

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

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

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

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

        var currentUserId = UserManager.GetUserId(User);

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

        Contact = await contacts.ToListAsync();
    }
}

更新 EditModel

新增授權處理常式,以確認使用者擁有連絡人。 由於正在驗證資源授權,因此 [Authorize] 屬性是不夠的。 評估屬性時,應用程式無法存取資源。 資源型授權必須是命令式的。 一旦應用程式能夠存取資源,就必須執行檢查,方法是在頁面模型中載入資源,或在處理常式本身內載入它。 您經常透過傳入資源金鑰來存取資源。

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

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

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

        Contact = contact;

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

        return Page();
    }

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

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

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

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

        Contact.OwnerID = contact.OwnerID;

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

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

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

        await Context.SaveChangesAsync();

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

更新 DeleteModel

更新刪除頁面模型,以使用授權處理常式來確認使用者具有連絡人的刪除權限。

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

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

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

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

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

        return Page();
    }

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

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

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

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

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

將授權服務插入檢視

目前,UI 會顯示使用者無法修改之連絡人的編輯和刪除連結。

將授權服務插入 Pages/_ViewImports.cshtml 檔案中,以便可供所有檢視使用:

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

上述標記會新增數個 using 陳述式。

更新 Pages/Contacts/Index.cshtml 中的 [編輯] 和 [刪除] 連結,使其僅針對具有適當權限的使用者進行轉譯:

@page
@model ContactManager.Pages.Contacts.IndexModel

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

<h1>Index</h1>

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

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

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

警告

隱藏沒有變更資料權限之使用者的連結,並不會保護應用程式的安全。 隱藏連結可讓應用程式更易於使用,也就是只顯示有效的連結。 使用者可以對產生的 URL 進行駭客攻擊,以叫用他們未擁有資料的編輯和刪除作業。 Razor Pages 或控制器必須強制執行存取檢查才能保護資料的安全。

更新詳細資料

更新詳細資料檢視,讓管理員可以核准或拒絕連絡人:

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

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

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

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

更新詳細資料頁面模型

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

    public Contact Contact { get; set; }

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

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

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

        var currentUserId = UserManager.GetUserId(User);

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

        return Page();
    }

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

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

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

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

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

將使用者新增至角色或移除使用者

如需下列資訊,請參閱這個問題

  • 從使用者移除權限。 例如,在聊天應用程式中將使用者靜音。
  • 將權限新增至使用者。

挑戰與禁止之間的差異

此應用程式會將預設原則設定為要求已驗證的使用者。 下列程式碼允許匿名使用者。 允許匿名使用者顯示挑戰與禁止之間的差異。

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

    public Contact Contact { get; set; }

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

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

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

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

        var currentUserId = UserManager.GetUserId(User);

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

        return Page();
    }
}

在上述程式碼中:

  • 當使用者通過驗證時,即會傳回 ChallengeResult。 傳回 ChallengeResult 時,使用者會重新導向至登入頁面。
  • 當使用者經過驗證但未獲得授權時,即會傳回 ForbidResult。 傳回 ForbidResult 時,使用者會重新導向至拒絕存取的頁面。

測試已完成的應用程式

如果您尚未為植入的使用者帳戶設定密碼,請使用祕密管理員工具來設定密碼:

  • 選擇強式密碼:使用 8 個以上的字元,以及至少一個大寫字元、數字和符號。 例如,Passw0rd! 符合強式密碼需求。

  • 從專案的資料夾中執行下列命令,其中 <PW> 是密碼:

    dotnet user-secrets set SeedUserPW <PW>
    

如果應用程式具有連絡人:

  • 刪除 Contact 資料表中的所有記錄。
  • 重新啟動應用程式以植入資料庫。

測試已完成應用程式的簡單方式是啟動三種不同的瀏覽器 (或 incognito/InPrivate 工作階段)。 在一個瀏覽器中,註冊新的使用者 (例如 test@example.com)。 以不同的使用者登入每個瀏覽器。 確認下列作業:

  • 已註冊的使用者可以檢視所有已核准的連絡人資料。
  • 已註冊的使用者可以編輯/刪除自己的資料。
  • 管理員可以核准/拒絕連絡人資料。 Details 檢視會顯示 [核准] 和 [拒絕] 按鈕。
  • 系統管理員可以核准/拒絕以及編輯/刪除所有資料。
使用者 核准或拒絕連絡人 選項。
test@example.com No 編輯和刪除其資料。
manager@contoso.com Yes 編輯和刪除其資料。
admin@contoso.com Yes 編輯和刪除所有資料。

在系統管理員的瀏覽器中建立連絡人。 從系統管理員連絡人複製要刪除和編輯的 URL。 將這些連結貼到測試使用者的瀏覽器中,以確認測試使用者無法執行這些作業。

建立入門應用程式

  • 建立名為「ContactManager」的 Razor Pages 應用程式

    • 使用個別使用者帳戶建立應用程式。
    • 將其命名為「ContactManager」,使命名空間符合範例中使用的命名空間。
    • -uld 會指定 LocalDB,而不是 SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • 新增 Models/Contact.cs:secure-data\samples\starter6\ContactManager\Models\Contact.cs

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

  • 建立初始移轉並更新資料庫:

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

注意

根據預設,要安裝的 .NET 二進位檔架構代表目前執行的 OS 架構。 若要指定不同的 OS 架構,請參閱 dotnet tool install, --arch option。 如需詳細資訊,請參閱 GitHub 問題 dotnet/AspNetCore.Docs #29262

  • 更新 Pages/Shared/_Layout.cshtml 檔案中的 ContactManager 錨點:

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • 建立、編輯和刪除連絡人以測試應用程式

植入資料庫

SeedData 類別新增至 Data 資料夾:

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;

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

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

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

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

    }
}

Program.cs 呼叫 SeedData.Initialize

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

var builder = WebApplication.CreateBuilder(args);

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

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

var app = builder.Build();

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

    await SeedData.Initialize(services);
}

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

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

app.UseRouting();

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

app.MapRazorPages();

app.Run();

測試應用程式已植入資料庫。 如果連絡人資料庫中有任何資料列,則植入方法不會執行。

本教學課程示範如何使用以授權機制保護的使用者資料來建立 ASP.NET Core Web 應用程式。 它會顯示已驗證 (已註冊) 使用者已建立的連絡人清單。 有三個安全性群組:

  • 已註冊的使用者可以檢視所有已核准的資料,且能編輯/刪除自己的資料。
  • 管理員可以核准或拒絕連絡人資料。 使用者只能看到核准的連絡人。
  • 系統管理員可以核准/拒絕以及編輯/刪除任何資料。

本文件中的影像與最新的範本不完全相符。

在下圖中,使用者 Rick (rick@example.com) 已登入。 Rick 只能檢視已核准的連絡人,以及 [編輯]/[刪除]/[新建] 連絡人的連結。 只有 Rick 建立的最後一筆記錄會顯示 [編輯] 和 [刪除] 連結。 在管理員或系統管理員將狀態變更為「已核准」之前,其他使用者不會看到最後一筆記錄。

Screenshot showing Rick signed in

在下圖中,manager@contoso.com 已登入且為管理員的角色:

Screenshot showing manager@contoso.com signed in

下圖顯示連絡人的管理員詳細資料檢視:

Manager's view of a contact

只有管理員和系統管理員才會看到 [核准] 和 [拒絕] 按鈕。

在下圖中,admin@contoso.com 已登入且為系統管理員的角色:

Screenshot showing admin@contoso.com signed in

系統管理員擁有所有權限。 她可以讀取/編輯/刪除任何連絡人,以及變更連絡人的狀態。

應用程式是透過對下列 Contact 模型執行 Scaffolding 所建立:

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

此範例包含下列授權處理常式:

  • ContactIsOwnerAuthorizationHandler:確保使用者只能編輯其資料。
  • ContactManagerAuthorizationHandler:允許管理員核准或拒絕連絡人。
  • ContactAdministratorsAuthorizationHandler:允許系統管理員:
    • 核准或拒絕連絡人
    • 編輯和刪除連絡人

必要條件

此為進階教學課程。 您應熟悉:

入門和已完成的應用程式

下載完整應用程式。 測試已完成的應用程式,讓您熟悉其安全性功能。

入門應用程式

下載入門應用程式。

執行應用程式、點選 ContactManager 連結,並確認您可以建立、編輯和刪除連絡人。 若要建立入門應用程式,請參閱建立入門應用程式

保護使用者資料

下列各節具有建立安全使用者資料應用程式的所有主要步驟。 您可能會發現參考已完成的專案會很有幫助。

將連絡人資料繫結至使用者

使用 ASP.NET Identity 使用者識別碼來確保使用者可以編輯其資料,但不能編輯其他使用者的資料。 將 OwnerIDContactStatus 新增至 Contact 模型:

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

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

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

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerIDIdentity 資料庫中 AspNetUser 資料表的使用者識別碼。 Status 欄位會決定一般使用者是否可以檢視連絡人。

建立新的移轉並更新資料庫:

dotnet ef migrations add userID_Status
dotnet ef database update

將角色服務新增至 Identity

附加 AddRoles 以新增角色服務:

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

要求已驗證的使用者

將後援驗證原則設定為要求使用者通過驗證:

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

    services.AddRazorPages();

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

上述醒目提示的程式碼會設定後援驗證原則。 後援驗證原則需要驗證所有使用者,但 Razor Pages、控制器或具有驗證屬性的動作方法除外。 例如,Razor Pages、控制器或搭配 [AllowAnonymous][Authorize(PolicyName="MyPolicy")] 的動作方法會使用套用的驗證屬性,而不是後援驗證原則。

RequireAuthenticatedUser 會將 DenyAnonymousAuthorizationRequirement 新增至目前的執行個體,這會強制執行目前的使用者已驗證。

後援驗證原則:

  • 會套用至未明確指定驗證原則的所有要求。 對於端點路由所提供的要求,這將包括未指定授權屬性的任何端點。 對於授權中介軟體之後由其他中介軟體提供的要求,例如靜態檔案,這會將原則套用至所有要求。

將後援驗證原則設定為要求使用者通過驗證來保護新增的 Razor Pages和控制器。 根據預設設定為需要驗證比依賴新的控制器和 Razor Pages 來包含 [Authorize] 屬性更安全。

AuthorizationOptions 類別也包含 AuthorizationOptions.DefaultPolicyDefaultPolicy 是未指定任何原則時,與 [Authorize] 屬性搭配使用的原則。 [Authorize] 不包含具名原則,與 [Authorize(PolicyName="MyPolicy")] 不同。

如需原則的詳細資訊,請參閱 ASP.NET Core 中的原則型授權

MVC 控制器和 Razor Pages要求所有使用者通過驗證的替代方式是新增授權篩選:

public void ConfigureServices(IServiceCollection services)
{

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

    services.AddRazorPages();

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

上述程式碼會使用授權篩選器,設定後援原則會使用端點路由。 設定後援原則是要求所有使用者通過驗證的慣用方式。

AllowAnonymous 新增至 IndexPrivacy 頁面,讓匿名使用者可以在註冊之前取得網站的相關資訊:

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

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

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

        public void OnGet()
        {

        }
    }
}

設定測試帳戶

SeedData 類別會建立兩個帳戶:系統管理員和管理員。 使用祕密管理員工具來設定這些帳戶的密碼。 從專案目錄設定密碼 (包含 Program.cs 的目錄):

dotnet user-secrets set SeedUserPW <PW>

如果未指定強式密碼,則會在呼叫 SeedData.Initialize 時擲回例外狀況。

更新 Main 以使用測試密碼:

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

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

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

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

                var testUserPw = config["SeedUserPW"];

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

        host.Run();
    }

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

建立測試帳戶並更新連絡人

更新 SeedData 類別中的 Initialize 方法,以建立測試帳戶:

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

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

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

        SeedDB(context, adminID);
    }
}

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

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

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

    return user.Id;
}

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

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

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

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

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

    var user = await userManager.FindByIdAsync(uid);

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

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

    return IR;
}

將系統管理員使用者識別碼和 ContactStatus 新增至連絡人。 將其中一個連絡人設為「已提交」而另一個設為「已拒絕」。 將使用者識別碼和狀態新增至所有連絡人。 只會顯示一個連絡人:

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

建立擁有者、管理員和系統管理員授權處理常式

Authorization 資料夾中建立 ContactIsOwnerAuthorizationHandler 類別。 ContactIsOwnerAuthorizationHandler 會確認對資源採取行動的使用者是否擁有資源。

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

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

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

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

            // If not asking for CRUD permission, return.

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

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

            return Task.CompletedTask;
        }
    }
}

如果目前已驗證的使用者是連絡人擁有者,則 ContactIsOwnerAuthorizationHandler 會呼叫 context.Succeed。 授權處理常式通常會:

  • 符合需求時呼叫 context.Succeed
  • 不符合需求時傳回 Task.CompletedTask。 在沒有先前呼叫 context.Successcontext.Fail 的情況下傳回 Task.CompletedTask 不屬於成功或失敗,它可讓其他授權處理常式執行。

如果您需要明確失敗,請呼叫 context.Fail

應用程式可讓連絡人擁有者編輯/刪除/建立自己的資料。 ContactIsOwnerAuthorizationHandler 不需要檢查在需求參數中傳遞的作業。

建立管理員授權處理常式

Authorization 資料夾中建立 ContactManagerAuthorizationHandler 類別。 ContactManagerAuthorizationHandler 會確認對資源採取行動的使用者是管理員。 只有管理員可以核准或拒絕內容變更 (新增或變更)。

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

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

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

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

            return Task.CompletedTask;
        }
    }
}

建立系統管理員授權處理常式

Authorization 資料夾中建立 ContactAdministratorsAuthorizationHandler 類別。 ContactAdministratorsAuthorizationHandler 會確認對資源採取行動的使用者是系統管理員。 系統管理員可以執行所有作業。

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

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

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

            return Task.CompletedTask;
        }
    }
}

註冊授權處理常式

使用 Entity Framework Core 的服務必須註冊才能使用 AddScoped 進行相依性插入ContactIsOwnerAuthorizationHandler 會使用建置在 Entity Framework Core 上的 ASP.NET Core Identity。 向服務集合註冊處理常式,以便透過相依性插入提供給 ContactsController。 在 ConfigureServices 的結尾新增下列程式碼:

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

    services.AddRazorPages();

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

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

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

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

ContactAdministratorsAuthorizationHandlerContactManagerAuthorizationHandler 會新增為單一資料庫。 其為單一資料庫,因為它們不會使用 EF,而所需的所有資訊都是在 HandleRequirementAsync 方法的 Context 參數中。

支援授權

在本節中,您會更新 Razor Pages 並新增作業需求類別。

檢閱連絡人作業需求類別

檢閱 ContactOperations 類別。 此類別包含應用程式支援的需求:

using Microsoft.AspNetCore.Authorization.Infrastructure;

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

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

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

建立連絡人 Razor Pages 的基底類別

建立基底類別,其中包含連絡人 Razor Pages 中使用的服務。 基底類別會將初始化程式碼放在單一位置:

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

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

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

上述 程式碼:

  • IAuthorizationService 服務新增至授權處理常式的存取權。
  • 新增 IdentityUserManager 服務。
  • 加入 ApplicationDbContext

更新 CreateModel

更新建立頁面模型建構函式以使用 DI_BasePageModel 基底類別:

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

更新 CreateModel.OnPostAsync 方法以:

  • 將使用者識別碼新增至 Contact 模型。
  • 呼叫授權處理常式,以確認使用者具有建立連絡人的權限。
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Contact.OwnerID = UserManager.GetUserId(User);

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

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

    return RedirectToPage("./Index");
}

更新 IndexModel

更新 OnGetAsync 方法,以僅向一般使用者顯示已核准的連絡人:

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

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

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

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

        var currentUserId = UserManager.GetUserId(User);

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

        Contact = await contacts.ToListAsync();
    }
}

更新 EditModel

新增授權處理常式,以確認使用者擁有連絡人。 由於正在驗證資源授權,因此 [Authorize] 屬性是不夠的。 評估屬性時,應用程式無法存取資源。 資源型授權必須是命令式的。 一旦應用程式能夠存取資源,就必須執行檢查,方法是在頁面模型中載入資源,或在處理常式本身內載入它。 您經常透過傳入資源金鑰來存取資源。

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

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

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

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

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

        return Page();
    }

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

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

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

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

        Contact.OwnerID = contact.OwnerID;

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

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

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

        await Context.SaveChangesAsync();

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

更新 DeleteModel

更新刪除頁面模型,以使用授權處理常式來確認使用者具有連絡人的刪除權限。

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

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

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

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

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

        return Page();
    }

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

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

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

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

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

將授權服務插入檢視

目前,UI 會顯示使用者無法修改之連絡人的編輯和刪除連結。

將授權服務插入 Pages/_ViewImports.cshtml 檔案中,以便可供所有檢視使用:

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

上述標記會新增數個 using 陳述式。

更新 Pages/Contacts/Index.cshtml 中的 [編輯] 和 [刪除] 連結,使其僅針對具有適當權限的使用者進行轉譯:

@page
@model ContactManager.Pages.Contacts.IndexModel

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

<h2>Index</h2>

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

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

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

警告

隱藏沒有變更資料權限之使用者的連結,並不會保護應用程式的安全。 隱藏連結可讓應用程式更易於使用,也就是只顯示有效的連結。 使用者可以對產生的 URL 進行駭客攻擊,以叫用他們未擁有資料的編輯和刪除作業。 Razor Pages 或控制器必須強制執行存取檢查才能保護資料的安全。

更新詳細資料

更新詳細資料檢視,讓管理員可以核准或拒絕連絡人:

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

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

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

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

更新詳細資料頁面模型:

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

    public Contact Contact { get; set; }

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

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

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

        var currentUserId = UserManager.GetUserId(User);

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

        return Page();
    }

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

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

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

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

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

將使用者新增至角色或移除使用者

如需下列資訊,請參閱這個問題

  • 從使用者移除權限。 例如,在聊天應用程式中將使用者靜音。
  • 將權限新增至使用者。

挑戰與禁止之間的差異

此應用程式會將預設原則設定為要求已驗證的使用者。 下列程式碼允許匿名使用者。 允許匿名使用者顯示挑戰與禁止之間的差異。

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

    public Contact Contact { get; set; }

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

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

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

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

        var currentUserId = UserManager.GetUserId(User);

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

        return Page();
    }
}

在上述程式碼中:

  • 當使用者通過驗證時,即會傳回 ChallengeResult。 傳回 ChallengeResult 時,使用者會重新導向至登入頁面。
  • 當使用者經過驗證但未獲得授權時,即會傳回 ForbidResult。 傳回 ForbidResult 時,使用者會重新導向至拒絕存取的頁面。

測試已完成的應用程式

如果您尚未為植入的使用者帳戶設定密碼,請使用祕密管理員工具來設定密碼:

  • 選擇強式密碼:使用 8 個以上的字元,以及至少一個大寫字元、數字和符號。 例如,Passw0rd! 符合強式密碼需求。

  • 從專案的資料夾中執行下列命令,其中 <PW> 是密碼:

    dotnet user-secrets set SeedUserPW <PW>
    

如果應用程式具有連絡人:

  • 刪除 Contact 資料表中的所有記錄。
  • 重新啟動應用程式以植入資料庫。

測試已完成應用程式的簡單方式是啟動三種不同的瀏覽器 (或 incognito/InPrivate 工作階段)。 在一個瀏覽器中,註冊新的使用者 (例如 test@example.com)。 以不同的使用者登入每個瀏覽器。 確認下列作業:

  • 已註冊的使用者可以檢視所有已核准的連絡人資料。
  • 已註冊的使用者可以編輯/刪除自己的資料。
  • 管理員可以核准/拒絕連絡人資料。 Details 檢視會顯示 [核准] 和 [拒絕] 按鈕。
  • 系統管理員可以核准/拒絕以及編輯/刪除所有資料。
使用者 由應用程式植入 選項。
test@example.com No 編輯/刪除自己的資料。
manager@contoso.com Yes 核准/拒絕以及編輯/刪除自己的資料。
admin@contoso.com Yes 核准/拒絕以及編輯/刪除所有資料。

在系統管理員的瀏覽器中建立連絡人。 從系統管理員連絡人複製要刪除和編輯的 URL。 將這些連結貼到測試使用者的瀏覽器中,以確認測試使用者無法執行這些作業。

建立入門應用程式

  • 建立名為「ContactManager」的 Razor Pages 應用程式

    • 使用個別使用者帳戶建立應用程式。
    • 將其命名為「ContactManager」,使命名空間符合範例中使用的命名空間。
    • -uld 會指定 LocalDB,而不是 SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • 新增 Models/Contact.cs

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

  • 建立初始移轉並更新資料庫:

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

注意

根據預設,要安裝的 .NET 二進位檔架構代表目前執行的 OS 架構。 若要指定不同的 OS 架構,請參閱 dotnet tool install, --arch option。 如需詳細資訊,請參閱 GitHub 問題 dotnet/AspNetCore.Docs #29262

如果您遇到 dotnet aspnet-codegenerator razorpage 命令的 BUG,請參閱這個 GitHub 問題

  • 更新 Pages/Shared/_Layout.cshtml 檔案中的 ContactManager 錨點:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • 建立、編輯和刪除連絡人以測試應用程式

植入資料庫

SeedData 類別新增至 Data 資料夾:

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

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

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

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

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

    }
}

Main 呼叫 SeedData.Initialize

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

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

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

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

            host.Run();
        }

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

測試應用程式已植入資料庫。 如果連絡人資料庫中有任何資料列,則植入方法不會執行。

其他資源