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.
- Gli amministratori possono approvare/rifiutare e modificare/eliminare qualsiasi dato.
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".
Nell'immagine seguente viene manager@contoso.com
eseguito l'accesso e il ruolo del manager:
L'immagine seguente mostra la visualizzazione dei dettagli dei manager di un contatto:
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:
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:
- ASP.NET Core
- Autenticazione
- Conferma dell'account e recupero della password
- Autorizzazione
- Entity Framework Core
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
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 viene specificata una password debole, 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 restituzioneTask.CompletedTask
senza una chiamata precedente acontext.Success
ocontext.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. L'amministratore 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 ContactsController
delle 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 Identity
UserManager
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.
- Aggiungere l'ID utente al
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 oggettoChallengeResult
, 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 oggettoForbidResult
, l'utente viene reindirizzato alla pagina di accesso negato.
Testare l'app completata
Avviso
Questo articolo usa lo strumento Secret Manager per archiviare la password per gli account utente con seeding. Lo strumento Secret Manager viene usato per archiviare i dati sensibili durante lo sviluppo locale. Per informazioni sulle procedure di autenticazione che possono essere usate quando un'app viene distribuita in un ambiente di test o produzione, vedere Proteggere i flussi di autenticazione.
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:
- Almeno 12 caratteri, ma 14 o più sono migliori.
- Combinazione di lettere maiuscole, lettere minuscole, numeri e simboli.
- Non una parola che può essere trovata in un dizionario o il nome di una persona, un carattere, un prodotto o un'organizzazione.
- Significativamente diversa dalle password precedenti.
- Facile da ricordare ma difficile per gli altri di indovinare. Prendere in considerazione l'uso di una frase memorabile come "6MonkeysRLooking^".
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 . - Gli amministratori 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 | Sì | Modificare ed eliminare i dati. |
admin@contoso.com | Sì | 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.
-uld
specifica LocalDB anziché SQLite
dotnet new webapp -o ContactManager -au Individual -uld
Aggiungi
Models/Contact.cs
: secure-data\samples\starter6\ContactManager\Models\Contact.csusing 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 dotnet tool install, --opzione 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.
- Gli amministratori possono approvare/rifiutare e modificare/eliminare qualsiasi dato.
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".
Nell'immagine seguente viene manager@contoso.com
eseguito l'accesso e il ruolo del manager:
L'immagine seguente mostra la visualizzazione dei dettagli dei manager di un contatto:
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:
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:
- ASP.NET Core
- Autenticazione
- Conferma dell'account e recupero della password
- Autorizzazione
- Entity Framework Core
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
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 restituzioneTask.CompletedTask
senza una chiamata precedente acontext.Success
ocontext.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. L'amministratore 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 ContactsController
delle 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 Identity
UserManager
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 oggettoChallengeResult
, 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 oggettoForbidResult
, 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 . - Gli amministratori 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 | Sì | Approvare/rifiutare e modificare/eliminare dati personalizzati. |
admin@contoso.com | Sì | 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.
-uld
specifica LocalDB anziché SQLite
dotnet new webapp -o ContactManager -au Individual -uld
Aggiungere
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 dotnet tool install, --opzione 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
- Esercitazione: Creare un'app ASP.NET Core con database SQL di Azure nel servizio app di Azure
- ASP.NET lab di autorizzazione di base. Questo lab illustra in dettaglio le funzionalità di sicurezza introdotte in questa esercitazione.
- Introduzione all'autorizzazione in ASP.NET Core
- Autorizzazione personalizzata basata su criteri