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é".

Screenshot showing Rick signed in

Dans l'image suivante, manager@contoso.com est connecté et dans le rôle du gestionnaire :

Screenshot showing manager@contoso.com signed in

L'image suivante montre la vue des détails du responsable d'un contact :

Manager's view of a 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 :

Screenshot showing admin@contoso.com signed in

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 :

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 fort n'est pas spécifié, une exception est levée lors 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 retour Task.CompletedTask sans appel préalable à context.Success ou context.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 IdentityUserManager 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.
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'un ChallengeResult est renvoyé, l'utilisateur est redirigé vers la page de connexion.
  • Lorsque l'utilisateur est authentifié, mais non autorisé, un ForbidResult est renvoyé. Lorsqu'un ForbidResult 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 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.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 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é".

Screenshot showing Rick signed in

Dans l'image suivante, manager@contoso.com est connecté et dans le rôle du gestionnaire :

Screenshot showing manager@contoso.com signed in

L'image suivante montre la vue des détails du responsable d'un contact :

Manager's view of a 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 :

Screenshot showing admin@contoso.com signed in

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 :

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 retour Task.CompletedTask sans appel préalable à context.Success ou context.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 IdentityUserManager 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'un ChallengeResult est renvoyé, l'utilisateur est redirigé vers la page de connexion.
  • Lorsque l'utilisateur est authentifié, mais non autorisé, un ForbidResult est renvoyé. Lorsqu'un ForbidResult 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