Udostępnij za pośrednictwem


Tworzenie aplikacji internetowej ASP.NET Core z danymi użytkownika chronionymi przez autoryzację

Przez Rick Anderson i Joe Audette

W tym samouczku pokazano, jak utworzyć aplikację internetową ASP.NET Core z danymi użytkownika chronionymi przez autoryzację. Zostanie wyświetlona lista kontaktów utworzonych przez uwierzytelnionych (zarejestrowanych) użytkowników. Istnieją trzy grupy zabezpieczeń:

  • Zarejestrowani użytkownicy mogą wyświetlać wszystkie zatwierdzone dane i edytować/usuwać własne dane.
  • Menedżerowie mogą zatwierdzać lub odrzucać dane kontaktowe. Tylko zatwierdzone kontakty są widoczne dla użytkowników.
  • Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszelkie dane.

Obrazy w tym dokumencie nie są dokładnie zgodne z najnowszymi szablonami.

Na poniższej ilustracji użytkownik Rick () jest zalogowany. Rick może wyświetlać tylko zatwierdzone kontakty i edytować linki dla swoich kontaktów. Tylko ostatni rekord utworzony przez Rick wyświetla łącza Edytuj i Usuń . Inni użytkownicy nie zobaczą ostatniego rekordu, dopóki menedżer lub administrator nie zmieni stanu na "Zatwierdzone".

Zrzut ekranu przedstawiający zalogowanego Ricka

Na poniższej ilustracji pokazano użytkownika zalogowanego w roli menedżera.

Zrzut ekranu przedstawiający logowanie

Na poniższej ilustracji przedstawiono widok szczegółów osoby odpowiedzialnej za zarządzanie kontaktami:

Widok menedżera na kontakt

Przyciski Zatwierdź i Odrzuć są wyświetlane tylko dla menedżerów i administratorów.

Na poniższej ilustracji jest zalogowany jako administrator.

Zrzut ekranu przedstawiający logowanie

Administrator ma wszystkie uprawnienia. Może odczytywać, edytować lub usuwać dowolny kontakt i zmieniać stan kontaktów.

Aplikacja została utworzona przez wykorzystanie szkieletu następującego modelu:

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

Przykład zawiera następujące procedury obsługi autoryzacji:

  • : zapewnia, że użytkownik może edytować tylko swoje dane.
  • : umożliwia menedżerom zatwierdzanie lub odrzucanie kontaktów.
  • : umożliwia administratorom zatwierdzanie lub odrzucanie kontaktów oraz edytowanie/usuwanie kontaktów.

Prerequisites

Ten samouczek jest zaawansowany. Należy zapoznać się z:

  • ASP.NET Core
  • Authentication
  • Potwierdzenie konta i odzyskiwanie hasła
  • Authorization
  • Entity Framework Core

Aplikacja startowa i ukończona

Pobierz ukończoną aplikację. Przetestuj ukończoną aplikację, aby zapoznać się z jej funkcjami zabezpieczeń.

Tip

Użyj , aby pobrać tylko przykładowy podfolder. Przykład:

git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse
cd AspNetCore.Docs
git sparse-checkout init --cone
git sparse-checkout set aspnetcore/security/authorization/secure-data/samples

Aplikacja startowa

Pobierzaplikację startową.

Uruchom aplikację, naciśnij link ContactManager i sprawdź, czy możesz utworzyć, edytować i usunąć kontakt. Aby utworzyć aplikację startową, zobacz Tworzenie aplikacji startowej.

Zabezpieczanie danych użytkownika

W poniższych sekcjach przedstawiono wszystkie główne kroki tworzenia bezpiecznej aplikacji danych użytkownika. Warto zapoznać się z ukończonym projektem.

Powiązanie danych kontaktowych z użytkownikiem

Użyj identyfikatora użytkownika ASP.NET Identity, aby upewnić się, że użytkownicy mogą edytować swoje dane, ale nie inne dane użytkowników. Dodaj element i do modelu:

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
}

to identyfikator użytkownika z tabeli w bazie danych. Pole określa, czy kontakt jest wyświetlany przez użytkowników ogólnych.

Utwórz nową migrację i zaktualizuj bazę danych:

dotnet ef migrations add userID_Status
dotnet ef database update

Dodaj usługi ról do

Dołącz , aby dodać usługi ról:

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

Wymaganie uwierzytelnionych użytkowników

Ustaw zasady autoryzacji rezerwowej, aby wymagać uwierzytelnienia użytkowników:

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

Powyższy wyróżniony kod ustawia zasady autoryzacji rezerwowej. Zasady autoryzacji rezerwowej wymagają uwierzytelnienia wszystkich użytkowników, z wyjątkiem stron, kontrolerów lub metod akcji z atrybutem autoryzacji. Na przykład strony, kontrolery lub metody akcji z zastosowanym atrybutem autoryzacji powinny korzystać z niego zamiast z zasad autoryzacji rezerwowej.

dodaje do bieżącego wystąpienia, co wymusza uwierzytelnienie bieżącego użytkownika.

Zasady autoryzacji rezerwowej:

  • Jest stosowany do wszystkich żądań, które nie określają jawnie zasad autoryzacji. W przypadku żądań obsługiwanych przez routing punktu końcowego obejmuje to dowolny punkt końcowy, który nie określa atrybutu autoryzacji. W przypadku żądań obsługiwanych przez inne oprogramowanie pośredniczące po oprogramowaniu pośredniczącym autoryzacji, takie jak pliki statyczne, zasady są stosowane do wszystkich żądań.

Ustawienie zasad autoryzacji rezerwowej w celu wymagania od użytkowników uwierzytelniania chroni nowo dodane strony i kontrolery. Posiadanie wymaganej domyślnie autoryzacji jest bezpieczniejsze niż poleganie na nowych kontrolerach i stronach w celu uwzględnienia atrybutu .

Klasa zawiera również element . Jest to zasady używane z atrybutem , gdy nie określono żadnych zasad. nie zawiera nazwanych zasad, w przeciwieństwie do .

Aby uzyskać więcej informacji na temat zasad, zobacz Policy oparte na autoryzacji w ASP.NET Core.

Alternatywnym sposobem, w jaki kontrolery MVC i strony wymagają uwierzytelnienia wszystkich użytkowników, jest dodanie filtru autoryzacji:

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

Powyższy kod używa filtru autoryzacji, a polityka rezerwowa jest ustawiana za pomocą routingu punktów końcowych. Ustawienie polityki rezerwowej jest preferowanym sposobem wymagania uwierzytelnienia wszystkich użytkowników.

Dodaj atrybut AllowAnonymous na stronach [page1] i [page2] tak, aby użytkownicy anonimowi mogli uzyskać informacje o witrynie, zanim się zarejestrują.

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

    }
}

Konfigurowanie konta testowego

Klasa tworzy dwa konta: administrator i menedżer. Użyj narzędzia Secret Manager, aby ustawić hasło dla tych kont. Ustaw hasło z katalogu project (katalog zawierający Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Jeśli zostanie określone słabe hasło, w wywołaniu zostanie zgłoszony wyjątek.

Zaktualizuj aplikację, aby korzystała z hasła testowego:

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

Tworzenie kont testowych i aktualizowanie kontaktów

Zaktualizuj metodę w klasie, aby utworzyć konta testowe:

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

Dodaj identyfikator użytkownika administratora i do kontaktów. Ustaw jeden z kontaktów jako "Przesłany" i jeden jako "Odrzucony". Dodaj identyfikator użytkownika i stan do wszystkich kontaktów. Wyświetlany jest tylko jeden kontakt:

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

Tworzenie procedur obsługi autoryzacji właściciela, menedżera i administratora

Utwórz klasę w folderze Autoryzacja . Funkcja sprawdza, czy użytkownik działający na zasobie jest właścicielem zasobu.

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

Kontekst wywołań . Powiedz się, jeśli bieżący uwierzytelniony użytkownik jest właścicielem kontaktu. Programy obsługi autoryzacji zazwyczaj:

  • Wywołaj, gdy zostaną spełnione wymagania.
  • Zwracaj, gdy wymagania nie są spełnione. Powrót bez wcześniejszego wywołania metody lub , nie jest powodzeniem lub niepowodzeniem, umożliwia uruchamianie innych procedur obsługi autoryzacji.

Jeśli musisz wyraźnie zakończyć operację niepowodzeniem, wywołaj metodę context.Fail.

Aplikacja umożliwia właścicielom kontaktów edytowanie/usuwanie/tworzenie własnych danych. nie musi sprawdzać operacji przekazanej w parametrze wymagania.

Tworzenie programu obsługi autoryzacji menedżera

Utwórz klasę w folderze Autoryzacja . Sprawdza , czy użytkownik działający na zasobie jest menedżerem. Tylko menedżerowie mogą zatwierdzać lub odrzucać zmiany zawartości (nowe lub zmienione).

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

Tworzenie procedury obsługi autoryzacji administratora

Utwórz klasę w folderze Autoryzacja . Sprawdza , czy użytkownik działający na zasobie jest administratorem. Administrator może wykonywać wszystkie operacje.

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

Rejestrowanie procedur obsługi autoryzacji

Usługi korzystające z programu Entity Framework Core muszą być zarejestrowane do wstrzykiwania zależności przy użyciu polecenia . ContactIsOwnerAuthorizationHandler używa ASP.NET Core Identity, który jest oparty na platformie Entity Framework Core. Zarejestruj programy obsługi w kolekcji usług, aby były dostępne dla iniekcji zależności. Dodaj następujący kod na końcu elementu :

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

i są dodawane jako singletony. Są to singletony, ponieważ nie używają platformy EF, a wszystkie potrzebne informacje znajdują się w parametrze metody.

Obsługa autoryzacji

W tej sekcji zaktualizujesz stronę i dodasz klasę wymagań dotyczących operacji.

Przejrzyj klasę wymagań dotyczących operacji kontaktu

Przejrzyj klasę. Ta klasa zawiera wymagania obsługiwane przez aplikację:

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

Tworzenie klasy bazowej dla stron kontaktów

Utwórz klasę bazową zawierającą usługi używane na stronach kontaktów . Klasa bazowa umieszcza kod inicjowania w jednej lokalizacji:

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

Poprzedni kod:

  • Dodaje usługę IAuthorizationService do dostępu do procedur obsługi autoryzacji.
  • Dodaje tę usługę.
  • Dodaj element .

Aktualizowanie modelu CreateModel

Zaktualizuj model tworzenia strony:

  • Konstruktor do użycia klasy bazowej .
  • metoda do:
    • Dodaj identyfikator użytkownika do modelu.
    • Wywołaj program obsługi autoryzacji, aby sprawdzić, czy użytkownik ma uprawnienia do tworzenia kontaktów.
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");
        }
    }
}

Aktualizowanie modelu IndexModel

Zaktualizuj metodę tak , aby tylko zatwierdzone kontakty zostały wyświetlone użytkownikom ogólnym:

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

Aktualizowanie modelu EditModel

Dodaj procedurę obsługi autoryzacji, aby zweryfikować, czy użytkownik jest właścicielem kontaktu. Ponieważ autoryzacja zasobów jest weryfikowana, atrybut nie jest wystarczający . Aplikacja nie ma access do zasobu podczas oceniania atrybutów. Autoryzacja oparta na zasobach musi być imperatywem. Testy muszą być wykonywane, gdy aplikacja ma dostęp do zasobu, ładując go w modelu strony lub bezpośrednio w samym kontrolerze. Często uzyskujesz dostęp do zasobu, przekazując klucz zasobu.

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

Aktualizowanie modelu DeleteModel

Zaktualizuj model strony usuwania, aby użyć programu obsługi autoryzacji, aby sprawdzić, czy użytkownik ma uprawnienia do usuwania kontaktu.

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

Wstrzykuj usługę autoryzacji do widoków

Obecnie w interfejsie użytkownika są wyświetlane linki do edycji i usuwania kontaktów, których użytkownik nie może modyfikować.

Wstrzyknij usługę uwierzytelniania w pliku, aby była ona dostępna dla wszystkich widoków.

@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

Poprzedni znacznik dodaje kilka instrukcji.

Zaktualizuj łącza Edytuj i Usuń, aby były renderowane tylko dla użytkowników z odpowiednimi uprawnieniami:

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

Warning

Ukrywanie linków od użytkowników, którzy nie mają uprawnień do zmiany danych, nie zabezpiecza aplikacji. Ukrywanie linków sprawia, że aplikacja jest bardziej przyjazna dla użytkownika, wyświetlając tylko prawidłowe linki. Użytkownicy mogą włamać się do wygenerowanych adresów URL, aby wywoływać operacje edycji i usuwania danych, których nie posiadają. Strona lub kontroler Razor musi wymusić kontrole dostępu, aby zabezpieczyć dane.

Szczegóły aktualizacji

Zaktualizuj widok szczegółów, aby menedżerowie mogli zatwierdzać lub odrzucać kontakty:

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

Aktualizowanie modelu strony szczegółów

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

Dodawanie lub usuwanie użytkownika do roli

Aby uzyskać więcej informacji, zobacz this issue:

  • Usuwanie uprawnień od użytkownika. Na przykład wyciszenie użytkownika w aplikacji do czatu.
  • Dodawanie uprawnień do użytkownika.

Różnice między wyzwaniem a zakazem

Ta aplikacja ustawia domyślne zasady, aby wymagać uwierzytelnionych użytkowników. Poniższy kod umożliwia anonimowym użytkownikom. Użytkownicy anonimowi mogą wyświetlać różnice między narzędziem Challenge a 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();
    }
}

We wcześniejszym kodzie:

  • Gdy użytkownik nie zostanie uwierzytelniony, zwracany jest element . Gdy element zostanie zwrócony, użytkownik zostanie przekierowany do strony logowania.
  • Kiedy użytkownik jest uwierzytelniony, ale nie jest autoryzowany, zwracany jest błąd. Gdy zostanie zwrócony ForbidResult, użytkownik zostanie przekierowany na stronę odmowy dostępu.

Testowanie ukończonej aplikacji

Warning

W tym artykule użyto narzędzia Secret Manager do przechowywania hasła dla początkowych kont użytkowników. Narzędzie Secret Manager służy do przechowywania poufnych danych podczas programowania lokalnego. Aby uzyskać informacje na temat procedur uwierzytelniania, które mogą być używane podczas wdrażania aplikacji w środowisku testowym lub produkcyjnym, zobacz Bezpieczne przepływy uwierzytelniania.

Jeśli nie ustawiono jeszcze hasła dla początkowych kont użytkowników, użyj narzędzia Secret Manager, aby ustawić hasło:

  • Wybierz silne hasło:

    • Co najmniej 12 znaków, ale 14 lub więcej jest lepsze.
    • Kombinacja wielkich liter, małych liter, cyfr i symboli.
    • Nie jest to słowo, które można znaleźć w słowniku ani nazwa osoby, postaci, produktu lub organizacji.
    • Znacznie różni się od poprzednich haseł.
    • Łatwe do zapamiętania, ale trudne dla innych do zgadnięcia. Rozważ użycie pamiętnej frazy, takiej jak "6MonkeysRLooking^".
  • Wykonaj następujące polecenie z folderu project, w którym <PW> jest hasłem:

    dotnet user-secrets set SeedUserPW <PW>
    

Jeśli aplikacja ma kontakty:

  • Usuń wszystkie rekordy w tabeli.
  • Uruchom ponownie aplikację, aby zasiać bazę danych.

Łatwym sposobem przetestowania ukończonej aplikacji jest uruchomienie trzech różnych przeglądarek (lub incognito/InPrivate sesji). W jednej przeglądarce zarejestruj nowego użytkownika (na przykład ). Zaloguj się do każdej przeglądarki przy użyciu innego użytkownika. Sprawdź następujące operacje:

  • Zarejestrowani użytkownicy mogą wyświetlać wszystkie zatwierdzone dane kontaktowe.
  • Zarejestrowani użytkownicy mogą edytować/usuwać własne dane.
  • Menedżerowie mogą zatwierdzać/odrzucać dane kontaktowe. W widoku są wyświetlane przyciski Zatwierdź i Odrzuć .
  • Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszystkie dane.
User Zatwierdzanie lub odrzucanie kontaktów Opcje
test@example.com No Edytuj i usuń swoje dane.
manager@contoso.com Yes Edytuj i usuń swoje dane.
admin@contoso.com Yes Edytuj i usuń wszystkie dane.

Utwórz kontakt w przeglądarce administratora. Skopiuj adres URL usuwania i edytowania z kontaktu administratora. Wklej te linki do przeglądarki użytkownika testowego, aby sprawdzić, czy użytkownik testowy nie może wykonać tych operacji.

Tworzenie aplikacji startowej

  • Tworzenie aplikacji Pages o nazwie "ContactManager"

    • Utwórz aplikację przy użyciu indywidualnych kont.
    • Nadaj mu nazwę "ContactManager", aby przestrzeń nazw była zgodna z przestrzenią nazw używaną w przykładzie.
    • określa localDB zamiast SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Dodaj : 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; }
        }
    }
    
  • Ustawianie szkieletu modelu.

  • Utwórz początkową migrację i zaktualizuj bazę danych:

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

Note

Domyślnie architektura plików binarnych .NET do zainstalowania reprezentuje obecnie uruchomioną architekturę systemu operacyjnego. Aby określić inną architekturę systemu operacyjnego, zobacz dotnet tool install, --arch option(Instalacja narzędzia dotnet). Aby uzyskać więcej informacji, zobacz GitHub problem dotnet/AspNetCore.Docs #29262.

  • Zaktualizuj kotwicę ContactManager w pliku:

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • Przetestuj aplikację, tworząc, edytując i usuwając kontakt

Zasiewanie bazy danych

Dodaj klasę SeedData do folderu 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();
        }

    }
}

Wywołaj z :

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

Przetestuj, czy aplikacja uzupełniła bazę danych. Jeśli w bazie danych kontaktów znajdują się jakiekolwiek wiersze, metoda seed nie zostanie uruchomiona.

W tym samouczku pokazano, jak utworzyć aplikację internetową ASP.NET Core z danymi użytkownika chronionymi przez autoryzację. Zostanie wyświetlona lista kontaktów utworzonych przez uwierzytelnionych (zarejestrowanych) użytkowników. Istnieją trzy grupy zabezpieczeń:

  • Zarejestrowani użytkownicy mogą wyświetlać wszystkie zatwierdzone dane i edytować/usuwać własne dane.
  • Menedżerowie mogą zatwierdzać lub odrzucać dane kontaktowe. Tylko zatwierdzone kontakty są widoczne dla użytkowników.
  • Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszelkie dane.

Obrazy w tym dokumencie nie są dokładnie zgodne z najnowszymi szablonami.

Na poniższej ilustracji użytkownik Rick () jest zalogowany. Rick może wyświetlać tylko zatwierdzone kontakty i edytować linki dla swoich kontaktów. Tylko ostatni rekord utworzony przez Rick wyświetla łącza Edytuj i Usuń . Inni użytkownicy nie zobaczą ostatniego rekordu, dopóki menedżer lub administrator nie zmieni stanu na "Zatwierdzone".

Zrzut ekranu przedstawiający zalogowanego Ricka

Na poniższej ilustracji pokazano użytkownika zalogowanego w roli menedżera.

Zrzut ekranu przedstawiający logowanie

Na poniższej ilustracji przedstawiono widok szczegółów osoby odpowiedzialnej za zarządzanie kontaktami:

Widok menedżera na kontakt

Przyciski Zatwierdź i Odrzuć są wyświetlane tylko dla menedżerów i administratorów.

Na poniższej ilustracji jest zalogowany jako administrator.

Zrzut ekranu przedstawiający logowanie

Administrator ma wszystkie uprawnienia. Może odczytywać/edytować/usuwać dowolny kontakt i zmieniać stan kontaktów.

Aplikacja została utworzona przez wykorzystanie szkieletu następującego modelu:

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

Przykład zawiera następujące procedury obsługi autoryzacji:

  • : zapewnia, że użytkownik może edytować tylko swoje dane.
  • : umożliwia menedżerom zatwierdzanie lub odrzucanie kontaktów.
  • : Umożliwia administratorom:
    • Zatwierdzanie lub odrzucanie kontaktów
    • Edytowanie i usuwanie kontaktów

Prerequisites

Ten samouczek jest zaawansowany. Należy zapoznać się z:

  • ASP.NET Core
  • Authentication
  • Potwierdzenie konta i odzyskiwanie hasła
  • Authorization
  • Entity Framework Core

Aplikacja startowa i ukończona

Pobierz ukończoną aplikację. Przetestuj ukończoną aplikację, aby zapoznać się z jej funkcjami zabezpieczeń.

Aplikacja startowa

Pobierzaplikację startową.

Uruchom aplikację, naciśnij link ContactManager i sprawdź, czy możesz utworzyć, edytować i usunąć kontakt. Aby utworzyć aplikację startową, zobacz Tworzenie aplikacji startowej.

Zabezpieczanie danych użytkownika

W poniższych sekcjach przedstawiono wszystkie główne kroki tworzenia bezpiecznej aplikacji danych użytkownika. Warto zapoznać się z ukończonym projektem.

Powiązanie danych kontaktowych z użytkownikiem

Użyj identyfikatora użytkownika ASP.NET Identity, aby upewnić się, że użytkownicy mogą edytować swoje dane, ale nie inne dane użytkowników. Dodaj i do modelu.

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
}

to identyfikator użytkownika z tabeli w bazie danych. Pole określa, czy kontakt jest wyświetlany przez użytkowników ogólnych.

Utwórz nową migrację i zaktualizuj bazę danych:

dotnet ef migrations add userID_Status
dotnet ef database update

Dodawanie usług ról do

Dołącz , aby dodać usługi ról:

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

Wymaganie uwierzytelnionych użytkowników

Ustaw zasady uwierzytelniania rezerwowego, aby wymagać uwierzytelnienia użytkowników:

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

Powyższy wyróżniony kod ustawia zasady uwierzytelniania rezerwowego. Zasady alternatywnego uwierzytelniania wymagają uwierzytelniania wszystkich użytkowników, z wyjątkiem stron, kontrolerów lub metod akcji z atrybutem uwierzytelniania. Na przykład, strony, kontrolery lub metody akcji z zastosowanym atrybutem uwierzytelniania lub używające go, a nie zasady uwierzytelniania rezerwowego.

dodaje do bieżącego wystąpienia, co wymusza uwierzytelnienie bieżącego użytkownika.

Zasady uwierzytelniania awaryjnego

  • Jest stosowany do wszystkich żądań, które nie określają jawnie zasad uwierzytelniania. W przypadku żądań obsługiwanych przez routing punktu końcowego obejmuje to dowolny punkt końcowy, który nie określa atrybutu autoryzacji. W przypadku żądań obsługiwanych przez inne oprogramowanie pośredniczące po oprogramowaniu pośredniczącym autoryzacji, takie jak żądania dotyczące plików statycznych, zasady zostaną zastosowane do wszystkich tego rodzaju żądań.

Ustawienie polityki rezerwowego uwierzytelniania, aby wymagać uwierzytelnienia od użytkowników, chroni nowo dodane Strony i kontrolery. Wymagane domyślnie uwierzytelnianie jest bezpieczniejsze niż poleganie na nowych kontrolerach i stronach w celu uwzględnienia atrybutu .

Klasa zawiera również element . Jest to polityka używana z atrybutem, gdy nie określono żadnej polityki. nie zawiera nazwanych zasad, w przeciwieństwie do .

Aby uzyskać więcej informacji na temat zasad, zobacz Policy oparte na autoryzacji w ASP.NET Core.

Alternatywnym sposobem, w jaki kontrolery MVC i strony wymagają uwierzytelnienia wszystkich użytkowników, jest dodanie filtru autoryzacji:

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

Powyższy kod używa filtru autoryzacji, a ustawienie polityki domyślnej wykorzystuje routingu punktu końcowego. Ustawienie polityki zastępczej jest preferowaną metodą wymuszania uwierzytelnienia wszystkich użytkowników.

Dodaj pozycję AllowAnonymous na stronach i , aby użytkownicy anonimowi mogli uzyskać informacje o witrynie przed zarejestrowaniem:

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

        }
    }
}

Konfigurowanie konta testowego

Klasa tworzy dwa konta: administrator i menedżer. Użyj narzędzia Secret Manager, aby ustawić hasło dla tych kont. Ustaw hasło z katalogu project (katalog zawierający Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Jeśli silne hasło nie zostało określone, podczas wywołania zostanie zgłoszony wyjątek.

Zaktualizuj , aby użyć hasła testowego:

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

Tworzenie kont testowych i aktualizowanie kontaktów

Zaktualizuj metodę w klasie, aby utworzyć konta testowe:

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

Dodaj identyfikator użytkownika administratora i do kontaktów. Zmień status jednego z kontaktów na "Przesłane" i jednego na "Odrzucono". Dodaj identyfikator użytkownika i stan do wszystkich kontaktów. Wyświetlany jest tylko jeden kontakt:

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

Tworzenie procedur obsługi autoryzacji właściciela, menedżera i administratora

Utwórz klasę w folderze Autoryzacja . Funkcja sprawdza, czy użytkownik działający na zasobie jest właścicielem zasobu.

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

Kontekst wywołań . Powiedz się, jeśli bieżący uwierzytelniony użytkownik jest właścicielem kontaktu. Programy obsługi autoryzacji zazwyczaj:

  • Wywołaj, gdy zostaną spełnione wymagania.
  • Zwracaj, gdy wymagania nie są spełnione. Powrót bez wcześniejszego wywołania metody lub , nie jest powodzeniem lub niepowodzeniem, umożliwia uruchamianie innych procedur obsługi autoryzacji.

Jeśli musisz wyraźnie zakończyć operację niepowodzeniem, wywołaj metodę context.Fail.

Aplikacja umożliwia właścicielom kontaktów edytowanie/usuwanie/tworzenie własnych danych. nie musi sprawdzać operacji przekazanej w parametrze wymagania.

Tworzenie programu obsługi autoryzacji menedżera

Utwórz klasę w folderze Autoryzacja . Sprawdza , czy użytkownik działający na zasobie jest menedżerem. Tylko menedżerowie mogą zatwierdzać lub odrzucać zmiany zawartości (nowe lub zmienione).

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

Tworzenie procedury obsługi autoryzacji administratora

Utwórz klasę w folderze Autoryzacja . Sprawdza , czy użytkownik działający na zasobie jest administratorem. Administrator może wykonywać wszystkie operacje.

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

Rejestrowanie procedur obsługi autoryzacji

Usługi korzystające z programu Entity Framework Core muszą być zarejestrowane do wstrzykiwania zależności przy użyciu polecenia . ContactIsOwnerAuthorizationHandler używa ASP.NET Core Identity, który jest oparty na platformie Entity Framework Core. Zarejestruj programy obsługi w kolekcji usług, aby były dostępne dla iniekcji zależności. Dodaj następujący kod na końcu elementu :

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

i są dodawane jako singletony. Są to singletony, ponieważ nie używają platformy EF, a wszystkie potrzebne informacje znajdują się w parametrze metody.

Obsługa autoryzacji

W tej sekcji zaktualizujesz stronę i dodasz klasę wymagań dotyczących operacji.

Przejrzyj klasę wymagań dotyczących operacji kontaktu

Przejrzyj klasę. Ta klasa zawiera wymagania obsługiwane przez aplikację:

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

Tworzenie klasy bazowej dla stron kontaktów

Utwórz klasę bazową zawierającą usługi używane na stronach kontaktów . Klasa bazowa umieszcza kod inicjowania w jednej lokalizacji:

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

Poprzedni kod:

  • Dodaje usługę IAuthorizationService do dostępu do procedur obsługi autoryzacji.
  • Dodaje tę usługę.
  • Dodaj element .

Aktualizowanie modelu CreateModel

Zaktualizuj konstruktor modelu tworzenia strony, aby użyć klasy bazowej :

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

Zaktualizuj metodę na :

  • Dodaj identyfikator użytkownika do modelu.
  • Wywołaj program obsługi autoryzacji, aby sprawdzić, czy użytkownik ma uprawnienia do tworzenia kontaktów.
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");
}

Aktualizowanie modelu IndexModel

Zaktualizuj metodę tak , aby tylko zatwierdzone kontakty zostały wyświetlone użytkownikom ogólnym:

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

Aktualizowanie modelu EditModel

Dodaj procedurę obsługi autoryzacji, aby zweryfikować, czy użytkownik jest właścicielem kontaktu. Ponieważ autoryzacja zasobów jest weryfikowana, atrybut nie jest wystarczający . Aplikacja nie ma access do zasobu podczas oceniania atrybutów. Autoryzacja oparta na zasobach musi być imperatywem. Testy muszą być wykonywane, gdy aplikacja ma dostęp do zasobu, ładując go w modelu strony lub bezpośrednio w samym kontrolerze. Często uzyskujesz dostęp do zasobu, przekazując klucz zasobu.

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

Aktualizowanie modelu DeleteModel

Zaktualizuj model strony usuwania, aby użyć programu obsługi autoryzacji, aby sprawdzić, czy użytkownik ma uprawnienia do usuwania kontaktu.

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

Wstrzykuj usługę autoryzacji do widoków

Obecnie w interfejsie użytkownika są wyświetlane linki do edycji i usuwania kontaktów, których użytkownik nie może modyfikować.

Wstrzyknij usługę uwierzytelniania w pliku, aby była ona dostępna dla wszystkich widoków.

@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

Poprzedni znacznik dodaje kilka instrukcji.

Zaktualizuj łącza Edytuj i Usuń, aby były renderowane tylko dla użytkowników z odpowiednimi uprawnieniami:

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

Warning

Ukrywanie linków od użytkowników, którzy nie mają uprawnień do zmiany danych, nie zabezpiecza aplikacji. Ukrywanie linków sprawia, że aplikacja jest bardziej przyjazna dla użytkownika, wyświetlając tylko prawidłowe linki. Użytkownicy mogą włamać się do wygenerowanych adresów URL, aby wywoływać operacje edycji i usuwania danych, których nie posiadają. Strona lub kontroler Razor musi wymusić kontrole dostępu, aby zabezpieczyć dane.

Szczegóły aktualizacji

Zaktualizuj widok szczegółów, aby menedżerowie mogli zatwierdzać lub odrzucać kontakty:

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

Zaktualizuj model strony szczegółów:

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

Dodawanie lub usuwanie użytkownika do roli

Aby uzyskać więcej informacji, zobacz this issue:

  • Usuwanie uprawnień od użytkownika. Na przykład wyciszenie użytkownika w aplikacji do czatu.
  • Dodawanie uprawnień do użytkownika.

Różnice między wyzwaniem a zakazem

Ta aplikacja ustawia domyślne zasady, aby wymagać uwierzytelnionych użytkowników. Poniższy kod umożliwia anonimowym użytkownikom. Użytkownicy anonimowi mogą wyświetlać różnice między narzędziem Challenge a 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();
    }
}

We wcześniejszym kodzie:

  • Gdy użytkownik nie zostanie uwierzytelniony, zwracany jest element . Gdy element zostanie zwrócony, użytkownik zostanie przekierowany do strony logowania.
  • Kiedy użytkownik jest uwierzytelniony, ale nie jest autoryzowany, zwracany jest błąd. Gdy zostanie zwrócony ForbidResult, użytkownik zostanie przekierowany na stronę odmowy dostępu.

Testowanie ukończonej aplikacji

Jeśli nie ustawiono jeszcze hasła dla początkowych kont użytkowników, użyj narzędzia Secret Manager, aby ustawić hasło:

  • Wybierz silne hasło: użyj co najmniej ośmiu znaków, w tym co najmniej jednej wielkiej litery, cyfry i symbolu. Na przykład spełnia wymagania dotyczące silnego hasła.

  • Wykonaj następujące polecenie z folderu project, w którym <PW> jest hasłem:

    dotnet user-secrets set SeedUserPW <PW>
    

Jeśli aplikacja ma kontakty:

  • Usuń wszystkie rekordy w tabeli.
  • Uruchom ponownie aplikację, aby zasiać bazę danych.

Łatwym sposobem przetestowania ukończonej aplikacji jest uruchomienie trzech różnych przeglądarek (lub incognito/InPrivate sesji). W jednej przeglądarce zarejestruj nowego użytkownika (na przykład ). Zaloguj się do każdej przeglądarki przy użyciu innego użytkownika. Sprawdź następujące operacje:

  • Zarejestrowani użytkownicy mogą wyświetlać wszystkie zatwierdzone dane kontaktowe.
  • Zarejestrowani użytkownicy mogą edytować/usuwać własne dane.
  • Menedżerowie mogą zatwierdzać/odrzucać dane kontaktowe. W widoku są wyświetlane przyciski Zatwierdź i Odrzuć .
  • Administratorzy mogą zatwierdzać/odrzucać i edytować/usuwać wszystkie dane.
User Rozpowszechniane przez aplikację Opcje
test@example.com No Edytuj/usuń własne dane.
manager@contoso.com Yes Zatwierdzanie/odrzucanie i edytowanie/usuwanie własnych danych.
admin@contoso.com Yes Zatwierdzanie/odrzucanie i edytowanie/usuwanie wszystkich danych.

Utwórz kontakt w przeglądarce administratora. Skopiuj adres URL usuwania i edytowania z kontaktu administratora. Wklej te linki do przeglądarki użytkownika testowego, aby sprawdzić, czy użytkownik testowy nie może wykonać tych operacji.

Tworzenie aplikacji startowej

  • Tworzenie aplikacji Pages o nazwie "ContactManager"

    • Utwórz aplikację przy użyciu indywidualnych kont.
    • Nadaj mu nazwę "ContactManager", aby przestrzeń nazw była zgodna z przestrzenią nazw używaną w przykładzie.
    • określa localDB zamiast SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Dodaj :

    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; }
    }
    
  • Ustawianie szkieletu modelu.

  • Utwórz początkową migrację i zaktualizuj bazę danych:

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

Note

Domyślnie architektura plików binarnych .NET do zainstalowania reprezentuje obecnie uruchomioną architekturę systemu operacyjnego. Aby określić inną architekturę systemu operacyjnego, zobacz dotnet tool install, --arch option(Instalacja narzędzia dotnet). Aby uzyskać więcej informacji, zobacz GitHub problem dotnet/AspNetCore.Docs #29262.

Jeśli wystąpi błąd z poleceniem dotnet aspnet-codegenerator razorpage, sprawdź to zgłoszenie na GitHubie.

  • Zaktualizuj kotwicę ContactManager w pliku:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Przetestuj aplikację, tworząc, edytując i usuwając kontakt

Zasiewanie bazy danych

Dodaj klasę SeedData do folderu 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();
        }

    }
}

Wywołaj z :

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

Przetestuj, czy aplikacja uzupełniła bazę danych. Jeśli w bazie danych kontaktów znajdują się jakiekolwiek wiersze, metoda seed nie zostanie uruchomiona.

Dodatkowe zasoby