Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Par Rick Anderson et Joe Audette
Ce tutoriel montre comment créer une application web ASP.NET Core avec des données utilisateur protégées par autorisation. Il affiche la liste des contacts créés par les utilisateurs authentifiés (inscrits). L’application prend en charge trois groupes de sécurité :
- Les utilisateurs inscrits peuvent afficher 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 marqués comme approuvés sont visibles par les utilisateurs.
- Les administrateurs peuvent approuver/rejeter et modifier/supprimer toutes les données.
Note
Les images de cet article ne correspondent pas exactement aux derniers modèles.
Dans l’image suivante, l’utilisateur rick@contoso.com est connecté à l’application web. Cet utilisateur peut afficher uniquement les contacts approuvés, ainsi que les liens Modifier/Supprimer/Créer pour ces contacts. Dans cette vue, seul le dernier enregistrement (créé par cet utilisateur) affiche les liens Modifier et Supprimer . Les autres utilisateurs ne voient pas le dernier enregistrement tant qu’un responsable ou un administrateur n’approuve pas l’enregistrement.
Dans l’image suivante, l’utilisateur manager@contoso.com est connecté et a accès aux fonctionnalités de gestion :
Un responsable peut sélectionner un contact pour afficher les détails de l’utilisateur, comme illustré dans l’image suivante :
Les options Approuver et Rejeter s’affichent uniquement pour les responsables et les administrateurs.
Dans l’image suivante, l’utilisateur admin@contoso.com est connecté et a accès aux fonctionnalités d’administration :
Un administrateur dispose de tous les privilèges. Ils peuvent lire, modifier ou supprimer n’importe quel contact et modifier l’état 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.
Prerequisites
Ce tutoriel est avancé. Vous devez être familiarisé avec :
- ASP.NET Core
- Authentication et ASP.NET Core Identity
- Confirmation du compte et récupération du mot de passe
- Autorisation
- Entity Framework Core
L'application initiale et complète
Téléchargez l’application complète. Testez l'application terminée afin de vous familiariser avec ses fonctions de sécurité.
Tip
Vous pouvez utiliser la commande git sparse-checkout pour télécharger uniquement le sous-dossier sample.
Par exemple:
git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse
cd AspNetCore.Docs
git sparse-checkout init --cone
git sparse-checkout set aspnetcore/security/authorization/secure-data/samples
Application de démarrage
Download l’application starter.
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 illustrent toutes les étapes principales pour créer l’application de données utilisateur sécurisée. Vous trouverez peut-être utile de faire référence 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 d’autres données. Ajoutez les champs OwnerID et ContactStatus au modèle Contact :
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
Activez l’application pour utiliser les services de rôle en ajoutant la AddRoles méthode :
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();
});
L'extrait de code mis en évidence ci-dessus définit la stratégie d'autorisation de repli. La stratégie d’autorisation de repli exige que tous les utilisateurs soient authentifiés, sauf pour les Razor Pages, les contrôleurs ou les méthodes d’action dotés d’un attribut d’autorisation. Par exemple, les pages Razor, 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 repli.
La RequireAuthenticatedUser méthode ajoute la DenyAnonymousAuthorizationRequirement classe à l’instance actuelle, qui applique l’authentification de l’utilisateur actuel.
La stratégie d’autorisation par défaut s’applique à toutes les requêtes qui ne spécifient pas explicitement de stratégie d’autorisation. Pour les demandes traitées par le routage de point de terminaison, la stratégie s’applique à tout point de terminaison qui ne spécifie pas d’attribut d’autorisation. Pour les demandes traitées par d’autres intergiciels après l’intergiciel d’autorisation, telles que les fichiers statiques, la stratégie s’applique à toutes les requêtes.
Définir la stratégie d'autorisation de secours afin d'exiger que les utilisateurs soient authentifiés protège les nouvelles Pages Razor ainsi que les nouveaux contrôleurs. Exiger une autorisation par défaut est plus sûr que de se fier aux nouveaux contrôleurs et aux Razor Pages pour inclure l’attribut [Authorize].
La AuthorizationOptions classe contient également la AuthorizationOptions.DefaultPolicy propriété. 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 les stratégies dans ASP.NET Core.
En guise d’autre approche, les contrôleurs MVC et Razor les pages peuvent ajouter un filtre d’autorisation pour exiger que tous les utilisateurs soient authentifiés :
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 et la définition de la stratégie de secours repose sur le routage du point de terminaison. La définition de la politique de repli est la méthode privilégiée pour exiger l'authentification de tous les utilisateurs.
Ajoutez l’attribut AllowAnonymous aux Index pages 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 (répertoire contenant le fichier Program.cs ) :
dotnet user-secrets set SeedUserPW <PW>
Si un mot de passe faible est spécifié, une exception est levée lorsque la SeedData.Initialize méthode est appelée.
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
Créez les comptes de test en mettant à jour la Initialize méthode dans la SeedData classe :
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 d’utilisateur administrateur et le ContactStatus champ aux contacts. Marquez l’un des contacts comme Soumis et l’autre comme 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 ContactIsOwnerAuthorizationHandler appelle la méthode context.Succeed si l’utilisateur authentifié actuel est le propriétaire du contact.
En règle générale, le gestionnaire d’autorisation :
- Appelle la
context.Succeedméthode lorsque les exigences sont remplies. - Lorsque les exigences ne sont pas remplies, elle retourne
Task.CompletedTask. LorsqueTask.CompletedTaskest renvoyé sans appel préalable àcontext.Succeedou àcontext.Fail, le résultat n’est ni un succès ni un échec. Au lieu de cela, il permet à d’autres gestionnaires d’autorisation de s’exécuter.
Si vous devez signaler explicitement un échec, appelez la méthode 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 transmise dans le paramètre requirement.
Créer un gestionnaire d'autorisation de responsable
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 qui utilisent Entity Framework Core doivent être enregistrés pour l’injection de dépendances avec la méthode AddScoped. Le ContactIsOwnerAuthorizationHandler utilise ASP.NET Core Identity, qui est basé sur Entity Framework Core. Enregistrez les gestionnaires dans la collection de services afin qu'ils soient disponibles pour le 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. Ils sont singletons, car ils n’utilisent pas Entity Framework et toutes les informations nécessaires se situent dans le Context paramètre de la HandleRequirementAsync méthode.
Soutien à l’autorisation
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
IAuthorizationServicepour accéder aux gestionnaires d'autorisations. - Ajoute le service Identity
UserManager. - Ajoutez
ApplicationDbContext.
Mettre à jour CreateModel
Mettez à jour le modèle de page de création :
- Définissez le constructeur pour utiliser la
DI_BasePageModelclasse de base. - Configurez la
OnPostAsyncmé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 OnGetAsync méthode afin que seuls les contacts approuvés soient affichés aux utilisateurs inscrits standard :
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 validée, l’attribut [Authorize] n’est pas suffisant. L'application n'a pas access à 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 la 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 et vérifiez que l’utilisateur dispose de l’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 déclarations using.
Mettez à jour les liens Modifier et supprimer dans le fichier Pages/Contacts/Index.cshtml afin qu’ils soient affichés uniquement 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>
Warning
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 Razor Page ou le contrôleur doit appliquer des contrôles d'accès pour sécuriser les données.
Mettre à jour les détails
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 des rôles d’utilisateur
La mise à jour des attributions de rôles d’un utilisateur vous permet de contrôler les privilèges disponibles pour l’utilisateur. Supprimez un utilisateur d’un rôle et réduisez sa capacité à modifier des données, telles que la modification ou la suppression d’un contact. Ajoutez un utilisateur à un rôle et augmentez ses privilèges pour apporter des modifications globales. Vous pouvez également utiliser des affectations de rôles pour limiter la participation des utilisateurs, par exemple en mettant un utilisateur en sourdine dans une conversation de chat.
Pour plus d’informations, consultez GitHub problème dotnet/aspnetcore #8502 - Mute ou supprimer des privilèges d’un utilisateur. Modifications de l’administrateur.
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 à afficher les différences entre Défi et Interdiction.
[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
ChallengeResultest renvoyé. Lorsqu'unChallengeResultest renvoyé, l'utilisateur est redirigé vers la page de connexion. - Lorsque l'utilisateur est authentifié, mais non autorisé, un
ForbidResultest renvoyé. Lorsqu’unForbidResultest retourné, l’utilisateur est redirigé vers la page access refusée.
Tester l’application terminée
Warning
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 longs, mais 14 ou plus sont meilleurs.
- 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, Envisagez d’utiliser une expression mémorable comme
6MonkeysRLooking^.
Exécutez la commande suivante à partir du dossier du project, 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@contoso.com). Connectez-vous à chaque navigateur avec un utilisateur différent. Vérifiez les opérations suivantes :
- Les utilisateurs inscrits peuvent afficher 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
Detailsaffiche les boutons Approuver et Rejeter. - Les administrateurs peuvent approuver/rejeter et modifier/supprimer toutes les données.
| User | Approuver/rejeter les contacts | Options |
|---|---|---|
test@contoso.com |
No | Modifier et supprimer leurs données. |
manager@contoso.com |
Yes | Modifier et supprimer leurs données. |
admin@contoso.com |
Yes | 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 de test et vérifiez que l’utilisateur de test ne peut pas effectuer ces opérations.
Créer l'application de démarrage
Créer une application Razor Pages :
- Créez l’application avec des comptes individuels.
- Nommez l’application ContactManager. L’espace de noms correspond donc à l’espace de noms utilisé dans l’exemple.
- Utilisez l’indicateur
-uldpour spécifier LocalDB au lieu de SQLite.
dotnet new webapp -o ContactManager -au Individual -uldAjoutez le fichier Models/Contact.cs :
using System.ComponentModel.DataAnnotations; namespace ContactManager.Models { public class Contact { public int ContactId { get; set; } public string? Name { get; set; } public string? Address { get; set; } public string? City { get; set; } public string? State { get; set; } public string? Zip { get; set; } [DataType(DataType.EmailAddress)] public string? Email { get; set; } } }É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 updateNote
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 autre architecture, passez en revue comment utiliser la
dotnet tool installcommande avec l’option « --arch ». Pour plus d'informations, consultez GitHub dotnet/aspnetcore.docs issue #29262 - Add '-a arm64' sur Apple Silicon.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 la SeedData.Initialize méthode à partir du fichier 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();
Testez l’application et vérifiez que la base de données a bien été initialisée. S’il existe des enregistrements dans la base de données des contacts, la méthode d’initialisation n’est pas exécutée.
Ce tutoriel 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 ci-dessous affiche la vue des détails du gestionnaire 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
Prerequisites
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 initiale et complète
Téléchargez l’application complète. Testez l'application terminée afin de vous familiariser avec ses fonctions de sécurité.
Application de démarrage
Download l’application starter.
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 faire référence 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 d’autres données. 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
Ajoutez 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 par défaut exige que tous les utilisateurs soient authentifiés, à l’exception des Razor pages, contrôleurs ou méthodes d’action dotés d’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.
Politique d'authentification alternative :
- 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.
Le fait de configurer la stratégie d’authentification de secours pour exiger que les utilisateurs soient authentifiés protège les Pages Razor et les contrôleurs nouvellement ajoutés. Exiger l'authentification par défaut est plus sécurisé que de compter sur les nouveaux contrôleurs et les pages Razor pour qu'ils incluent 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 les stratégies 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 et la définition de la stratégie de secours repose sur le routage du point de terminaison. La définition de la politique de repli est la méthode privilégiée pour exiger l'authentification de tous les utilisateurs.
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 project (répertoire contenant Program.cs) :
dotnet user-secrets set SeedUserPW <PW>
Si un mot de passe fort n'est pas défini, une exception est levée lorsque 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 de l'un des contacts un "Soumis" et de l'autre 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 ContactIsOwnerAuthorizationHandler appelle context.Succeed si l'utilisateur actuellement authentifié est le propriétaire du contact. Gestionnaires d'autorisations en général :
- Appelez
context.Succeedlorsque les conditions sont remplies. - Renvoyer
Task.CompletedTasklorsque les conditions requises ne sont pas remplies. Le retourTask.CompletedTasksans appel préalable àcontext.Successoucontext.Failn'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 transmise dans le paramètre requirement.
Créer un gestionnaire d'autorisation de responsable
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 dans la collection de services afin qu'ils soient disponibles pour le 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.
Soutien à l’autorisation
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
IAuthorizationServicepour accéder aux gestionnaires d'autorisations. - Ajoute le service Identity
UserManager. - Ajoutez
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 access à 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 access à 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 la 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 déclarations 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>
Warning
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 Razor Page ou le contrôleur doit appliquer des contrôles d'accès pour sécuriser les données.
Mettre à jour les détails
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 this issue 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
ChallengeResultest renvoyé. Lorsqu'unChallengeResultest renvoyé, l'utilisateur est redirigé vers la page de connexion. - Lorsque l'utilisateur est authentifié, mais non autorisé, un
ForbidResultest renvoyé. Lorsqu’unForbidResultest retourné, l’utilisateur est redirigé vers la page access refusée.
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 project, 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
Detailsaffiche les boutons Approuver et Rejeter. - Les administrateurs peuvent approuver/rejeter et modifier/supprimer toutes les données.
| User | Ensemencé par l'application | Options |
|---|---|---|
| test@example.com | No | Modifier/supprimer les propres données. |
| manager@contoso.com | Yes | Approuver/rejeter et modifier/supprimer ses propres données. |
| admin@contoso.com | Yes | 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 individuels.
- Nommez-le "ContactManager" afin que l'espace de noms corresponde à l'espace de noms utilisé dans l'exemple.
-
-uldspécifie Base de données locale au lieu de SQLite
dotnet new webapp -o ContactManager -au Individual -uldAjouter
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
Note
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 autre architecture, passez en revue comment utiliser la dotnet tool install commande avec l’option « --arch ».
Pour plus d'informations, consultez GitHub dotnet/aspnetcore.docs issue #29262 - Add '-a arm64' sur Apple Silicon.
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>();
});
}
}
Testez que l'application a initialisé 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.
Contenu connexe
- Tutorial : Créer une application ASP.NET Core et Azure SQL Database dans Azure App Service
- ASP.NET Core Laboratoire d’autorisation (détails étendus sur les fonctionnalités de sécurité)
- Introduction à l’autorisation dans ASP.NET Core
- Autorisation basée sur une stratégie personnalisée