Créer une application Web ASP.NET Core avec des données utilisateur protégées par autorisation
Par Rick Anderson et Joe Audette
Ce didacticiel montre comment créer une application Web ASP.NET Core avec des données utilisateur protégées par autorisation. Il affiche une liste de contacts créés par des utilisateurs authentifiés (enregistrés). Il existe trois groupes de sécurité :
- Les utilisateurs enregistrés peuvent voir toutes les données approuvées et peuvent modifier/supprimer leurs propres données.
- Les responsables peuvent approuver ou rejeter les données de contact. Seuls les contacts approuvés sont visibles pour les utilisateurs.
- Les administrateurs peuvent approuver/rejeter et modifier/supprimer toutes les données.
Les images de ce document ne correspondent pas exactement aux derniers modèles.
Dans l'image suivante, l'utilisateur Rick (rick@example.com
) est connecté. Rick ne peut afficher que les contacts approuvés et Modifier/Supprimer/Créer de nouveaux liens pour ses contacts. Seul le dernier enregistrement, créé par Rick, affiche les liens Modifier et Supprimer. Les autres utilisateurs ne verront pas le dernier enregistrement tant qu'un responsable ou un administrateur n'aura pas modifié le statut en "Approuvé".
Dans l'image suivante, manager@contoso.com
est connecté et dans le rôle du gestionnaire :
L'image suivante montre la vue des détails du responsable d'un contact :
Les boutons Approuver et Rejeter ne sont affichés que pour les responsables et les administrateurs.
Dans l'image suivante, admin@contoso.com
est connecté et dans le rôle d'administrateur :
L'administrateur a tous les privilèges. Elle peut lire, modifier ou supprimer n'importe quel contact et modifier le statut des contacts.
L'application a été créée en échafaudant le modèle Contact
suivant :
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'exemple contient les gestionnaires d'autorisation suivants :
ContactIsOwnerAuthorizationHandler
: Garantit qu'un utilisateur ne peut modifier que ses données.ContactManagerAuthorizationHandler
: Permet aux responsables d'approuver ou de rejeter des contacts.ContactAdministratorsAuthorizationHandler
: Permet aux administrateurs d'approuver ou de rejeter des contacts et de modifier/supprimer des contacts.
Prérequis
Ce tutoriel est avancé. Vous devez être familiarisé avec :
- ASP.NET Core
- Authentification
- Confirmation de compte et récupération de mot de passe
- Autorisation
- Entity Framework Core
L'application de démarrage et terminée
Téléchargez l'application terminée. Testez l'application terminée afin de vous familiariser avec ses fonctions de sécurité.
Application de démarrage
Téléchargez l'application de démarrage.
Exécutez l'application, appuyez sur le lien ContactManager et vérifiez que vous pouvez créer, modifier et supprimer un contact. Pour créer l'application de démarrage, voir Créer l'application de démarrage.
Sécuriser les données utilisateur
Les sections suivantes présentent toutes les étapes principales pour créer l'application de données utilisateur sécurisées. Vous trouverez peut-être utile de vous reporter au projet terminé.
Liez les données de contact à l'utilisateur
Utilisez l'ID utilisateur ASP.NET Identity pour vous assurer que les utilisateurs peuvent modifier leurs données, mais pas les données des autres utilisateurs. Ajoutez OwnerID
et ContactStatus
au Contact
modèle :
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
est l'ID de l'utilisateur de la table AspNetUser
de la base de données Identity. Le champ Status
détermine si un contact est visible par les utilisateurs généraux.
Créez une nouvelle migration et mettez à jour la base de données :
dotnet ef migrations add userID_Status
dotnet ef database update
Ajouter des services de rôle à Identity
Ajouter AddRoles pour ajouter des services de rôle :
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>();
Exiger des utilisateurs authentifiés
Définissez la règle d'autorisation de secours pour exiger que les utilisateurs soient authentifiés :
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();
});
Le code en surbrillance précédent définit la stratégie d'autorisation de secours. La stratégie d'autorisation de secours exige que tous les utilisateurs soient authentifiés, à l'exception des Pages Razor, des contrôleurs ou des méthodes d'action avec un attribut d'autorisation. Par exemple, Razor Pages, les contrôleurs ou les méthodes d’action avec [AllowAnonymous]
ou [Authorize(PolicyName="MyPolicy")]
utilisent l’attribut d’autorisation appliqué plutôt que la stratégie d’autorisation de secours.
RequireAuthenticatedUser ajoute DenyAnonymousAuthorizationRequirement à l’instance actuelle, ce qui impose l’authentification de l’utilisateur actuel.
La stratégie d'autorisation de secours :
- S'applique à toutes les requêtes qui ne spécifient pas explicitement une stratégie d'autorisation. Pour les demandes servies par routage de point de terminaison, cela inclut tout point de terminaison qui ne spécifie pas d'attribut d'autorisation. Pour les requêtes servies par d'autres middleware après le middleware d'autorisation, comme les fichiers statiques, cela applique la stratégie à toutes les requêtes.
La définition de la stratégie d'autorisation de secours pour exiger que les utilisateurs soient authentifiés protège les Pages Razor et les contrôleurs nouvellement ajoutés. Avoir l'autorisation requise par défaut est plus sûr que de compter sur de nouveaux contrôleurs et Pages Razor pour inclure l'attribut [Authorize]
.
La classe AuthorizationOptions contient également AuthorizationOptions.DefaultPolicy. La DefaultPolicy
est la politique utilisée avec l'attribut [Authorize]
lorsqu'aucune politique n'est spécifiée. [Authorize]
ne contient pas de stratégie nommée, contrairement à [Authorize(PolicyName="MyPolicy")]
.
Pour plus d'informations sur les stratégies, consultez Autorisation basée sur une stratégie dans ASP.NET Core.
Une autre façon pour les contrôleurs MVC et Pages Razor d'exiger que tous les utilisateurs soient authentifiés consiste à ajouter un filtre d'autorisation :
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();
Le code précédent utilise un filtre d'autorisation, la définition de la stratégie de secours utilise le routage du point de terminaison. La définition de la stratégie de secours est la méthode privilégiée pour exiger que tous les utilisateurs soient authentifiés.
Ajoutez AllowAnonymous aux Pages Index
et Privacy
afin que les utilisateurs anonymes puissent obtenir des informations sur le site avant de s'inscrire :
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()
{
}
}
Configurer le compte de test
La classe SeedData
crée deux comptes : administrateur et gestionnaire. Utilisez l'outil Secret Manager pour définir un mot de passe pour ces comptes. Définissez le mot de passe à partir du répertoire du projet (le répertoire contenant Program.cs
) :
dotnet user-secrets set SeedUserPW <PW>
Si un mot de passe faible est pas spécifié, une exception est levée lorsque SeedData.Initialize
est appelé.
Mettez à jour l'application pour utiliser le mot de passe de 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);
}
Créer les comptes de test et mettre à jour les contacts
Mettez à jour la méthode Initialize
dans la classe SeedData
pour créer les comptes de 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;
}
Ajoutez l'ID utilisateur de l'administrateur et ContactStatus
aux contacts. Faites un des contacts "Soumis" et un "Rejeté". Ajoutez l'ID utilisateur et le statut à tous les contacts. Un seul contact est affiché :
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
},
Créer des gestionnaires d'autorisation de propriétaire, de gestionnaire et d'administrateur
Créez une classe ContactIsOwnerAuthorizationHandler
dans le dossier Authorization . Le ContactIsOwnerAuthorizationHandler
vérifie que l'utilisateur agissant sur une ressource est propriétaire de la ressource.
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;
}
}
}
Le contexte ContactIsOwnerAuthorizationHandler
des appels.Réussit si l'utilisateur actuellement authentifié est le propriétaire du contact. Gestionnaires d'autorisations en général :
- Appelez
context.Succeed
lorsque les conditions sont remplies. - Revenir
Task.CompletedTask
lorsque les conditions ne sont pas remplies. Le retourTask.CompletedTask
sans appel préalable àcontext.Success
oucontext.Fail
n'est pas un succès ou un échec, il permet à d'autres gestionnaires d'autorisation de s'exécuter.
Si vous devez explicitement échouer, appelez context.Fail.
L'application permet aux propriétaires de contacts de modifier/supprimer/créer leurs propres données. ContactIsOwnerAuthorizationHandler
n'a pas besoin de vérifier l'opération passée dans le paramètre requirements.
Créer un gestionnaire d'autorisation de gestionnaire
Créez une classe ContactManagerAuthorizationHandler
dans le dossier Authorization. Le ContactManagerAuthorizationHandler
vérifie que l'utilisateur agissant sur la ressource est un gestionnaire. Seuls les gestionnaires peuvent approuver ou rejeter les modifications de contenu (nouvelles ou modifiées).
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;
}
}
}
Créer un gestionnaire d'autorisation d'administrateur
Créez une classe ContactAdministratorsAuthorizationHandler
dans le dossier Authorization. Le ContactAdministratorsAuthorizationHandler
vérifie que l'utilisateur agissant sur la ressource est un administrateur. L'administrateur peut effectuer toutes les opérations.
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;
}
}
}
Enregistrer les gestionnaires d'autorisation
Les services utilisant Entity Framework Core doivent être enregistrés pour l'injection de dépendances à l'aide de AddScoped. Le ContactIsOwnerAuthorizationHandler
utilise ASP.NET Core Identity, qui est basé sur Entity Framework Core. Enregistrez les gestionnaires avec la collection de services afin qu'ils soient disponibles ContactsController
via l'injection de dépendances. Ajoutez le code suivant à la fin de 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
et ContactManagerAuthorizationHandler
sont ajoutés en tant que singletons. Ce sont des singletons car ils n'utilisent pas EF et toutes les informations nécessaires se trouvent dans le paramètre Context
de la méthode HandleRequirementAsync
.
Autorisation de soutien
Dans cette section, vous mettez à jour les Pages Razor et ajoutez une classe d'exigences d'opérations.
Examiner la classe d'exigences des opérations de contact
Passez en revue la classe ContactOperations
. Cette classe contient les exigences prises en charge par l'application :
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";
}
}
Créer une classe de base pour les pages Contacts Razor
Créez une classe de base qui contient les services utilisés dans les pages de contacts Razor. La classe de base place le code d'initialisation à un emplacement :
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;
}
}
}
Le code précédent :
- Ajoute le service
IAuthorizationService
pour accéder aux gestionnaires d'autorisation. - Ajoute le Identity
UserManager
service IdentityUserManager
. - Ajoutez la
ApplicationDbContext
.
Mettre à jour CreateModel
Mettez à jour le modèle de page de création :
- Constructeur pour utiliser la classe de base
DI_BasePageModel
. OnPostAsync
méthode pour :- Ajoutez l'ID utilisateur au modèle
Contact
. - Appelez le gestionnaire d'autorisation pour vérifier que l'utilisateur est autorisé à créer des contacts.
- Ajoutez l'ID utilisateur au modèle
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");
}
}
}
Mettre à jour le modèle d'index
Mettez à jour la méthode OnGetAsync
afin que seuls les contacts approuvés soient affichés pour les utilisateurs généraux :
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();
}
}
Mettre à jour le EditModel
Ajoutez un gestionnaire d'autorisation pour vérifier que l'utilisateur est propriétaire du contact. Étant donné que l'autorisation de ressource est en cours de validation, l'attribut [Authorize]
n'est pas suffisant. L'application n'a pas accès à la ressource lorsque les attributs sont évalués. L'autorisation basée sur les ressources doit être impérative. Les vérifications doivent être effectuées une fois que l'application a accès à la ressource, soit en la chargeant dans le modèle de page, soit en la chargeant dans le gestionnaire lui-même. Vous accédez fréquemment à la ressource en transmettant la clé de ressource.
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");
}
}
Mettre à jour le DeleteModel
Mettez à jour le modèle de page de suppression pour utiliser le gestionnaire d'autorisation afin de vérifier que l'utilisateur dispose d'une autorisation de suppression sur le contact.
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");
}
}
Injecter le service d'autorisation dans les vues
Actuellement, l'interface utilisateur affiche les liens de modification et de suppression pour les contacts que l'utilisateur ne peut pas modifier.
Injectez le service d'autorisation dans le fichier Pages/_ViewImports.cshtml
afin qu'il soit disponible pour toutes les vues :
@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
Le balisage précédent ajoute plusieurs instructions using
.
Mettez à jour les liens Modifier et Supprimer dans Pages/Contacts/Index.cshtml
afin qu'ils ne soient affichés que pour les utilisateurs disposant des autorisations appropriées :
@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>
Avertissement
Masquer les liens des utilisateurs qui n'ont pas l'autorisation de modifier les données ne sécurise pas l'application. Le masquage des liens rend l'application plus conviviale en affichant uniquement les liens valides. Les utilisateurs peuvent pirater les URL générées pour invoquer des opérations de modification et de suppression sur des données qu'ils ne possèdent pas. La Page Razor ou le contrôleur doit appliquer des contrôles d'accès pour sécuriser les données.
Détails de la mise à jour
Mettez à jour la vue des détails afin que les responsables puissent approuver ou rejeter des contacts :
@*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>
Mettre à jour le modèle de page de détails
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");
}
}
Ajouter ou supprimer un utilisateur à un rôle
Consultez ce numéro pour plus d'informations sur :
- Suppression des privilèges d'un utilisateur. Par exemple, désactiver un utilisateur dans une application de chat.
- Ajout de privilèges à un utilisateur.
Différences entre défi et interdiction
Cette application définit la politique par défaut pour exiger des utilisateurs authentifiés. Le code suivant autorise les utilisateurs anonymes. Les utilisateurs anonymes sont autorisés à montrer les différences entre Challenge vs Forbid.
[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
public Details2Model(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact? _contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);
if (_contact == null)
{
return NotFound();
}
Contact = _contact;
if (!User.Identity!.IsAuthenticated)
{
return Challenge();
}
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return Forbid();
}
return Page();
}
}
Dans le code précédent :
- Lorsque l'utilisateur n'est pas authentifié, un
ChallengeResult
est renvoyé. Lorsqu'unChallengeResult
est renvoyé, l'utilisateur est redirigé vers la page de connexion. - Lorsque l'utilisateur est authentifié, mais non autorisé, un
ForbidResult
est renvoyé. Lorsqu'unForbidResult
est renvoyé, l'utilisateur est redirigé vers la page d'accès refusé.
Tester l’application terminée
Avertissement
Cet article utilise l’outil Secret Manager pour stocker le mot de passe des comptes utilisateur prédéfinis. L’outil Secret Manager est utilisé pour stocker les données sensibles pendant le développement local. Pour plus d’informations sur les procédures d’authentification qui peuvent être utilisées lorsqu’une application est déployée dans un environnement de test ou de production, consultez Flux d’authentification sécurisés.
Si vous n'avez pas encore défini de mot de passe pour les comptes utilisateur prédéfinis, utilisez l'outil Secret Manager pour définir un mot de passe :
Choisissez un mot de passe fort :
- Au moins 12 caractères, 14 ou plus de préférence
- Combinaison de lettres majuscules, de lettres minuscules, de chiffres et de symboles
- Pas un mot qui se trouve dans un dictionnaire ou le nom d’une personne, d’un personnage, d’un produit ou d’une organisation
- Sensiblement différent de vos mots de passe précédents
- Facile à mémoriser pour vous, mais difficile à deviner pour les autres, par exemple une expression mémorable telle que « 6MonkeysRLooking^ ».
Exécutez la commande suivante à partir du dossier du projet, où
<PW>
est le mot de passe :dotnet user-secrets set SeedUserPW <PW>
Si l'application a des contacts :
- Supprimer tous les enregistrements de la table
Contact
. - Redémarrez l'application pour amorcer la base de données.
Un moyen simple de tester l'application terminée consiste à lancer trois navigateurs différents (ou des sessions incognito/InPrivate). Dans un navigateur, enregistrez un nouvel utilisateur (par exemple, test@example.com
). Connectez-vous à chaque navigateur avec un utilisateur différent. Vérifiez les opérations suivantes :
- Les utilisateurs enregistrés peuvent voir toutes les données de contact approuvées.
- Les utilisateurs enregistrés peuvent modifier/supprimer leurs propres données.
- Les responsables peuvent approuver/rejeter les données de contact. La vue
Details
affiche les boutons Approuver et Rejeter. - Les administrateurs peuvent approuver/rejeter et modifier/supprimer toutes les données.
Utilisateur | Approuver ou rejeter des contacts | Options |
---|---|---|
test@example.com | Non | Modifier et supprimer leurs données. |
manager@contoso.com | Oui | Modifier et supprimer leurs données. |
admin@contoso.com | Oui | Modifier et supprimer toutes les données. |
Créez un contact dans le navigateur de l'administrateur. Copiez l'URL à supprimer et à modifier à partir du contact de l'administrateur. Collez ces liens dans le navigateur de l'utilisateur test pour vérifier que l'utilisateur test ne peut pas effectuer ces opérations.
Créer l'application de démarrage
Créer une application Pages Razor nommée "ContactManager"
- Créez l'application avec des comptes d'utilisateurs individuels.
- Nommez-le "ContactManager" afin que l'espace de noms corresponde à l'espace de noms utilisé dans l'exemple.
-uld
spécifie Base de données locale au lieu de SQLite
dotnet new webapp -o ContactManager -au Individual -uld
Ajouter
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; } } }
Échafaudez le modèle
Contact
.Créez la migration initiale et mettez à jour la base de données :
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
Notes
Par défaut, l’architecture des fichiers binaires .NET à installer représente l’architecture du système d’exploitation en cours d’exécution. Pour spécifier une architecture de système d’exploitation différente, consultez dotnet tool install, --arch option. Pour plus d'informations, consultez le problème GitHub dotnet/AspNetCore.Docs #29262.
Mettez à jour l'ancre ContactManager dans le fichier
Pages/Shared/_Layout.cshtml
:<a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
Testez l'application en créant, modifiant et supprimant un contact
Amorcer la base de données
Ajoutez la classe SeedData au dossier 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();
}
}
}
Appelez SeedData.Initialize
à partir de 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();
Vérifiez que l'application a amorcé la base de données. S'il y a des lignes dans la base de données de contact, la méthode seed ne s'exécute pas.
Ce didacticiel montre comment créer une application Web ASP.NET Core avec des données utilisateur protégées par autorisation. Il affiche une liste de contacts créés par des utilisateurs authentifiés (enregistrés). Il existe trois groupes de sécurité :
- Les utilisateurs enregistrés peuvent voir toutes les données approuvées et peuvent modifier/supprimer leurs propres données.
- Les responsables peuvent approuver ou rejeter les données de contact. Seuls les contacts approuvés sont visibles pour les utilisateurs.
- Les administrateurs peuvent approuver/rejeter et modifier/supprimer toutes les données.
Les images de ce document ne correspondent pas exactement aux derniers modèles.
Dans l'image suivante, l'utilisateur Rick (rick@example.com
) est connecté. Rick ne peut afficher que les contacts approuvés et Modifier/Supprimer/Créer de nouveaux liens pour ses contacts. Seul le dernier enregistrement, créé par Rick, affiche les liens Modifier et Supprimer. Les autres utilisateurs ne verront pas le dernier enregistrement tant qu'un responsable ou un administrateur n'aura pas modifié le statut en "Approuvé".
Dans l'image suivante, manager@contoso.com
est connecté et dans le rôle du gestionnaire :
L'image suivante montre la vue des détails du responsable d'un contact :
Les boutons Approuver et Rejeter ne sont affichés que pour les responsables et les administrateurs.
Dans l'image suivante, admin@contoso.com
est connecté et dans le rôle d'administrateur :
L'administrateur a tous les privilèges. Elle peut lire/modifier/supprimer n'importe quel contact et changer le statut des contacts.
L'application a été créée en échafaudant le modèle Contact
suivant :
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'exemple contient les gestionnaires d'autorisation suivants :
ContactIsOwnerAuthorizationHandler
: Garantit qu'un utilisateur ne peut modifier que ses données.ContactManagerAuthorizationHandler
: Permet aux responsables d'approuver ou de rejeter des contacts.ContactAdministratorsAuthorizationHandler
: Permet aux administrateurs de :- Approuver ou rejeter des contacts
- Modifier et supprimer des contacts
Prérequis
Ce tutoriel est avancé. Vous devez être familiarisé avec :
- ASP.NET Core
- Authentification
- Confirmation de compte et récupération de mot de passe
- Autorisation
- Entity Framework Core
L'application de démarrage et terminée
Téléchargez l'application terminée. Testez l'application terminée afin de vous familiariser avec ses fonctions de sécurité.
Application de démarrage
Téléchargez l'application de démarrage.
Exécutez l'application, appuyez sur le lien ContactManager et vérifiez que vous pouvez créer, modifier et supprimer un contact. Pour créer l'application de démarrage, voir Créer l'application de démarrage.
Sécuriser les données utilisateur
Les sections suivantes présentent toutes les étapes principales pour créer l'application de données utilisateur sécurisées. Vous trouverez peut-être utile de vous reporter au projet terminé.
Liez les données de contact à l'utilisateur
Utilisez l'ID utilisateur ASP.NET Identity pour vous assurer que les utilisateurs peuvent modifier leurs données, mais pas les données des autres utilisateurs. Ajoutez OwnerID
et ContactStatus
au Contact
modèle :
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
est l'ID de l'utilisateur de la table AspNetUser
de la base de données Identity. Le champ Status
détermine si un contact est visible par les utilisateurs généraux.
Créez une nouvelle migration et mettez à jour la base de données :
dotnet ef migrations add userID_Status
dotnet ef database update
Ajouter des services de rôle à Identity
Ajouter AddRoles pour ajouter des services de rôle :
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>();
Exiger des utilisateurs authentifiés
Définissez la stratégie d'authentification de secours pour exiger que les utilisateurs soient authentifiés :
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();
});
Le code en surbrillance précédent définit la stratégie d'authentification de secours. La stratégie d'authentification de secours exige que tous les utilisateurs soient authentifiés, à l'exception des Pages Razor, des contrôleurs ou des méthodes d'action avec un attribut d'authentification. Par exemple, les Pages Razor, les contrôleurs ou les méthodes d'action avec [AllowAnonymous]
ou [Authorize(PolicyName="MyPolicy")]
utilisent l'attribut d'authentification appliqué plutôt que la stratégie d'authentification de secours.
RequireAuthenticatedUser ajoute DenyAnonymousAuthorizationRequirement à l’instance actuelle, ce qui impose l’authentification de l’utilisateur actuel.
La stratégie d'authentification de secours :
- S'applique à toutes les requêtes qui ne spécifient pas explicitement une stratégie d'authentification. Pour les demandes servies par routage de point de terminaison, cela inclurait tout point de terminaison qui ne spécifie pas d'attribut d'autorisation. Pour les requêtes servies par d'autres intergiciels après l'intergiciel d'autorisation, comme les fichiers statiques, cela appliquerait la stratégie à toutes les requêtes.
La définition de la stratégie d'authentification de secours pour exiger que les utilisateurs soient authentifiés protège Pages Razor et les contrôleurs nouvellement ajoutés. Avoir l'authentification requise par défaut est plus sûr que de s'appuyer sur de nouveaux contrôleurs et Pages Razor pour inclure l'attribut [Authorize]
.
La classe AuthorizationOptions contient également AuthorizationOptions.DefaultPolicy. La DefaultPolicy
est la politique utilisée avec l'attribut [Authorize]
lorsqu'aucune politique n'est spécifiée. [Authorize]
ne contient pas de stratégie nommée, contrairement à [Authorize(PolicyName="MyPolicy")]
.
Pour plus d'informations sur les stratégies, consultez Autorisation basée sur une stratégie dans ASP.NET Core.
Une autre façon pour les contrôleurs MVC et Pages Razor d'exiger que tous les utilisateurs soient authentifiés consiste à ajouter un filtre d'autorisation :
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));
});
Le code précédent utilise un filtre d'autorisation, la définition de la stratégie de secours utilise le routage du point de terminaison. La définition de la stratégie de secours est la méthode privilégiée pour exiger que tous les utilisateurs soient authentifiés.
Ajoutez AllowAnonymous aux Pages Index
et Privacy
afin que les utilisateurs anonymes puissent obtenir des informations sur le site avant de s'inscrire :
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()
{
}
}
}
Configurer le compte de test
La classe SeedData
crée deux comptes : administrateur et gestionnaire. Utilisez l'outil Secret Manager pour définir un mot de passe pour ces comptes. Définissez le mot de passe à partir du répertoire du projet (le répertoire contenant Program.cs
) :
dotnet user-secrets set SeedUserPW <PW>
Si un mot de passe fort n'est pas spécifié, une exception est levée lors SeedData.Initialize
est appelé.
Mise à jour Main
pour utiliser le mot de passe de 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>();
});
}
Créer les comptes de test et mettre à jour les contacts
Mettez à jour la méthode Initialize
dans la classe SeedData
pour créer les comptes de 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;
}
Ajoutez l'ID utilisateur de l'administrateur et ContactStatus
aux contacts. Faites un des contacts "Soumis" et un "Rejeté". Ajoutez l'ID utilisateur et le statut à tous les contacts. Un seul contact est affiché :
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
},
Créer des gestionnaires d'autorisation de propriétaire, de gestionnaire et d'administrateur
Créez une classe ContactIsOwnerAuthorizationHandler
dans le dossier Authorization . Le ContactIsOwnerAuthorizationHandler
vérifie que l'utilisateur agissant sur une ressource est propriétaire de la ressource.
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;
}
}
}
Le contexte ContactIsOwnerAuthorizationHandler
des appels.Réussit si l'utilisateur actuellement authentifié est le propriétaire du contact. Gestionnaires d'autorisations en général :
- Appelez
context.Succeed
lorsque les conditions sont remplies. - Revenir
Task.CompletedTask
lorsque les conditions ne sont pas remplies. Le retourTask.CompletedTask
sans appel préalable àcontext.Success
oucontext.Fail
n'est pas un succès ou un échec, il permet à d'autres gestionnaires d'autorisation de s'exécuter.
Si vous devez explicitement échouer, appelez context.Fail.
L'application permet aux propriétaires de contacts de modifier/supprimer/créer leurs propres données. ContactIsOwnerAuthorizationHandler
n'a pas besoin de vérifier l'opération passée dans le paramètre requirements.
Créer un gestionnaire d'autorisation de gestionnaire
Créez une classe ContactManagerAuthorizationHandler
dans le dossier Authorization. Le ContactManagerAuthorizationHandler
vérifie que l'utilisateur agissant sur la ressource est un gestionnaire. Seuls les gestionnaires peuvent approuver ou rejeter les modifications de contenu (nouvelles ou modifiées).
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;
}
}
}
Créer un gestionnaire d'autorisation d'administrateur
Créez une classe ContactAdministratorsAuthorizationHandler
dans le dossier Authorization. Le ContactAdministratorsAuthorizationHandler
vérifie que l'utilisateur agissant sur la ressource est un administrateur. L'administrateur peut effectuer toutes les opérations.
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;
}
}
}
Enregistrer les gestionnaires d'autorisation
Les services utilisant Entity Framework Core doivent être enregistrés pour l'injection de dépendances à l'aide de AddScoped. Le ContactIsOwnerAuthorizationHandler
utilise ASP.NET Core Identity, qui est basé sur Entity Framework Core. Enregistrez les gestionnaires avec la collection de services afin qu'ils soient disponibles ContactsController
via l'injection de dépendances. Ajoutez le code suivant à la fin de 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
et ContactManagerAuthorizationHandler
sont ajoutés en tant que singletons. Ce sont des singletons car ils n'utilisent pas EF et toutes les informations nécessaires se trouvent dans le paramètre Context
de la méthode HandleRequirementAsync
.
Autorisation de soutien
Dans cette section, vous mettez à jour les Pages Razor et ajoutez une classe d'exigences d'opérations.
Examiner la classe d'exigences des opérations de contact
Passez en revue la classe ContactOperations
. Cette classe contient les exigences prises en charge par l'application :
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";
}
}
Créer une classe de base pour les pages Contacts Razor
Créez une classe de base qui contient les services utilisés dans les pages de contacts Razor. La classe de base place le code d'initialisation à un emplacement :
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;
}
}
}
Le code précédent :
- Ajoute le service
IAuthorizationService
pour accéder aux gestionnaires d'autorisation. - Ajoute le Identity
UserManager
service IdentityUserManager
. - Ajoutez la
ApplicationDbContext
.
Mettre à jour CreateModel
Mettez à jour le constructeur de modèle de création de page pour utiliser la classe De base DI_BasePageModel
:
public class CreateModel : DI_BasePageModel
{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
Mettez à jour la méthode CreateModel.OnPostAsync
pour :
- Ajoutez l'ID utilisateur au modèle
Contact
. - Appelez le gestionnaire d'autorisation pour vérifier que l'utilisateur est autorisé à créer des contacts.
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");
}
Mettre à jour le modèle d'index
Mettez à jour la méthode OnGetAsync
afin que seuls les contacts approuvés soient affichés pour les utilisateurs généraux :
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();
}
}
Mettre à jour le EditModel
Ajoutez un gestionnaire d'autorisation pour vérifier que l'utilisateur est propriétaire du contact. Étant donné que l'autorisation de ressource est en cours de validation, l'attribut [Authorize]
n'est pas suffisant. L'application n'a pas accès à la ressource lorsque les attributs sont évalués. L'autorisation basée sur les ressources doit être impérative. Les vérifications doivent être effectuées une fois que l'application a accès à la ressource, soit en la chargeant dans le modèle de page, soit en la chargeant dans le gestionnaire lui-même. Vous accédez fréquemment à la ressource en transmettant la clé de ressource.
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");
}
}
Mettre à jour le DeleteModel
Mettez à jour le modèle de page de suppression pour utiliser le gestionnaire d'autorisation afin de vérifier que l'utilisateur dispose d'une autorisation de suppression sur le contact.
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");
}
}
Injecter le service d'autorisation dans les vues
Actuellement, l'interface utilisateur affiche les liens de modification et de suppression pour les contacts que l'utilisateur ne peut pas modifier.
Injectez le service d'autorisation dans le fichier Pages/_ViewImports.cshtml
afin qu'il soit disponible pour toutes les vues :
@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
Le balisage précédent ajoute plusieurs instructions using
.
Mettez à jour les liens Modifier et Supprimer dans Pages/Contacts/Index.cshtml
afin qu'ils ne soient affichés que pour les utilisateurs disposant des autorisations appropriées :
@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>
Avertissement
Masquer les liens des utilisateurs qui n'ont pas l'autorisation de modifier les données ne sécurise pas l'application. Le masquage des liens rend l'application plus conviviale en affichant uniquement les liens valides. Les utilisateurs peuvent pirater les URL générées pour invoquer des opérations de modification et de suppression sur des données qu'ils ne possèdent pas. La Page Razor ou le contrôleur doit appliquer des contrôles d'accès pour sécuriser les données.
Détails de la mise à jour
Mettez à jour la vue des détails afin que les responsables puissent approuver ou rejeter des contacts :
@*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>
Mettez à jour le modèle de page de détails :
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");
}
}
Ajouter ou supprimer un utilisateur à un rôle
Consultez ce numéro pour plus d'informations sur :
- Suppression des privilèges d'un utilisateur. Par exemple, désactiver un utilisateur dans une application de chat.
- Ajout de privilèges à un utilisateur.
Différences entre défi et interdiction
Cette application définit la politique par défaut pour exiger des utilisateurs authentifiés. Le code suivant autorise les utilisateurs anonymes. Les utilisateurs anonymes sont autorisés à montrer les différences entre Challenge vs Forbid.
[AllowAnonymous]
public class Details2Model : DI_BasePageModel
{
public Details2Model(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
public Contact Contact { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Contact = await Context.Contact.FirstOrDefaultAsync(m => m.ContactId == id);
if (Contact == null)
{
return NotFound();
}
if (!User.Identity.IsAuthenticated)
{
return Challenge();
}
var isAuthorized = User.IsInRole(Constants.ContactManagersRole) ||
User.IsInRole(Constants.ContactAdministratorsRole);
var currentUserId = UserManager.GetUserId(User);
if (!isAuthorized
&& currentUserId != Contact.OwnerID
&& Contact.Status != ContactStatus.Approved)
{
return Forbid();
}
return Page();
}
}
Dans le code précédent :
- Lorsque l'utilisateur n'est pas authentifié, un
ChallengeResult
est renvoyé. Lorsqu'unChallengeResult
est renvoyé, l'utilisateur est redirigé vers la page de connexion. - Lorsque l'utilisateur est authentifié, mais non autorisé, un
ForbidResult
est renvoyé. Lorsqu'unForbidResult
est renvoyé, l'utilisateur est redirigé vers la page d'accès refusé.
Tester l’application terminée
Si vous n'avez pas encore défini de mot de passe pour les comptes utilisateur prédéfinis, utilisez l'outil Secret Manager pour définir un mot de passe :
Choisissez un mot de passe fort : utilisez huit caractères ou plus et au moins une majuscule, un chiffre et un symbole. Par exemple,
Passw0rd!
répond aux exigences de mot de passe fort.Exécutez la commande suivante à partir du dossier du projet, où
<PW>
est le mot de passe :dotnet user-secrets set SeedUserPW <PW>
Si l'application a des contacts :
- Supprimer tous les enregistrements de la table
Contact
. - Redémarrez l'application pour amorcer la base de données.
Un moyen simple de tester l'application terminée consiste à lancer trois navigateurs différents (ou des sessions incognito/InPrivate). Dans un navigateur, enregistrez un nouvel utilisateur (par exemple, test@example.com
). Connectez-vous à chaque navigateur avec un utilisateur différent. Vérifiez les opérations suivantes :
- Les utilisateurs enregistrés peuvent voir toutes les données de contact approuvées.
- Les utilisateurs enregistrés peuvent modifier/supprimer leurs propres données.
- Les responsables peuvent approuver/rejeter les données de contact. La vue
Details
affiche les boutons Approuver et Rejeter. - Les administrateurs peuvent approuver/rejeter et modifier/supprimer toutes les données.
Utilisateur | Ensemencé par l'application | Options |
---|---|---|
test@example.com | Non | Modifier/supprimer les propres données. |
manager@contoso.com | Oui | Approuver/rejeter et modifier/supprimer ses propres données. |
admin@contoso.com | Oui | Approuver/rejeter et modifier/supprimer toutes les données. |
Créez un contact dans le navigateur de l'administrateur. Copiez l'URL à supprimer et à modifier à partir du contact de l'administrateur. Collez ces liens dans le navigateur de l'utilisateur test pour vérifier que l'utilisateur test ne peut pas effectuer ces opérations.
Créer l'application de démarrage
Créer une application Pages Razor nommée "ContactManager"
- Créez l'application avec des comptes d'utilisateurs individuels.
- Nommez-le "ContactManager" afin que l'espace de noms corresponde à l'espace de noms utilisé dans l'exemple.
-uld
spécifie Base de données locale au lieu de SQLite
dotnet new webapp -o ContactManager -au Individual -uld
Ajoutez
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; } }
Échafaudez le modèle
Contact
.Créez la migration initiale et mettez à jour la base de données :
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
Notes
Par défaut, l’architecture des fichiers binaires .NET à installer représente l’architecture du système d’exploitation en cours d’exécution. Pour spécifier une architecture de système d’exploitation différente, consultez dotnet tool install, --arch option. Pour plus d'informations, consultez le problème GitHub dotnet/AspNetCore.Docs #29262.
Si vous rencontrez un bogue avec la commande dotnet aspnet-codegenerator razorpage
, consultez ce problème GitHub.
- Mettez à jour l'ancre ContactManager dans le fichier
Pages/Shared/_Layout.cshtml
:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
- Testez l'application en créant, modifiant et supprimant un contact
Amorcer la base de données
Ajoutez la classe SeedData au dossier 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();
}
}
}
Appelez SeedData.Initialize
à partir de 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>();
});
}
}
Vérifiez que l'application a amorcé la base de données. S'il y a des lignes dans la base de données de contact, la méthode seed ne s'exécute pas.
Ressources supplémentaires
- Tutoriel : Générer une application ASP.NET Core et Azure SQL Database dans Azure App Service
- Laboratoire d'autorisation ASP.NET Core. Cet atelier aborde plus en détail les fonctionnalités de sécurité introduites dans ce didacticiel.
- Introduction à l'autorisation dans ASP.NET Core
- Autorisation basée sur une stratégie personnalisée