Membuat aplikasi web ASP.NET Core dengan data pengguna yang dilindungi oleh otorisasi

Oleh Rick Anderson dan Joe Audette

Tutorial ini menunjukkan cara membuat aplikasi web ASP.NET Core dengan data pengguna yang dilindungi oleh otorisasi. Ini menampilkan daftar kontak yang telah dibuat pengguna yang diautentikasi (terdaftar). Ada tiga kelompok keamanan:

  • Pengguna terdaftar dapat melihat semua data yang disetujui dan dapat mengedit/menghapus data mereka sendiri.
  • Manajer dapat menyetujui atau menolak data kontak. Hanya kontak yang disetujui yang terlihat oleh pengguna.
  • Administrator dapat menyetujui/menolak dan mengedit/menghapus data apa pun.

Gambar dalam dokumen ini tidak sama persis dengan templat terbaru.

Dalam gambar berikut, pengguna Rick (rick@example.com) masuk. Rick hanya dapat melihat kontak yang disetujui dan Edit/Hapus/Tautan Buat Baru untuk kontaknya. Hanya rekaman terakhir, yang dibuat oleh Rick, yang menampilkan tautan Edit dan Hapus . Pengguna lain tidak akan melihat rekaman terakhir hingga manajer atau administrator mengubah status menjadi "Disetujui".

Screenshot showing Rick signed in

Dalam gambar berikut, manager@contoso.com masuk dan dalam peran manajer:

Screenshot showing manager@contoso.com signed in

Gambar berikut menunjukkan tampilan detail manajer kontak:

Manager's view of a contact

Tombol Setujui dan Tolak hanya ditampilkan untuk manajer dan administrator.

Dalam gambar berikut, admin@contoso.com masuk dan dalam peran administrator:

Screenshot showing admin@contoso.com signed in

Administrator memiliki semua hak istimewa. Dia dapat membaca, mengedit, atau menghapus kontak apa pun dan mengubah status kontak.

Aplikasi ini dibuat dengan membuat perancah model berikut Contact :

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

Sampel berisi penangan otorisasi berikut:

  • ContactIsOwnerAuthorizationHandler: Memastikan bahwa pengguna hanya dapat mengedit data mereka.
  • ContactManagerAuthorizationHandler: Memungkinkan manajer menyetujui atau menolak kontak.
  • ContactAdministratorsAuthorizationHandler: Memungkinkan administrator menyetujui atau menolak kontak dan mengedit/menghapus kontak.

Prasyarat

Tutorial ini dimajukan. Anda harus memiliki pemahaman terkait:

Aplikasi pemula dan selesai

Unduh aplikasi yang telah selesai . Uji aplikasi yang telah selesai sehingga Anda terbiasa dengan fitur keamanannya.

Aplikasi pemula

Unduh aplikasi pemula.

Jalankan aplikasi, ketuk tautan ContactManager , dan verifikasi bahwa Anda dapat membuat, mengedit, dan menghapus kontak. Untuk membuat aplikasi pemula, lihat Membuat aplikasi pemula.

Mengamankan data pengguna

Bagian berikut memiliki semua langkah utama untuk membuat aplikasi data pengguna yang aman. Anda mungkin merasa berguna untuk merujuk ke proyek yang telah selesai.

Mengikat data kontak ke pengguna

Gunakan ID pengguna ASP.NET Identity untuk memastikan pengguna dapat mengedit data mereka, tetapi bukan data pengguna lain. Tambahkan OwnerID dan ContactStatus ke 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 adalah ID pengguna dari AspNetUser tabel dalam Identity database. Bidang Status menentukan apakah kontak dapat dilihat oleh pengguna umum.

Buat migrasi baru dan perbarui database:

dotnet ef migrations add userID_Status
dotnet ef database update

Menambahkan layanan Peran ke Identity

Tambahkan AddRoles untuk menambahkan layanan Peran:

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

Mengharuskan pengguna terautentikasi

Atur kebijakan otorisasi fallback untuk mengharuskan pengguna diautentikasi:

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

Kode yang disorot sebelumnya menetapkan kebijakan otorisasi fallback. Kebijakan otorisasi fallback mengharuskan semua pengguna untuk diautentikasi, kecuali untuk Razor Halaman, pengontrol, atau metode tindakan dengan atribut otorisasi. Misalnya, Razor Halaman, pengontrol, atau metode tindakan dengan [AllowAnonymous] atau [Authorize(PolicyName="MyPolicy")] menggunakan atribut otorisasi yang diterapkan daripada kebijakan otorisasi fallback.

RequireAuthenticatedUser menambahkan ke instans DenyAnonymousAuthorizationRequirement saat ini, yang memberlakukan bahwa pengguna saat ini diautentikasi.

Kebijakan otorisasi fallback:

  • Diterapkan ke semua permintaan yang tidak secara eksplisit menentukan kebijakan otorisasi. Untuk permintaan yang dilayani oleh perutean titik akhir, ini mencakup titik akhir apa pun yang tidak menentukan atribut otorisasi. Untuk permintaan yang dilayani oleh middleware lain setelah middleware otorisasi, seperti file statis, ini menerapkan kebijakan untuk semua permintaan.

Mengatur kebijakan otorisasi fallback untuk mengharuskan pengguna diautentikasi melindungi Halaman dan pengontrol yang baru ditambahkan Razor . Memiliki otorisasi yang diperlukan secara default lebih aman daripada mengandalkan pengontrol dan Razor Pages baru untuk menyertakan [Authorize] atribut .

Kelas AuthorizationOptions juga berisi AuthorizationOptions.DefaultPolicy. DefaultPolicy adalah kebijakan yang digunakan dengan [Authorize] atribut ketika tidak ada kebijakan yang ditentukan. [Authorize] tidak berisi kebijakan bernama, tidak seperti [Authorize(PolicyName="MyPolicy")].

Untuk informasi selengkapnya tentang kebijakan, lihat Otorisasi berbasis kebijakan di ASP.NET Core.

Cara alternatif bagi pengontrol MVC dan Razor Pages untuk mengharuskan semua pengguna diautentikasi adalah menambahkan filter otorisasi:

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

Kode sebelumnya menggunakan filter otorisasi, mengatur kebijakan fallback menggunakan perutean titik akhir. Mengatur kebijakan fallback adalah cara yang lebih disukai untuk mengharuskan semua pengguna diautentikasi.

Tambahkan AllowAnonymous ke Index halaman dan Privacy sehingga pengguna anonim bisa mendapatkan informasi tentang situs sebelum mereka mendaftar:

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

    }
}

Mengonfigurasi akun pengujian

Kelas SeedData membuat dua akun: administrator dan manajer. Gunakan alat Secret Manager untuk mengatur kata sandi untuk akun ini. Atur kata sandi dari direktori proyek (direktori yang berisi Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Jika kata sandi yang kuat tidak ditentukan, pengecualian akan dilemparkan saat SeedData.Initialize dipanggil.

Perbarui aplikasi untuk menggunakan kata sandi pengujian:

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

Membuat akun pengujian dan memperbarui kontak

Initialize Perbarui metode di SeedData kelas untuk membuat akun pengujian:

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

Tambahkan ID pengguna administrator dan ContactStatus ke kontak. Buat salah satu kontak "Dikirim" dan satu "Ditolak". Tambahkan ID pengguna dan status ke semua kontak. Hanya satu kontak yang ditampilkan:

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

Membuat penangan otorisasi pemilik, manajer, dan administrator

Buat ContactIsOwnerAuthorizationHandler kelas di folder Otorisasi . Memverifikasi ContactIsOwnerAuthorizationHandler bahwa pengguna yang bertindak pada sumber daya memiliki sumber daya.

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

Konteks ContactIsOwnerAuthorizationHandler panggilan . Berhasil jika pengguna terautentikasi saat ini adalah pemilik kontak. Handler otorisasi umumnya:

  • Hubungi context.Succeed saat persyaratan terpenuhi.
  • Kembalikan Task.CompletedTask saat persyaratan tidak terpenuhi. Task.CompletedTask Kembali tanpa panggilan sebelumnya ke context.Success atau context.Fail, bukan keberhasilan atau kegagalan, itu memungkinkan penangan otorisasi lain untuk berjalan.

Jika Anda perlu secara eksplisit gagal, panggil konteks. Gagal.

Aplikasi ini memungkinkan pemilik kontak mengedit/menghapus/membuat data mereka sendiri. ContactIsOwnerAuthorizationHandler tidak perlu memeriksa operasi yang diteruskan dalam parameter persyaratan.

Membuat handler otorisasi manajer

Buat ContactManagerAuthorizationHandler kelas di folder Otorisasi . Memverifikasi ContactManagerAuthorizationHandler pengguna yang bertindak pada sumber daya adalah manajer. Hanya manajer yang dapat menyetujui atau menolak perubahan konten (baru atau diubah).

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

Membuat handler otorisasi administrator

Buat ContactAdministratorsAuthorizationHandler kelas di folder Otorisasi . Memverifikasi ContactAdministratorsAuthorizationHandler bahwa pengguna yang bertindak pada sumber daya adalah administrator. Administrator dapat melakukan semua operasi.

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

Mendaftarkan handler otorisasi

Layanan yang menggunakan Entity Framework Core harus didaftarkan untuk injeksi dependensi menggunakan AddScoped. ContactIsOwnerAuthorizationHandler menggunakan ASP.NET Core Identity, yang dibangun di Entity Framework Core. Daftarkan handler dengan koleksi layanan sehingga tersedia untuk ContactsController melalui injeksi dependensi. Tambahkan kode berikut ke akhir :ConfigureServices

var builder = WebApplication.CreateBuilder(args);

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

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

builder.Services.AddRazorPages();

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

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

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

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

var app = builder.Build();

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

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

   await SeedData.Initialize(services, testUserPw);
}

ContactAdministratorsAuthorizationHandler dan ContactManagerAuthorizationHandler ditambahkan sebagai singleton. Mereka adalah singleton karena tidak menggunakan EF dan semua informasi yang diperlukan berada dalam Context parameter HandleRequirementAsync metode .

Otorisasi dukungan

Di bagian ini, Anda memperbarui Razor Halaman dan menambahkan kelas persyaratan operasi.

Meninjau kelas persyaratan operasi kontak

ContactOperations Tinjau kelas. Kelas ini berisi persyaratan yang didukung aplikasi:

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

Membuat kelas dasar untuk Halaman Kontak Razor

Buat kelas dasar yang berisi layanan yang digunakan di Halaman kontak Razor . Kelas dasar menempatkan kode inisialisasi di satu lokasi:

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

Kode sebelumnya:

  • IAuthorizationService Menambahkan layanan untuk mengakses penangan otorisasi.
  • IdentityUserManager Menambahkan layanan.
  • ApplicationDbContextTambahkan .

Memperbarui CreateModel

Perbarui model halaman buat:

  • Konstruktor untuk menggunakan DI_BasePageModel kelas dasar.
  • OnPostAsync metode untuk:
    • Tambahkan ID pengguna ke Contact model.
    • Panggil handler otorisasi untuk memverifikasi bahwa pengguna memiliki izin untuk membuat kontak.
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");
        }
    }
}

Memperbarui IndexModel

OnGetAsync Perbarui metode sehingga hanya kontak yang disetujui yang ditampilkan kepada pengguna umum:

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

Memperbarui EditModel

Tambahkan handler otorisasi untuk memverifikasi bahwa pengguna memiliki kontak. Karena otorisasi sumber daya sedang divalidasi, [Authorize] atribut tidak cukup. Aplikasi tidak memiliki akses ke sumber daya saat atribut dievaluasi. Otorisasi berbasis sumber daya harus penting. Pemeriksaan harus dilakukan setelah aplikasi memiliki akses ke sumber daya, baik dengan memuatnya dalam model halaman atau dengan memuatnya dalam handler itu sendiri. Anda sering mengakses sumber daya dengan meneruskan kunci sumber daya.

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

Memperbarui DeleteModel

Perbarui model halaman penghapusan untuk menggunakan handler otorisasi untuk memverifikasi bahwa pengguna memiliki izin hapus pada kontak.

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

Menyuntikkan layanan otorisasi ke dalam tampilan

Saat ini, UI memperlihatkan tautan edit dan hapus untuk kontak yang tidak dapat diubah pengguna.

Masukkan layanan otorisasi dalam Pages/_ViewImports.cshtml file sehingga tersedia untuk semua tampilan:

@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

Markup sebelumnya menambahkan beberapa using pernyataan.

Perbarui tautan Edit dan Hapus agar hanya dirender Pages/Contacts/Index.cshtml untuk pengguna dengan izin yang sesuai:

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

Peringatan

Menyembunyikan tautan dari pengguna yang tidak memiliki izin untuk mengubah data tidak mengamankan aplikasi. Menyembunyikan tautan membuat aplikasi lebih mudah digunakan dengan hanya menampilkan tautan yang valid. Pengguna dapat meretas URL yang dihasilkan untuk memanggil operasi edit dan hapus pada data yang tidak mereka miliki. Halaman Razor atau pengontrol harus memberlakukan pemeriksaan akses untuk mengamankan data.

Perbarui Detail

Perbarui tampilan detail sehingga manajer dapat menyetujui atau menolak kontak:

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

Memperbarui model halaman detail

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

Menambahkan atau menghapus pengguna ke peran

Lihat masalah ini untuk informasi tentang:

  • Menghapus hak istimewa dari pengguna. Misalnya, membisukan pengguna di aplikasi obrolan.
  • Menambahkan hak istimewa ke pengguna.

Perbedaan antara Tantangan dan Terlarang

Aplikasi ini menetapkan kebijakan default untuk mewajibkan pengguna yang diautentikasi. Kode berikut memungkinkan pengguna anonim. Pengguna anonim diizinkan untuk menunjukkan perbedaan antara 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? _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();
    }
}

Dalam kode sebelumnya:

  • Saat pengguna tidak diautentikasi, akan ChallengeResult dikembalikan. ChallengeResult Saat dikembalikan, pengguna dialihkan ke halaman masuk.
  • Saat pengguna diautentikasi, tetapi tidak diotorisasi, dikembalikan ForbidResult . ForbidResult Saat dikembalikan, pengguna dialihkan ke halaman akses ditolak.

Menguji aplikasi yang telah selesai

Jika Anda belum mengatur kata sandi untuk akun pengguna seeded, gunakan alat Secret Manager untuk mengatur kata sandi:

  • Pilih kata sandi yang kuat: Gunakan delapan karakter atau lebih dan setidaknya satu karakter huruf besar, angka, dan simbol. Misalnya, Passw0rd! memenuhi persyaratan kata sandi yang kuat.

  • Jalankan perintah berikut dari folder proyek, di mana <PW> adalah kata sandi:

    dotnet user-secrets set SeedUserPW <PW>
    

Jika aplikasi memiliki kontak:

  • Hapus semua rekaman dalam Contact tabel.
  • Mulai ulang aplikasi untuk menyemai database.

Cara mudah untuk menguji aplikasi yang telah selesai adalah dengan meluncurkan tiga browser yang berbeda (atau sesi incognito/InPrivate). Dalam satu browser, daftarkan pengguna baru (misalnya, test@example.com). Masuk ke setiap browser dengan pengguna lain. Verifikasi operasi berikut:

  • Pengguna terdaftar dapat melihat semua data kontak yang disetujui.
  • Pengguna terdaftar dapat mengedit/menghapus data mereka sendiri.
  • Manajer dapat menyetujui/menolak data kontak. Tampilan Details memperlihatkan tombol Setujui dan Tolak .
  • Administrator dapat menyetujui/menolak dan mengedit/menghapus semua data.
User Menyetujui atau menolak kontak Opsi
test@example.com Tidak Mengedit dan menghapus data mereka.
manager@contoso.com Ya Mengedit dan menghapus data mereka.
admin@contoso.com Ya Edit dan hapus semua data.

Buat kontak di browser administrator. Salin URL untuk dihapus dan diedit dari kontak administrator. Tempelkan tautan ini ke browser pengguna uji untuk memverifikasi bahwa pengguna uji tidak dapat melakukan operasi ini.

Membuat aplikasi pemula

  • Membuat Razor aplikasi Pages bernama "ContactManager"

    • Buat aplikasi dengan Akun Pengguna Individual.
    • Beri nama "ContactManager" sehingga namespace cocok dengan namespace yang digunakan dalam sampel.
    • -uld menentukan LocalDB alih-alih SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Tambahkan 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; }
        }
    }
    
  • Perancah Contact model.

  • Buat migrasi awal dan perbarui 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

Catatan

Secara default arsitektur biner .NET yang akan diinstal mewakili arsitektur OS yang sedang berjalan. Untuk menentukan arsitektur OS yang berbeda, lihat penginstalan alat dotnet, opsi --arch. Untuk informasi selengkapnya, lihat Masalah GitHub dotnet/AspNetCore.Docs #29262.

  • Perbarui jangkar ContactManager dalam Pages/Shared/_Layout.cshtml file:

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • Menguji aplikasi dengan membuat, mengedit, dan menghapus kontak

Seed database

Tambahkan kelas SeedData ke folder 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();
        }

    }
}

Hubungi SeedData.Initialize dari Program.cs:

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

Uji bahwa aplikasi menyemai database. Jika ada baris dalam kontak DB, metode seed tidak berjalan.

Tutorial ini menunjukkan cara membuat aplikasi web ASP.NET Core dengan data pengguna yang dilindungi oleh otorisasi. Ini menampilkan daftar kontak yang telah dibuat pengguna yang diautentikasi (terdaftar). Ada tiga kelompok keamanan:

  • Pengguna terdaftar dapat melihat semua data yang disetujui dan dapat mengedit/menghapus data mereka sendiri.
  • Manajer dapat menyetujui atau menolak data kontak. Hanya kontak yang disetujui yang terlihat oleh pengguna.
  • Administrator dapat menyetujui/menolak dan mengedit/menghapus data apa pun.

Gambar dalam dokumen ini tidak sama persis dengan templat terbaru.

Dalam gambar berikut, pengguna Rick (rick@example.com) masuk. Rick hanya dapat melihat kontak yang disetujui dan Edit/Hapus/Tautan Buat Baru untuk kontaknya. Hanya rekaman terakhir, yang dibuat oleh Rick, yang menampilkan tautan Edit dan Hapus . Pengguna lain tidak akan melihat rekaman terakhir hingga manajer atau administrator mengubah status menjadi "Disetujui".

Screenshot showing Rick signed in

Dalam gambar berikut, manager@contoso.com masuk dan dalam peran manajer:

Screenshot showing manager@contoso.com signed in

Gambar berikut menunjukkan tampilan detail manajer kontak:

Manager's view of a contact

Tombol Setujui dan Tolak hanya ditampilkan untuk manajer dan administrator.

Dalam gambar berikut, admin@contoso.com masuk dan dalam peran administrator:

Screenshot showing admin@contoso.com signed in

Administrator memiliki semua hak istimewa. Dia dapat membaca/mengedit/menghapus kontak apa pun dan mengubah status kontak.

Aplikasi ini dibuat dengan membuat perancah model berikut Contact :

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

Sampel berisi penangan otorisasi berikut:

  • ContactIsOwnerAuthorizationHandler: Memastikan bahwa pengguna hanya dapat mengedit data mereka.
  • ContactManagerAuthorizationHandler: Memungkinkan manajer menyetujui atau menolak kontak.
  • ContactAdministratorsAuthorizationHandler: Memungkinkan administrator untuk:
    • Menyetujui atau menolak kontak
    • Mengedit dan menghapus kontak

Prasyarat

Tutorial ini dimajukan. Anda harus memiliki pemahaman terkait:

Aplikasi pemula dan selesai

Unduh aplikasi yang telah selesai . Uji aplikasi yang telah selesai sehingga Anda terbiasa dengan fitur keamanannya.

Aplikasi pemula

Unduh aplikasi pemula.

Jalankan aplikasi, ketuk tautan ContactManager , dan verifikasi bahwa Anda dapat membuat, mengedit, dan menghapus kontak. Untuk membuat aplikasi pemula, lihat Membuat aplikasi pemula.

Mengamankan data pengguna

Bagian berikut memiliki semua langkah utama untuk membuat aplikasi data pengguna yang aman. Anda mungkin merasa berguna untuk merujuk ke proyek yang telah selesai.

Mengikat data kontak ke pengguna

Gunakan ID pengguna ASP.NET Identity untuk memastikan pengguna dapat mengedit data mereka, tetapi bukan data pengguna lain. Tambahkan OwnerID dan ContactStatus ke 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 adalah ID pengguna dari AspNetUser tabel dalam Identity database. Bidang Status menentukan apakah kontak dapat dilihat oleh pengguna umum.

Buat migrasi baru dan perbarui database:

dotnet ef migrations add userID_Status
dotnet ef database update

Menambahkan layanan Peran ke Identity

Tambahkan AddRoles untuk menambahkan layanan Peran:

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

Mengharuskan pengguna terautentikasi

Atur kebijakan autentikasi fallback untuk mengharuskan pengguna diautentikasi:

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

Kode yang disorot sebelumnya menetapkan kebijakan autentikasi fallback. Kebijakan autentikasi fallback mengharuskan semua pengguna diautentikasi, kecuali untuk Razor Halaman, pengontrol, atau metode tindakan dengan atribut autentikasi. Misalnya, Razor Halaman, pengontrol, atau metode tindakan dengan [AllowAnonymous] atau [Authorize(PolicyName="MyPolicy")] menggunakan atribut autentikasi yang diterapkan daripada kebijakan autentikasi fallback.

RequireAuthenticatedUser menambahkan ke instans DenyAnonymousAuthorizationRequirement saat ini, yang memberlakukan bahwa pengguna saat ini diautentikasi.

Kebijakan autentikasi fallback:

  • Diterapkan ke semua permintaan yang tidak secara eksplisit menentukan kebijakan autentikasi. Untuk permintaan yang dilayani oleh perutean titik akhir, ini akan mencakup titik akhir apa pun yang tidak menentukan atribut otorisasi. Untuk permintaan yang dilayani oleh middleware lain setelah middleware otorisasi, seperti file statis, ini akan menerapkan kebijakan untuk semua permintaan.

Mengatur kebijakan autentikasi fallback untuk mengharuskan pengguna diautentikasi melindungi Halaman dan pengontrol yang baru ditambahkan Razor . Memiliki autentikasi yang diperlukan secara default lebih aman daripada mengandalkan pengontrol dan Razor Pages baru untuk menyertakan [Authorize] atribut .

Kelas AuthorizationOptions juga berisi AuthorizationOptions.DefaultPolicy. DefaultPolicy adalah kebijakan yang digunakan dengan [Authorize] atribut ketika tidak ada kebijakan yang ditentukan. [Authorize] tidak berisi kebijakan bernama, tidak seperti [Authorize(PolicyName="MyPolicy")].

Untuk informasi selengkapnya tentang kebijakan, lihat Otorisasi berbasis kebijakan di ASP.NET Core.

Cara alternatif bagi pengontrol MVC dan Razor Pages untuk mengharuskan semua pengguna diautentikasi adalah menambahkan filter otorisasi:

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

Kode sebelumnya menggunakan filter otorisasi, mengatur kebijakan fallback menggunakan perutean titik akhir. Mengatur kebijakan fallback adalah cara yang lebih disukai untuk mengharuskan semua pengguna diautentikasi.

Tambahkan AllowAnonymous ke Index halaman dan Privacy sehingga pengguna anonim bisa mendapatkan informasi tentang situs sebelum mereka mendaftar:

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

        }
    }
}

Mengonfigurasi akun pengujian

Kelas SeedData membuat dua akun: administrator dan manajer. Gunakan alat Secret Manager untuk mengatur kata sandi untuk akun ini. Atur kata sandi dari direktori proyek (direktori yang berisi Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Jika kata sandi yang kuat tidak ditentukan, pengecualian akan dilemparkan saat SeedData.Initialize dipanggil.

Perbarui Main untuk menggunakan kata sandi pengujian:

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

Membuat akun pengujian dan memperbarui kontak

Initialize Perbarui metode di SeedData kelas untuk membuat akun pengujian:

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

Tambahkan ID pengguna administrator dan ContactStatus ke kontak. Buat salah satu kontak "Dikirim" dan satu "Ditolak". Tambahkan ID pengguna dan status ke semua kontak. Hanya satu kontak yang ditampilkan:

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

Membuat penangan otorisasi pemilik, manajer, dan administrator

Buat ContactIsOwnerAuthorizationHandler kelas di folder Otorisasi . Memverifikasi ContactIsOwnerAuthorizationHandler bahwa pengguna yang bertindak pada sumber daya memiliki sumber daya.

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

Konteks ContactIsOwnerAuthorizationHandler panggilan . Berhasil jika pengguna terautentikasi saat ini adalah pemilik kontak. Handler otorisasi umumnya:

  • Hubungi context.Succeed saat persyaratan terpenuhi.
  • Kembalikan Task.CompletedTask saat persyaratan tidak terpenuhi. Task.CompletedTask Kembali tanpa panggilan sebelumnya ke context.Success atau context.Fail, bukan keberhasilan atau kegagalan, itu memungkinkan penangan otorisasi lain untuk berjalan.

Jika Anda perlu secara eksplisit gagal, panggil konteks. Gagal.

Aplikasi ini memungkinkan pemilik kontak mengedit/menghapus/membuat data mereka sendiri. ContactIsOwnerAuthorizationHandler tidak perlu memeriksa operasi yang diteruskan dalam parameter persyaratan.

Membuat handler otorisasi manajer

Buat ContactManagerAuthorizationHandler kelas di folder Otorisasi . Memverifikasi ContactManagerAuthorizationHandler pengguna yang bertindak pada sumber daya adalah manajer. Hanya manajer yang dapat menyetujui atau menolak perubahan konten (baru atau diubah).

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

Membuat handler otorisasi administrator

Buat ContactAdministratorsAuthorizationHandler kelas di folder Otorisasi . Memverifikasi ContactAdministratorsAuthorizationHandler bahwa pengguna yang bertindak pada sumber daya adalah administrator. Administrator dapat melakukan semua operasi.

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

Mendaftarkan handler otorisasi

Layanan yang menggunakan Entity Framework Core harus didaftarkan untuk injeksi dependensi menggunakan AddScoped. ContactIsOwnerAuthorizationHandler menggunakan ASP.NET Core Identity, yang dibangun di Entity Framework Core. Daftarkan handler dengan koleksi layanan sehingga tersedia untuk ContactsController melalui injeksi dependensi. Tambahkan kode berikut ke akhir :ConfigureServices

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

    services.AddRazorPages();

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

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

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

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

ContactAdministratorsAuthorizationHandler dan ContactManagerAuthorizationHandler ditambahkan sebagai singleton. Mereka adalah singleton karena tidak menggunakan EF dan semua informasi yang diperlukan berada dalam Context parameter HandleRequirementAsync metode .

Otorisasi dukungan

Di bagian ini, Anda memperbarui Razor Halaman dan menambahkan kelas persyaratan operasi.

Meninjau kelas persyaratan operasi kontak

ContactOperations Tinjau kelas. Kelas ini berisi persyaratan yang didukung aplikasi:

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

Membuat kelas dasar untuk Halaman Kontak Razor

Buat kelas dasar yang berisi layanan yang digunakan di Halaman kontak Razor . Kelas dasar menempatkan kode inisialisasi di satu lokasi:

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

Kode sebelumnya:

  • IAuthorizationService Menambahkan layanan untuk mengakses penangan otorisasi.
  • IdentityUserManager Menambahkan layanan.
  • ApplicationDbContextTambahkan .

Memperbarui CreateModel

Perbarui konstruktor buat model halaman untuk menggunakan DI_BasePageModel kelas dasar:

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

Perbarui metode ke CreateModel.OnPostAsync :

  • Tambahkan ID pengguna ke Contact model.
  • Panggil handler otorisasi untuk memverifikasi bahwa pengguna memiliki izin untuk membuat kontak.
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");
}

Memperbarui IndexModel

OnGetAsync Perbarui metode sehingga hanya kontak yang disetujui yang ditampilkan kepada pengguna umum:

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

Memperbarui EditModel

Tambahkan handler otorisasi untuk memverifikasi bahwa pengguna memiliki kontak. Karena otorisasi sumber daya sedang divalidasi, [Authorize] atribut tidak cukup. Aplikasi tidak memiliki akses ke sumber daya saat atribut dievaluasi. Otorisasi berbasis sumber daya harus penting. Pemeriksaan harus dilakukan setelah aplikasi memiliki akses ke sumber daya, baik dengan memuatnya dalam model halaman atau dengan memuatnya dalam handler itu sendiri. Anda sering mengakses sumber daya dengan meneruskan kunci sumber daya.

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

Memperbarui DeleteModel

Perbarui model halaman penghapusan untuk menggunakan handler otorisasi untuk memverifikasi bahwa pengguna memiliki izin hapus pada kontak.

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

Menyuntikkan layanan otorisasi ke dalam tampilan

Saat ini, UI memperlihatkan tautan edit dan hapus untuk kontak yang tidak dapat diubah pengguna.

Masukkan layanan otorisasi dalam Pages/_ViewImports.cshtml file sehingga tersedia untuk semua tampilan:

@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

Markup sebelumnya menambahkan beberapa using pernyataan.

Perbarui tautan Edit dan Hapus agar hanya dirender Pages/Contacts/Index.cshtml untuk pengguna dengan izin yang sesuai:

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

Peringatan

Menyembunyikan tautan dari pengguna yang tidak memiliki izin untuk mengubah data tidak mengamankan aplikasi. Menyembunyikan tautan membuat aplikasi lebih mudah digunakan dengan hanya menampilkan tautan yang valid. Pengguna dapat meretas URL yang dihasilkan untuk memanggil operasi edit dan hapus pada data yang tidak mereka miliki. Halaman Razor atau pengontrol harus memberlakukan pemeriksaan akses untuk mengamankan data.

Perbarui Detail

Perbarui tampilan detail sehingga manajer dapat menyetujui atau menolak kontak:

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

Perbarui model halaman detail:

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

Menambahkan atau menghapus pengguna ke peran

Lihat masalah ini untuk informasi tentang:

  • Menghapus hak istimewa dari pengguna. Misalnya, membisukan pengguna di aplikasi obrolan.
  • Menambahkan hak istimewa ke pengguna.

Perbedaan antara Tantangan dan Terlarang

Aplikasi ini menetapkan kebijakan default untuk mewajibkan pengguna yang diautentikasi. Kode berikut memungkinkan pengguna anonim. Pengguna anonim diizinkan untuk menunjukkan perbedaan antara 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();
    }
}

Dalam kode sebelumnya:

  • Saat pengguna tidak diautentikasi, akan ChallengeResult dikembalikan. ChallengeResult Saat dikembalikan, pengguna dialihkan ke halaman masuk.
  • Saat pengguna diautentikasi, tetapi tidak diotorisasi, dikembalikan ForbidResult . ForbidResult Saat dikembalikan, pengguna dialihkan ke halaman akses ditolak.

Menguji aplikasi yang telah selesai

Jika Anda belum mengatur kata sandi untuk akun pengguna seeded, gunakan alat Secret Manager untuk mengatur kata sandi:

  • Pilih kata sandi yang kuat: Gunakan delapan karakter atau lebih dan setidaknya satu karakter huruf besar, angka, dan simbol. Misalnya, Passw0rd! memenuhi persyaratan kata sandi yang kuat.

  • Jalankan perintah berikut dari folder proyek, di mana <PW> adalah kata sandi:

    dotnet user-secrets set SeedUserPW <PW>
    

Jika aplikasi memiliki kontak:

  • Hapus semua rekaman dalam Contact tabel.
  • Mulai ulang aplikasi untuk menyemai database.

Cara mudah untuk menguji aplikasi yang telah selesai adalah dengan meluncurkan tiga browser yang berbeda (atau sesi incognito/InPrivate). Dalam satu browser, daftarkan pengguna baru (misalnya, test@example.com). Masuk ke setiap browser dengan pengguna lain. Verifikasi operasi berikut:

  • Pengguna terdaftar dapat melihat semua data kontak yang disetujui.
  • Pengguna terdaftar dapat mengedit/menghapus data mereka sendiri.
  • Manajer dapat menyetujui/menolak data kontak. Tampilan Details memperlihatkan tombol Setujui dan Tolak .
  • Administrator dapat menyetujui/menolak dan mengedit/menghapus semua data.
User Disemai oleh aplikasi Opsi
test@example.com Tidak Edit/hapus data sendiri.
manager@contoso.com Ya Menyetujui/menolak dan mengedit/menghapus data sendiri.
admin@contoso.com Ya Setujui/tolak dan edit/hapus semua data.

Buat kontak di browser administrator. Salin URL untuk dihapus dan diedit dari kontak administrator. Tempelkan tautan ini ke browser pengguna uji untuk memverifikasi bahwa pengguna uji tidak dapat melakukan operasi ini.

Membuat aplikasi pemula

  • Membuat Razor aplikasi Pages bernama "ContactManager"

    • Buat aplikasi dengan Akun Pengguna Individual.
    • Beri nama "ContactManager" sehingga namespace cocok dengan namespace yang digunakan dalam sampel.
    • -uld menentukan LocalDB alih-alih SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Tambahkan 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; }
    }
    
  • Perancah Contact model.

  • Buat migrasi awal dan perbarui 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

Catatan

Secara default arsitektur biner .NET yang akan diinstal mewakili arsitektur OS yang sedang berjalan. Untuk menentukan arsitektur OS yang berbeda, lihat penginstalan alat dotnet, opsi --arch. Untuk informasi selengkapnya, lihat Masalah GitHub dotnet/AspNetCore.Docs #29262.

Jika Anda mengalami bug dengan dotnet aspnet-codegenerator razorpage perintah , lihat masalah GitHub ini.

  • Perbarui jangkar ContactManager dalam Pages/Shared/_Layout.cshtml file:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Menguji aplikasi dengan membuat, mengedit, dan menghapus kontak

Seed database

Tambahkan kelas SeedData ke folder 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();
        }

    }
}

Hubungi SeedData.Initialize dari 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>();
                });
    }
}

Uji bahwa aplikasi menyemai database. Jika ada baris dalam kontak DB, metode seed tidak berjalan.

Sumber Daya Tambahan: