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.
  • Administracja istratory 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 (rick@example.com) jest zalogowany. Rick może wyświetlać tylko zatwierdzone kontakty i edytować/usuń/Utwórz nowe 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".

Screenshot showing Rick signed in

Na poniższej ilustracji manager@contoso.com jest zalogowany i w roli menedżera:

Screenshot showing manager@contoso.com signed in

Na poniższej ilustracji przedstawiono widok szczegółów kontaktu menedżerów:

Manager's view of a contact

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

Na poniższej ilustracji admin@contoso.com jest zalogowany i w roli administratora:

Screenshot showing admin@contoso.com signed in

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

Aplikacja została utworzona przez utworzenie szkieletu następującego Contact 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:

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

Wymagania wstępne

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

Aplikacja początkowa i ukończona

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

Aplikacja startowa

Pobierz aplikację 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. Pomocne może być odwołanie się do ukończonego projektu.

Powiązanie danych kontaktowych z użytkownikiem

Użyj ASP.NET Identity identyfikatora użytkownika, aby upewnić się, że użytkownicy mogą edytować swoje dane, ale nie inne dane użytkowników. Dodaj OwnerID element i ContactStatus do Contact 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
}

OwnerID to identyfikator użytkownika z AspNetUser tabeli w Identity bazie danych. Pole Status 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 Identity

Dołącz AddRoles , 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 Razor stron, kontrolerów lub metod akcji z atrybutem autoryzacji. Na przykład Razor strony, kontrolery lub metody akcji z zastosowanym atrybutem [AllowAnonymous] autoryzacji lub [Authorize(PolicyName="MyPolicy")] użyj ich, a nie zasad autoryzacji rezerwowej.

RequireAuthenticatedUser dodaje DenyAnonymousAuthorizationRequirement 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 uzyskaniu oprogramowania pośredniczącego autoryzacji, takiego jak pliki statyczne, zasady są stosowane do wszystkich żądań.

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

Klasa AuthorizationOptions zawiera AuthorizationOptions.DefaultPolicyrównież element . Jest DefaultPolicy to zasady używane z atrybutem [Authorize] , gdy nie określono żadnych zasad. [Authorize] nie zawiera nazwanych zasad, w przeciwieństwie do [Authorize(PolicyName="MyPolicy")].

Aby uzyskać więcej informacji na temat zasad, zobacz Autoryzacja oparta na zasadach w programie ASP.NET Core.

Alternatywnym sposobem, w jaki kontrolery MVC i Razor 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, ustawiając zasady rezerwowe używają routingu punktu końcowego. Ustawienie zasad rezerwowych jest preferowanym sposobem, aby wymagać uwierzytelnienia wszystkich użytkowników.

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

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 SeedData 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 projektu (katalog zawierający Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Jeśli nie określono silnego hasła, w wywołaniu SeedData.Initialize jest zgłaszany 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ę Initialize w klasie, SeedData 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 ContactStatus do kontaktów. Utwórz jeden z kontaktów "Przesłane" i jeden "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ę ContactIsOwnerAuthorizationHandler w folderze Autoryzacja . Funkcja ContactIsOwnerAuthorizationHandler 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 ContactIsOwnerAuthorizationHandler wywołań . Powiedz się, jeśli bieżący uwierzytelniony użytkownik jest właścicielem kontaktu. Programy obsługi autoryzacji zazwyczaj:

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

Jeśli musisz jawnie zakończyć się niepowodzeniem, wywołaj kontekst. Niepowodzenie.

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

Tworzenie programu obsługi autoryzacji menedżera

Utwórz klasę ContactManagerAuthorizationHandler w folderze Autoryzacja . Sprawdza ContactManagerAuthorizationHandler , 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ę ContactAdministratorsAuthorizationHandler w folderze Autoryzacja . Sprawdza ContactAdministratorsAuthorizationHandler , czy użytkownik działający na zasobie jest administratorem. Administracja istrator 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 AddScoped. Używa ContactIsOwnerAuthorizationHandler 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 ContactsController iniekcji zależności. Dodaj następujący kod na końcu elementu ConfigureServices:

var builder = WebApplication.CreateBuilder(args);

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

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

builder.Services.AddRazorPages();

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

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

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

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

var app = builder.Build();

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

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

   await SeedData.Initialize(services, testUserPw);
}

ContactAdministratorsAuthorizationHandler i ContactManagerAuthorizationHandler są dodawane jako singletony. Są one singletonami, ponieważ nie używają platformy EF, a wszystkie potrzebne informacje są w Context parametrze HandleRequirementAsync metody .

Obsługa autoryzacji

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

Przejrzyj klasę wymagań dotyczących operacji kontaktu

Przejrzyj klasę ContactOperations . 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 Razor

Utwórz klasę bazową zawierającą usługi używane na stronach kontaktów Razor . 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;
        } 
    }
}

Powyższy kod:

  • Dodaje usługę IAuthorizationService w celu uzyskania dostępu do procedur obsługi autoryzacji.
  • Dodaje usługę IdentityUserManager .
  • Dodaj element ApplicationDbContext.

Aktualizowanie modelu CreateModel

Zaktualizuj model tworzenia strony:

  • Konstruktor do użycia klasy bazowej DI_BasePageModel .
  • OnPostAsync metoda do:
    • Dodaj identyfikator użytkownika do Contact 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 OnGetAsync , 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 [Authorize] . Aplikacja nie ma dostępu 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 ładując ją w ramach samej procedury obsługi. 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");
    }
}

Wstrzykiwanie usługi 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ć.

Wstrzyknąć usługę autoryzacji w Pages/_ViewImports.cshtml pliku, aby była 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

Powyższy znacznik dodaje kilka using instrukcji.

Zaktualizuj łącza Edytuj i Usuń, Pages/Contacts/Index.cshtml 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>

Ostrzeżenie

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ą. Aby Razor zabezpieczyć dane, strona lub kontroler musi wymusić kontrole dostępu.

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

Zobacz ten problem, aby uzyskać informacje na temat:

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

Powyższy kod:

  • Gdy użytkownik nie zostanie uwierzytelniony, ChallengeResult zostanie zwrócony element . ChallengeResult Gdy element zostanie zwrócony, użytkownik zostanie przekierowany do strony logowania.
  • Gdy użytkownik jest uwierzytelniony, ale nie jest autoryzowany, ForbidResult zwracany jest element . Gdy element ForbidResult zostanie zwrócony, użytkownik zostanie przekierowany do strony 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 i co najmniej jednego znaku, cyfry i symbolu. Na przykład Passw0rd! spełnia wymagania dotyczące silnego hasła.

  • Wykonaj następujące polecenie z folderu projektu, gdzie <PW> jest hasłem:

    dotnet user-secrets set SeedUserPW <PW>
    

Jeśli aplikacja ma kontakty:

  • Usuń wszystkie rekordy w Contact tabeli.
  • Uruchom ponownie aplikację, aby zainicjować 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 test@example.com). 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 Details widoku są wyświetlane przyciski Zatwierdź i Odrzuć .
  • Administracja istratory mogą zatwierdzać/odrzucać i edytować/usuwać wszystkie dane.
Użytkownik Zatwierdzanie lub odrzucanie kontaktów Opcje
test@example.com Nie. Edytuj i usuń swoje dane.
manager@contoso.com Tak Edytuj i usuń swoje dane.
admin@contoso.com Tak 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 Razor aplikacji Pages o nazwie "ContactManager"

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

    using System.ComponentModel.DataAnnotations;
    
    namespace ContactManager.Models
    {
        public class Contact
        {
            public int ContactId { get; set; }
            public string? Name { get; set; }
            public string? Address { get; set; }
            public string? City { get; set; }
            public string? State { get; set; }
            public string? Zip { get; set; }
            [DataType(DataType.EmailAddress)]
            public string? Email { get; set; }
        }
    }
    
  • Tworzenie szkieletu Contact 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

Uwaga

Domyślnie architektura plików binarnych platformy .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 problem z usługą GitHub dotnet/AspNetCore.Docs #29262.

  • Zaktualizuj kotwicę ContactManager w Pages/Shared/_Layout.cshtml 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

Inicjowanie bazy danych

Dodaj klasę SeedData do folderu Dane :

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 SeedData.Initialize z :Program.cs

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

var builder = WebApplication.CreateBuilder(args);

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

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

var app = builder.Build();

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

    await SeedData.Initialize(services);
}

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

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

app.UseRouting();

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

app.MapRazorPages();

app.Run();

Przetestuj, czy aplikacja wysunął bazę danych. Jeśli w bazie danych kontaktu znajdują się jakiekolwiek wiersze, metoda inicjowania 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.
  • Administracja istratory 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 (rick@example.com) jest zalogowany. Rick może wyświetlać tylko zatwierdzone kontakty i edytować/usuń/Utwórz nowe 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".

Screenshot showing Rick signed in

Na poniższej ilustracji manager@contoso.com jest zalogowany i w roli menedżera:

Screenshot showing manager@contoso.com signed in

Na poniższej ilustracji przedstawiono widok szczegółów kontaktu menedżerów:

Manager's view of a contact

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

Na poniższej ilustracji admin@contoso.com jest zalogowany i w roli administratora:

Screenshot showing admin@contoso.com signed in

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

Aplikacja została utworzona przez utworzenie szkieletu następującego Contact 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:

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

Wymagania wstępne

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

Aplikacja początkowa i ukończona

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

Aplikacja startowa

Pobierz aplikację 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. Pomocne może być odwołanie się do ukończonego projektu.

Powiązanie danych kontaktowych z użytkownikiem

Użyj ASP.NET Identity identyfikatora użytkownika, aby upewnić się, że użytkownicy mogą edytować swoje dane, ale nie inne dane użytkowników. Dodaj OwnerID element i ContactStatus do Contact 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
}

OwnerID to identyfikator użytkownika z AspNetUser tabeli w Identity bazie danych. Pole Status 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 Identity

Dołącz AddRoles , 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 uwierzytelniania rezerwowego wymagają uwierzytelnienia wszystkich użytkowników, z wyjątkiem Razor stron, kontrolerów lub metod akcji z atrybutem uwierzytelniania. Na przykład Razor strony, kontrolery lub metody akcji z zastosowanym atrybutem [AllowAnonymous] uwierzytelniania, [Authorize(PolicyName="MyPolicy")] a nie zasady uwierzytelniania rezerwowego.

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

Zasady uwierzytelniania rezerwowego:

  • 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 zastosowaniu oprogramowania pośredniczącego autoryzacji, takiego jak pliki statyczne, zasady zostaną zastosowane do wszystkich żądań.

Ustawienie zasad uwierzytelniania rezerwowego w celu wymagania od użytkowników uwierzytelniania chroni nowo dodane Razor strony i kontrolery. Wymagane domyślnie uwierzytelnianie jest bezpieczniejsze niż poleganie na nowych kontrolerach i Razor stronach w celu uwzględnienia atrybutu [Authorize] .

Klasa AuthorizationOptions zawiera AuthorizationOptions.DefaultPolicyrównież element . Jest DefaultPolicy to zasady używane z atrybutem [Authorize] , gdy nie określono żadnych zasad. [Authorize] nie zawiera nazwanych zasad, w przeciwieństwie do [Authorize(PolicyName="MyPolicy")].

Aby uzyskać więcej informacji na temat zasad, zobacz Autoryzacja oparta na zasadach w programie ASP.NET Core.

Alternatywnym sposobem, w jaki kontrolery MVC i Razor 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, ustawiając zasady rezerwowe używają routingu punktu końcowego. Ustawienie zasad rezerwowych jest preferowanym sposobem, aby wymagać uwierzytelnienia wszystkich użytkowników.

Dodaj pozycję AllowAnonymous na Index stronach i Privacy , 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 SeedData 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 projektu (katalog zawierający Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Jeśli nie określono silnego hasła, w wywołaniu SeedData.Initialize jest zgłaszany wyjątek.

Zaktualizuj Main , 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ę Initialize w klasie, SeedData 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 ContactStatus do kontaktów. Utwórz jeden z kontaktów "Przesłane" i jeden "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ę ContactIsOwnerAuthorizationHandler w folderze Autoryzacja . Funkcja ContactIsOwnerAuthorizationHandler 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 ContactIsOwnerAuthorizationHandler wywołań . Powiedz się, jeśli bieżący uwierzytelniony użytkownik jest właścicielem kontaktu. Programy obsługi autoryzacji zazwyczaj:

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

Jeśli musisz jawnie zakończyć się niepowodzeniem, wywołaj kontekst. Niepowodzenie.

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

Tworzenie programu obsługi autoryzacji menedżera

Utwórz klasę ContactManagerAuthorizationHandler w folderze Autoryzacja . Sprawdza ContactManagerAuthorizationHandler , 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ę ContactAdministratorsAuthorizationHandler w folderze Autoryzacja . Sprawdza ContactAdministratorsAuthorizationHandler , czy użytkownik działający na zasobie jest administratorem. Administracja istrator 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 AddScoped. Używa ContactIsOwnerAuthorizationHandler 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 ContactsController iniekcji zależności. Dodaj następujący kod na końcu elementu ConfigureServices:

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

    services.AddRazorPages();

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

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

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

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

ContactAdministratorsAuthorizationHandler i ContactManagerAuthorizationHandler są dodawane jako singletony. Są one singletonami, ponieważ nie używają platformy EF, a wszystkie potrzebne informacje są w Context parametrze HandleRequirementAsync metody .

Obsługa autoryzacji

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

Przejrzyj klasę wymagań dotyczących operacji kontaktu

Przejrzyj klasę ContactOperations . 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 Razor

Utwórz klasę bazową zawierającą usługi używane na stronach kontaktów Razor . 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;
        } 
    }
}

Powyższy kod:

  • Dodaje usługę IAuthorizationService w celu uzyskania dostępu do procedur obsługi autoryzacji.
  • Dodaje usługę IdentityUserManager .
  • Dodaj element ApplicationDbContext.

Aktualizowanie modelu CreateModel

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

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

Zaktualizuj metodę na CreateModel.OnPostAsync :

  • Dodaj identyfikator użytkownika do Contact 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 OnGetAsync , 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 [Authorize] . Aplikacja nie ma dostępu 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 ładując ją w ramach samej procedury obsługi. 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");
    }
}

Wstrzykiwanie usługi 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ć.

Wstrzyknąć usługę autoryzacji w Pages/_ViewImports.cshtml pliku, aby była 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

Powyższy znacznik dodaje kilka using instrukcji.

Zaktualizuj łącza Edytuj i Usuń, Pages/Contacts/Index.cshtml 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>

Ostrzeżenie

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ą. Aby Razor zabezpieczyć dane, strona lub kontroler musi wymusić kontrole dostępu.

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

Zobacz ten problem, aby uzyskać informacje na temat:

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

Powyższy kod:

  • Gdy użytkownik nie zostanie uwierzytelniony, ChallengeResult zostanie zwrócony element . ChallengeResult Gdy element zostanie zwrócony, użytkownik zostanie przekierowany do strony logowania.
  • Gdy użytkownik jest uwierzytelniony, ale nie jest autoryzowany, ForbidResult zwracany jest element . Gdy element ForbidResult zostanie zwrócony, użytkownik zostanie przekierowany do strony 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 i co najmniej jednego znaku, cyfry i symbolu. Na przykład Passw0rd! spełnia wymagania dotyczące silnego hasła.

  • Wykonaj następujące polecenie z folderu projektu, gdzie <PW> jest hasłem:

    dotnet user-secrets set SeedUserPW <PW>
    

Jeśli aplikacja ma kontakty:

  • Usuń wszystkie rekordy w Contact tabeli.
  • Uruchom ponownie aplikację, aby zainicjować 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 test@example.com). 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 Details widoku są wyświetlane przyciski Zatwierdź i Odrzuć .
  • Administracja istratory mogą zatwierdzać/odrzucać i edytować/usuwać wszystkie dane.
Użytkownik Rozmieszczane przez aplikację Opcje
test@example.com Nie. Edytuj/usuń własne dane.
manager@contoso.com Tak Zatwierdzanie/odrzucanie i edytowanie/usuwanie własnych danych.
admin@contoso.com Tak 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 Razor aplikacji Pages o nazwie "ContactManager"

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

    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; }
    }
    
  • Tworzenie szkieletu Contact 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

Uwaga

Domyślnie architektura plików binarnych platformy .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 problem z usługą GitHub dotnet/AspNetCore.Docs #29262.

Jeśli wystąpi błąd z poleceniem dotnet aspnet-codegenerator razorpage , zobacz ten problem z usługą GitHub.

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

Inicjowanie bazy danych

Dodaj klasę SeedData do folderu Dane :

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 SeedData.Initialize z :Main

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

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

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

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

            host.Run();
        }

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

Przetestuj, czy aplikacja wysunął bazę danych. Jeśli w bazie danych kontaktu znajdują się jakiekolwiek wiersze, metoda inicjowania nie zostanie uruchomiona.

Dodatkowe zasoby