Creare un'app Web ASP.NET Core con i dati utente protetti dall'autorizzazione

Di Rick Anderson e Joe Audette

Questa esercitazione illustra come creare un'app Web ASP.NET Core con i dati utente protetti dall'autorizzazione. Visualizza un elenco di contatti creati dagli utenti autenticati (registrati). Esistono tre gruppi di sicurezza:

  • Gli utenti registrati possono visualizzare tutti i dati approvati e possono modificare/eliminare i propri dati.
  • I responsabili possono approvare o rifiutare i dati di contatto. Solo i contatti approvati sono visibili agli utenti.
  • Amministrazione istrator possono approvare/rifiutare e modificare/eliminare i dati.

Le immagini di questo documento non corrispondono esattamente ai modelli più recenti.

Nell'immagine seguente l'utente Rick (rick@example.com) ha eseguito l'accesso. Rick può visualizzare solo i contatti approvati e Modifica/Elimina/crea nuovi collegamenti per i suoi contatti. Solo l'ultimo record, creato da Rick, visualizza i collegamenti Modifica ed Elimina . Gli altri utenti non vedranno l'ultimo record finché un manager o un amministratore non modifica lo stato su "Approvato".

Screenshot showing Rick signed in

Nell'immagine seguente viene manager@contoso.com eseguito l'accesso e il ruolo del manager:

Screenshot showing manager@contoso.com signed in

L'immagine seguente mostra la visualizzazione dei dettagli dei manager di un contatto:

Manager's view of a contact

I pulsanti Approva e Rifiuta vengono visualizzati solo per i manager e gli amministratori.

Nell'immagine seguente viene admin@contoso.com eseguito l'accesso e il ruolo dell'amministratore:

Screenshot showing admin@contoso.com signed in

L'amministratore dispone di tutti i privilegi. Può leggere, modificare o eliminare qualsiasi contatto e modificare lo stato dei contatti.

L'app è stata creata eseguendo lo scaffolding del modello seguente Contact :

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

L'esempio contiene i gestori di autorizzazione seguenti:

  • ContactIsOwnerAuthorizationHandler: assicura che un utente possa modificare solo i dati.
  • ContactManagerAuthorizationHandler: consente ai responsabili di approvare o rifiutare i contatti.
  • ContactAdministratorsAuthorizationHandler: consente agli amministratori di approvare o rifiutare i contatti e di modificare/eliminare i contatti.

Prerequisiti

Questa esercitazione è avanzata. È necessario avere familiarità con:

App iniziale e completata

Scaricare l'app completata . Testare l'app completata in modo da acquisire familiarità con le funzionalità di sicurezza.

L'app di avvio

Scaricare l'app iniziale .

Eseguire l'app, toccare il collegamento ContactManager e verificare che sia possibile creare, modificare ed eliminare un contatto. Per creare l'app iniziale, vedere Creare l'app iniziale.

Proteggere i dati utente

Le sezioni seguenti illustrano tutti i passaggi principali per creare l'app per i dati utente sicuri. Può risultare utile fare riferimento al progetto completato.

Collegare i dati di contatto all'utente

Usare l'ID utente ASP.NET Identity per assicurarsi che gli utenti possano modificare i dati, ma non altri dati degli utenti. Aggiungere OwnerID e ContactStatus al Contact modello:

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 è l'ID dell'utente della AspNetUser tabella nel Identity database. Il Status campo determina se un contatto è visualizzabile dagli utenti generali.

Creare una nuova migrazione e aggiornare il database:

dotnet ef migrations add userID_Status
dotnet ef database update

Aggiungere servizi ruolo a Identity

Aggiungere AddRoles per aggiungere servizi ruolo:

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

Richiedere utenti autenticati

Impostare i criteri di autorizzazione di fallback per richiedere l'autenticazione degli utenti:

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

Il codice evidenziato precedente imposta i criteri di autorizzazione di fallback. I criteri di autorizzazione di fallback richiedono l'autenticazione di tutti gli utenti, ad eccezione di Razor Pages, controller o metodi di azione con un attributo di autorizzazione. Ad esempio, Razor Pages, controller o metodi di azione con [AllowAnonymous] o [Authorize(PolicyName="MyPolicy")] usano l'attributo di autorizzazione applicato anziché i criteri di autorizzazione di fallback.

RequireAuthenticatedUser aggiunge DenyAnonymousAuthorizationRequirement all'istanza corrente, che impone l'autenticazione dell'utente corrente.

Criteri di autorizzazione di fallback:

  • Viene applicato a tutte le richieste che non specificano in modo esplicito un criterio di autorizzazione. Per le richieste gestite dal routing degli endpoint, include qualsiasi endpoint che non specifichi un attributo di autorizzazione. Per le richieste gestite da altri middleware dopo il middleware di autorizzazione, ad esempio i file statici, questo applica il criterio a tutte le richieste.

L'impostazione dei criteri di autorizzazione di fallback per richiedere l'autenticazione degli utenti protegge le pagine e i controller appena aggiunti Razor . La disponibilità dell'autorizzazione richiesta per impostazione predefinita è più sicura rispetto alla presenza di nuovi controller e Razor pagine per includere l'attributo [Authorize] .

La AuthorizationOptions classe contiene AuthorizationOptions.DefaultPolicyanche . DefaultPolicy è il criterio usato con l'attributo [Authorize] quando non viene specificato alcun criterio. [Authorize] non contiene criteri denominati, a differenza [Authorize(PolicyName="MyPolicy")]di .

Per altre informazioni sui criteri, vedere Autorizzazione basata su criteri in ASP.NET Core.

Un modo alternativo per i controller MVC e Razor le pagine per richiedere l'autenticazione di tutti gli utenti consiste nell'aggiungere un filtro di autorizzazione:

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

Il codice precedente usa un filtro di autorizzazione, impostando il criterio di fallback usa il routing degli endpoint. L'impostazione dei criteri di fallback è il modo migliore per richiedere l'autenticazione di tutti gli utenti.

Aggiungere AllowAnonymous alle Index pagine e Privacy in modo che gli utenti anonimi possano ottenere informazioni sul sito prima di registrarsi:

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

    }
}

Configurare l'account di test

La SeedData classe crea due account: amministratore e responsabile. Usare lo strumento Secret Manager per impostare una password per questi account. Impostare la password dalla directory del progetto (la directory contenente Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Se non viene specificata una password complessa, viene generata un'eccezione quando SeedData.Initialize viene chiamato .

Aggiornare l'app per usare la password di test:

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

Creare gli account di test e aggiornare i contatti

Aggiornare il Initialize metodo nella SeedData classe per creare gli account di test:

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

Aggiungere l'ID utente amministratore e ContactStatus i contatti. Effettuare uno dei contatti "Inviati" e uno "Rifiutato". Aggiungere l'ID utente e lo stato a tutti i contatti. Viene visualizzato un solo contatto:

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

Creare gestori di autorizzazione proprietario, responsabile e amministratore

Creare una ContactIsOwnerAuthorizationHandler classe nella cartella Authorization . Verifica ContactIsOwnerAuthorizationHandler che l'utente che agisca su una risorsa sia proprietaria della risorsa.

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

Contesto ContactIsOwnerAuthorizationHandler delle chiamate . Esito positivo se l'utente autenticato corrente è il proprietario del contatto. I gestori di autorizzazione in genere:

  • Chiamare context.Succeed quando vengono soddisfatti i requisiti.
  • Restituisce Task.CompletedTask quando i requisiti non vengono soddisfatti. La restituzione Task.CompletedTask senza una chiamata precedente a context.Success o context.Fail, non è un esito positivo o negativo, consente l'esecuzione di altri gestori di autorizzazione.

Se è necessario avere esito negativo in modo esplicito, chiamare il contesto. Errore.

L'app consente ai proprietari dei contatti di modificare/eliminare/creare i propri dati. ContactIsOwnerAuthorizationHandler non è necessario controllare l'operazione passata nel parametro del requisito.

Creare un gestore di autorizzazione del manager

Creare una ContactManagerAuthorizationHandler classe nella cartella Authorization . ContactManagerAuthorizationHandler Verifica che l'utente che agisca sulla risorsa sia un manager. Solo i responsabili possono approvare o rifiutare le modifiche al contenuto (nuove o modificate).

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

Creare un gestore di autorizzazione dell'amministratore

Creare una ContactAdministratorsAuthorizationHandler classe nella cartella Authorization . ContactAdministratorsAuthorizationHandler Verifica che l'utente che agisca sulla risorsa sia un amministratore. Amministrazione istrator può eseguire tutte le operazioni.

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

Registrare i gestori di autorizzazione

I servizi che usano Entity Framework Core devono essere registrati per l'inserimento delle dipendenze tramite AddScoped. ContactIsOwnerAuthorizationHandler usa ASP.NET CoreIdentity, basato su Entity Framework Core. Registrare i gestori con la raccolta di servizi in modo che siano disponibili per tramite l'inserimento ContactsControllerdelle dipendenze. Aggiungere il codice seguente alla fine di 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 e ContactManagerAuthorizationHandler vengono aggiunti come singleton. Sono singleton perché non usano Entity Framework e tutte le informazioni necessarie si trovano nel Context parametro del HandleRequirementAsync metodo .

Autorizzazione di supporto

In questa sezione si aggiornaNo le Razor pagine e si aggiunge una classe di requisiti per le operazioni.

Esaminare la classe dei requisiti delle operazioni di contatto

Esaminare la ContactOperations classe . Questa classe contiene i requisiti supportati dall'app:

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

Creare una classe di base per le pagine contatti Razor

Creare una classe base contenente i servizi utilizzati nelle pagine dei contatti Razor . La classe base inserisce il codice di inizializzazione in un'unica posizione:

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

Il codice precedente:

  • Aggiunge il IAuthorizationService servizio per accedere ai gestori di autorizzazione.
  • Aggiunge il IdentityUserManager servizio.
  • Aggiungere l'oggetto ApplicationDbContext.

Aggiornare CreateModel

Aggiornare il modello di pagina di creazione:

  • Costruttore per l'uso della DI_BasePageModel classe base.
  • OnPostAsync metodo per:
    • Aggiungere l'ID utente al Contact modello.
    • Chiamare il gestore dell'autorizzazione per verificare che l'utente disponga dell'autorizzazione per creare contatti.
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");
        }
    }
}

Aggiornare IndexModel

Aggiornare il OnGetAsync metodo in modo che vengano visualizzati solo i contatti approvati agli utenti generali:

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

Aggiornare EditModel

Aggiungere un gestore di autorizzazione per verificare che l'utente sia proprietario del contatto. Poiché l'autorizzazione delle risorse viene convalidata, l'attributo [Authorize] non è sufficiente. L'app non ha accesso alla risorsa quando vengono valutati gli attributi. L'autorizzazione basata sulle risorse deve essere imperativa. I controlli devono essere eseguiti una volta che l'app ha accesso alla risorsa, caricandola nel modello di pagina o caricandola all'interno del gestore stesso. Spesso si accede alla risorsa passando la chiave di risorsa.

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

Aggiornare DeleteModel

Aggiornare il modello di pagina di eliminazione per usare il gestore di autorizzazione per verificare che l'utente disponga dell'autorizzazione di eliminazione per il contatto.

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

Inserire il servizio di autorizzazione nelle visualizzazioni

Attualmente, l'interfaccia utente mostra collegamenti di modifica ed eliminazione per i contatti che l'utente non può modificare.

Inserire il servizio di autorizzazione nel Pages/_ViewImports.cshtml file in modo che sia disponibile per tutte le visualizzazioni:

@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

Il markup precedente aggiunge diverse using istruzioni.

Aggiornare i collegamenti Modifica ed Elimina in Pages/Contacts/Index.cshtml in modo che vengano visualizzati solo per gli utenti con le autorizzazioni appropriate:

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

Avviso

Nascondere i collegamenti agli utenti che non dispongono dell'autorizzazione per modificare i dati non protegge l'app. Nascondere i collegamenti rende l'app più intuitiva visualizzando solo collegamenti validi. Gli utenti possono violare gli URL generati per richiamare operazioni di modifica ed eliminazione sui dati di cui non sono proprietari. Il Razor controller o la pagina deve applicare i controlli di accesso per proteggere i dati.

Dettagli aggiornamento

Aggiornare la visualizzazione dei dettagli in modo che i responsabili possano approvare o rifiutare i contatti:

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

Aggiornare il modello di pagina dei dettagli

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

Aggiungere o rimuovere un utente a un ruolo

Per informazioni su, vedere questo problema :

  • Rimozione dei privilegi da un utente. Ad esempio, disattivare un utente in un'app di chat.
  • Aggiunta di privilegi a un utente.

Differenze tra sfida e proibissi

Questa app imposta i criteri predefiniti per richiedere gli utenti autenticati. Il codice seguente consente agli utenti anonimi. Gli utenti anonimi possono mostrare le differenze tra Challenge e 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();
    }
}

Nel codice precedente:

  • Quando l'utente non è autenticato, viene restituito un oggetto ChallengeResult . Quando viene restituito un oggetto ChallengeResult , l'utente viene reindirizzato alla pagina di accesso.
  • Quando l'utente viene autenticato, ma non autorizzato, viene restituito un oggetto ForbidResult . Quando viene restituito un oggetto ForbidResult , l'utente viene reindirizzato alla pagina di accesso negato.

Testare l'app completata

Se non è già stata impostata una password per gli account utente con seeding, usare lo strumento Secret Manager per impostare una password:

  • Scegliere una password complessa: usare otto o più caratteri e almeno un carattere maiuscolo, un numero e un simbolo. Ad esempio, Passw0rd! soddisfa i requisiti della password complessa.

  • Eseguire il comando seguente dalla cartella del progetto, dove <PW> è la password:

    dotnet user-secrets set SeedUserPW <PW>
    

Se l'app ha contatti:

  • Eliminare tutti i record nella Contact tabella.
  • Riavviare l'app per eseguire il seeding del database.

Un modo semplice per testare l'app completata consiste nell'avviare tre browser diversi (o sessioni in incognito/InPrivate). In un browser registrare un nuovo utente , ad esempio test@example.com. Accedere a ogni browser con un utente diverso. Verificare le operazioni seguenti:

  • Gli utenti registrati possono visualizzare tutti i dati di contatto approvati.
  • Gli utenti registrati possono modificare o eliminare i propri dati.
  • I responsabili possono approvare/rifiutare i dati di contatto. La Details visualizzazione mostra i pulsanti Approva e Rifiuta .
  • Amministrazione istrator possono approvare/rifiutare e modificare/eliminare tutti i dati.
User Approvare o rifiutare i contatti Opzioni
test@example.com No Modificare ed eliminare i dati.
manager@contoso.com Modificare ed eliminare i dati.
admin@contoso.com Modificare ed eliminare tutti i dati.

Creare un contatto nel browser dell'amministratore. Copiare l'URL per eliminare e modificare dal contatto dell'amministratore. Incollare questi collegamenti nel browser dell'utente di test per verificare che l'utente di test non possa eseguire queste operazioni.

Creare l'app iniziale

  • Creare un'app Razor Pages denominata "ContactManager"

    • Creare l'app con singoli account utente.
    • Denominarlo "ContactManager" in modo che lo spazio dei nomi corrisponda allo spazio dei nomi usato nell'esempio.
    • -uldspecifica Local DB anziché SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Aggiungi 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; }
        }
    }
    
  • Eseguire lo scaffolding del Contact modello.

  • Creare la migrazione iniziale e aggiornare il database:

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

Nota

Per impostazione predefinita, l'architettura dei file binari .NET da installare rappresenta l'architettura del sistema operativo attualmente in esecuzione. Per specificare un'architettura del sistema operativo diversa, vedere l'opzione dotnet tool install, --arch. Per altre informazioni, vedere Problema di GitHub dotnet/AspNetCore.Docs #29262.

  • Aggiornare l'ancoraggio ContactManager nel Pages/Shared/_Layout.cshtml file:

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • Testare l'app creando, modificando ed eliminando un contatto

Specificare il valore di inizializzazione del database

Aggiungere la classe SeedData alla cartella Data :

using ContactManager.Models;
using Microsoft.EntityFrameworkCore;

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

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

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

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

    }
}

Chiamare SeedData.Initialize da 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();

Verificare che l'app ha eseguito il seeding del database. Se sono presenti righe nel database di contatto, il metodo di inizializzazione non viene eseguito.

Questa esercitazione illustra come creare un'app Web ASP.NET Core con i dati utente protetti dall'autorizzazione. Visualizza un elenco di contatti creati dagli utenti autenticati (registrati). Esistono tre gruppi di sicurezza:

  • Gli utenti registrati possono visualizzare tutti i dati approvati e possono modificare/eliminare i propri dati.
  • I responsabili possono approvare o rifiutare i dati di contatto. Solo i contatti approvati sono visibili agli utenti.
  • Amministrazione istrator possono approvare/rifiutare e modificare/eliminare i dati.

Le immagini di questo documento non corrispondono esattamente ai modelli più recenti.

Nell'immagine seguente l'utente Rick (rick@example.com) ha eseguito l'accesso. Rick può visualizzare solo i contatti approvati e Modifica/Elimina/crea nuovi collegamenti per i suoi contatti. Solo l'ultimo record, creato da Rick, visualizza i collegamenti Modifica ed Elimina . Gli altri utenti non vedranno l'ultimo record finché un manager o un amministratore non modifica lo stato su "Approvato".

Screenshot showing Rick signed in

Nell'immagine seguente viene manager@contoso.com eseguito l'accesso e il ruolo del manager:

Screenshot showing manager@contoso.com signed in

L'immagine seguente mostra la visualizzazione dei dettagli dei manager di un contatto:

Manager's view of a contact

I pulsanti Approva e Rifiuta vengono visualizzati solo per i manager e gli amministratori.

Nell'immagine seguente viene admin@contoso.com eseguito l'accesso e il ruolo dell'amministratore:

Screenshot showing admin@contoso.com signed in

L'amministratore dispone di tutti i privilegi. Può leggere,modificare/eliminare qualsiasi contatto e modificare lo stato dei contatti.

L'app è stata creata eseguendo lo scaffolding del modello seguente Contact :

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

L'esempio contiene i gestori di autorizzazione seguenti:

  • ContactIsOwnerAuthorizationHandler: assicura che un utente possa modificare solo i dati.
  • ContactManagerAuthorizationHandler: consente ai responsabili di approvare o rifiutare i contatti.
  • ContactAdministratorsAuthorizationHandler: consente agli amministratori di:
    • Approvare o rifiutare i contatti
    • Modificare ed eliminare i contatti

Prerequisiti

Questa esercitazione è avanzata. È necessario avere familiarità con:

App iniziale e completata

Scaricare l'app completata . Testare l'app completata in modo da acquisire familiarità con le funzionalità di sicurezza.

L'app di avvio

Scaricare l'app iniziale .

Eseguire l'app, toccare il collegamento ContactManager e verificare che sia possibile creare, modificare ed eliminare un contatto. Per creare l'app iniziale, vedere Creare l'app iniziale.

Proteggere i dati utente

Le sezioni seguenti illustrano tutti i passaggi principali per creare l'app per i dati utente sicuri. Può risultare utile fare riferimento al progetto completato.

Collegare i dati di contatto all'utente

Usare l'ID utente ASP.NET Identity per assicurarsi che gli utenti possano modificare i dati, ma non altri dati degli utenti. Aggiungere OwnerID e ContactStatus al Contact modello:

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 è l'ID dell'utente della AspNetUser tabella nel Identity database. Il Status campo determina se un contatto è visualizzabile dagli utenti generali.

Creare una nuova migrazione e aggiornare il database:

dotnet ef migrations add userID_Status
dotnet ef database update

Aggiungere servizi ruolo a Identity

Aggiungere AddRoles per aggiungere servizi ruolo:

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

Richiedere utenti autenticati

Impostare i criteri di autenticazione di fallback per richiedere l'autenticazione degli utenti:

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

Il codice evidenziato precedente imposta i criteri di autenticazione di fallback. I criteri di autenticazione di fallback richiedono che tutti gli utenti siano autenticati, ad eccezione di Razor Pages, controller o metodi di azione con un attributo di autenticazione. Ad esempio, Razor Pagine, controller o metodi di azione con [AllowAnonymous] o [Authorize(PolicyName="MyPolicy")] usare l'attributo di autenticazione applicato anziché i criteri di autenticazione di fallback.

RequireAuthenticatedUser aggiunge DenyAnonymousAuthorizationRequirement all'istanza corrente, che impone l'autenticazione dell'utente corrente.

Criteri di autenticazione di fallback:

  • Viene applicato a tutte le richieste che non specificano in modo esplicito un criterio di autenticazione. Per le richieste gestite dal routing degli endpoint, questo include qualsiasi endpoint che non specifica un attributo di autorizzazione. Per le richieste gestite da altri middleware dopo il middleware di autorizzazione, ad esempio i file statici, il criterio verrà applicato a tutte le richieste.

L'impostazione dei criteri di autenticazione di fallback per richiedere l'autenticazione degli utenti protegge le pagine e i controller appena aggiunti Razor . La presenza dell'autenticazione richiesta per impostazione predefinita è più sicura rispetto alla presenza di nuovi controller e Razor pagine per includere l'attributo [Authorize] .

La AuthorizationOptions classe contiene AuthorizationOptions.DefaultPolicyanche . DefaultPolicy è il criterio usato con l'attributo [Authorize] quando non viene specificato alcun criterio. [Authorize] non contiene criteri denominati, a differenza [Authorize(PolicyName="MyPolicy")]di .

Per altre informazioni sui criteri, vedere Autorizzazione basata su criteri in ASP.NET Core.

Un modo alternativo per i controller MVC e Razor le pagine per richiedere l'autenticazione di tutti gli utenti consiste nell'aggiungere un filtro di autorizzazione:

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

Il codice precedente usa un filtro di autorizzazione, impostando il criterio di fallback usa il routing degli endpoint. L'impostazione dei criteri di fallback è il modo migliore per richiedere l'autenticazione di tutti gli utenti.

Aggiungere AllowAnonymous alle Index pagine e Privacy in modo che gli utenti anonimi possano ottenere informazioni sul sito prima di registrarsi:

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

        }
    }
}

Configurare l'account di test

La SeedData classe crea due account: amministratore e responsabile. Usare lo strumento Secret Manager per impostare una password per questi account. Impostare la password dalla directory del progetto (la directory contenente Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Se non viene specificata una password complessa, viene generata un'eccezione quando SeedData.Initialize viene chiamato .

Aggiornare Main per usare la password di test:

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

Creare gli account di test e aggiornare i contatti

Aggiornare il Initialize metodo nella SeedData classe per creare gli account di test:

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

Aggiungere l'ID utente amministratore e ContactStatus i contatti. Effettuare uno dei contatti "Inviati" e uno "Rifiutato". Aggiungere l'ID utente e lo stato a tutti i contatti. Viene visualizzato un solo contatto:

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

Creare gestori di autorizzazione proprietario, responsabile e amministratore

Creare una ContactIsOwnerAuthorizationHandler classe nella cartella Authorization . Verifica ContactIsOwnerAuthorizationHandler che l'utente che agisca su una risorsa sia proprietaria della risorsa.

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

Contesto ContactIsOwnerAuthorizationHandler delle chiamate . Esito positivo se l'utente autenticato corrente è il proprietario del contatto. I gestori di autorizzazione in genere:

  • Chiamare context.Succeed quando vengono soddisfatti i requisiti.
  • Restituisce Task.CompletedTask quando i requisiti non vengono soddisfatti. La restituzione Task.CompletedTask senza una chiamata precedente a context.Success o context.Fail, non è un esito positivo o negativo, consente l'esecuzione di altri gestori di autorizzazione.

Se è necessario avere esito negativo in modo esplicito, chiamare il contesto. Errore.

L'app consente ai proprietari dei contatti di modificare/eliminare/creare i propri dati. ContactIsOwnerAuthorizationHandler non è necessario controllare l'operazione passata nel parametro del requisito.

Creare un gestore di autorizzazione del manager

Creare una ContactManagerAuthorizationHandler classe nella cartella Authorization . ContactManagerAuthorizationHandler Verifica che l'utente che agisca sulla risorsa sia un manager. Solo i responsabili possono approvare o rifiutare le modifiche al contenuto (nuove o modificate).

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

Creare un gestore di autorizzazione dell'amministratore

Creare una ContactAdministratorsAuthorizationHandler classe nella cartella Authorization . ContactAdministratorsAuthorizationHandler Verifica che l'utente che agisca sulla risorsa sia un amministratore. Amministrazione istrator può eseguire tutte le operazioni.

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

Registrare i gestori di autorizzazione

I servizi che usano Entity Framework Core devono essere registrati per l'inserimento delle dipendenze tramite AddScoped. ContactIsOwnerAuthorizationHandler usa ASP.NET CoreIdentity, basato su Entity Framework Core. Registrare i gestori con la raccolta di servizi in modo che siano disponibili per tramite l'inserimento ContactsControllerdelle dipendenze. Aggiungere il codice seguente alla fine di 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 e ContactManagerAuthorizationHandler vengono aggiunti come singleton. Sono singleton perché non usano Entity Framework e tutte le informazioni necessarie si trovano nel Context parametro del HandleRequirementAsync metodo .

Autorizzazione di supporto

In questa sezione si aggiornaNo le Razor pagine e si aggiunge una classe di requisiti per le operazioni.

Esaminare la classe dei requisiti delle operazioni di contatto

Esaminare la ContactOperations classe . Questa classe contiene i requisiti supportati dall'app:

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

Creare una classe di base per le pagine contatti Razor

Creare una classe base contenente i servizi utilizzati nelle pagine dei contatti Razor . La classe base inserisce il codice di inizializzazione in un'unica posizione:

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

Il codice precedente:

  • Aggiunge il IAuthorizationService servizio per accedere ai gestori di autorizzazione.
  • Aggiunge il IdentityUserManager servizio.
  • Aggiungere l'oggetto ApplicationDbContext.

Aggiornare CreateModel

Aggiornare il costruttore del modello di pagina di creazione per usare la DI_BasePageModel classe base:

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

Aggiornare il CreateModel.OnPostAsync metodo in:

  • Aggiungere l'ID utente al Contact modello.
  • Chiamare il gestore dell'autorizzazione per verificare che l'utente disponga dell'autorizzazione per creare contatti.
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");
}

Aggiornare IndexModel

Aggiornare il OnGetAsync metodo in modo che vengano visualizzati solo i contatti approvati agli utenti generali:

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

Aggiornare EditModel

Aggiungere un gestore di autorizzazione per verificare che l'utente sia proprietario del contatto. Poiché l'autorizzazione delle risorse viene convalidata, l'attributo [Authorize] non è sufficiente. L'app non ha accesso alla risorsa quando vengono valutati gli attributi. L'autorizzazione basata sulle risorse deve essere imperativa. I controlli devono essere eseguiti una volta che l'app ha accesso alla risorsa, caricandola nel modello di pagina o caricandola all'interno del gestore stesso. Spesso si accede alla risorsa passando la chiave di risorsa.

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

Aggiornare DeleteModel

Aggiornare il modello di pagina di eliminazione per usare il gestore di autorizzazione per verificare che l'utente disponga dell'autorizzazione di eliminazione per il contatto.

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

Inserire il servizio di autorizzazione nelle visualizzazioni

Attualmente, l'interfaccia utente mostra collegamenti di modifica ed eliminazione per i contatti che l'utente non può modificare.

Inserire il servizio di autorizzazione nel Pages/_ViewImports.cshtml file in modo che sia disponibile per tutte le visualizzazioni:

@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

Il markup precedente aggiunge diverse using istruzioni.

Aggiornare i collegamenti Modifica ed Elimina in Pages/Contacts/Index.cshtml in modo che vengano visualizzati solo per gli utenti con le autorizzazioni appropriate:

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

Avviso

Nascondere i collegamenti agli utenti che non dispongono dell'autorizzazione per modificare i dati non protegge l'app. Nascondere i collegamenti rende l'app più intuitiva visualizzando solo collegamenti validi. Gli utenti possono violare gli URL generati per richiamare operazioni di modifica ed eliminazione sui dati di cui non sono proprietari. Il Razor controller o la pagina deve applicare i controlli di accesso per proteggere i dati.

Dettagli aggiornamento

Aggiornare la visualizzazione dei dettagli in modo che i responsabili possano approvare o rifiutare i contatti:

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

Aggiornare il modello di pagina dei dettagli:

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

Aggiungere o rimuovere un utente a un ruolo

Per informazioni su, vedere questo problema :

  • Rimozione dei privilegi da un utente. Ad esempio, disattivare un utente in un'app di chat.
  • Aggiunta di privilegi a un utente.

Differenze tra sfida e proibissi

Questa app imposta i criteri predefiniti per richiedere gli utenti autenticati. Il codice seguente consente agli utenti anonimi. Gli utenti anonimi possono mostrare le differenze tra Challenge e 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();
    }
}

Nel codice precedente:

  • Quando l'utente non è autenticato, viene restituito un oggetto ChallengeResult . Quando viene restituito un oggetto ChallengeResult , l'utente viene reindirizzato alla pagina di accesso.
  • Quando l'utente viene autenticato, ma non autorizzato, viene restituito un oggetto ForbidResult . Quando viene restituito un oggetto ForbidResult , l'utente viene reindirizzato alla pagina di accesso negato.

Testare l'app completata

Se non è già stata impostata una password per gli account utente con seeding, usare lo strumento Secret Manager per impostare una password:

  • Scegliere una password complessa: usare otto o più caratteri e almeno un carattere maiuscolo, un numero e un simbolo. Ad esempio, Passw0rd! soddisfa i requisiti della password complessa.

  • Eseguire il comando seguente dalla cartella del progetto, dove <PW> è la password:

    dotnet user-secrets set SeedUserPW <PW>
    

Se l'app ha contatti:

  • Eliminare tutti i record nella Contact tabella.
  • Riavviare l'app per eseguire il seeding del database.

Un modo semplice per testare l'app completata consiste nell'avviare tre browser diversi (o sessioni in incognito/InPrivate). In un browser registrare un nuovo utente , ad esempio test@example.com. Accedere a ogni browser con un utente diverso. Verificare le operazioni seguenti:

  • Gli utenti registrati possono visualizzare tutti i dati di contatto approvati.
  • Gli utenti registrati possono modificare o eliminare i propri dati.
  • I responsabili possono approvare/rifiutare i dati di contatto. La Details visualizzazione mostra i pulsanti Approva e Rifiuta .
  • Amministrazione istrator possono approvare/rifiutare e modificare/eliminare tutti i dati.
User Seeding dall'app Opzioni
test@example.com No Modificare/eliminare i propri dati.
manager@contoso.com Approvare/rifiutare e modificare/eliminare dati personalizzati.
admin@contoso.com Approvare/rifiutare e modificare/eliminare tutti i dati.

Creare un contatto nel browser dell'amministratore. Copiare l'URL per eliminare e modificare dal contatto dell'amministratore. Incollare questi collegamenti nel browser dell'utente di test per verificare che l'utente di test non possa eseguire queste operazioni.

Creare l'app iniziale

  • Creare un'app Razor Pages denominata "ContactManager"

    • Creare l'app con singoli account utente.
    • Denominarlo "ContactManager" in modo che lo spazio dei nomi corrisponda allo spazio dei nomi usato nell'esempio.
    • -uldspecifica Local DB anziché SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Aggiungi Models/Contact.cs:

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

  • Creare la migrazione iniziale e aggiornare il database:

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

Nota

Per impostazione predefinita, l'architettura dei file binari .NET da installare rappresenta l'architettura del sistema operativo attualmente in esecuzione. Per specificare un'architettura del sistema operativo diversa, vedere l'opzione dotnet tool install, --arch. Per altre informazioni, vedere Problema di GitHub dotnet/AspNetCore.Docs #29262.

Se si verifica un bug con il dotnet aspnet-codegenerator razorpage comando, vedere questo problema di GitHub.

  • Aggiornare l'ancoraggio ContactManager nel Pages/Shared/_Layout.cshtml file:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Testare l'app creando, modificando ed eliminando un contatto

Specificare il valore di inizializzazione del database

Aggiungere la classe SeedData alla cartella Data :

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

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

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

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

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

    }
}

Chiamare SeedData.Initialize da 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>();
                });
    }
}

Verificare che l'app ha eseguito il seeding del database. Se sono presenti righe nel database di contatto, il metodo di inizializzazione non viene eseguito.

Risorse aggiuntive