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 tutoriel montre comment créer une application web ASP.NET Core avec des données utilisateur protégées par autorisation. Il affiche la liste des contacts qui ont été authentifiés (inscrits) que les utilisateurs ont créés. Il existe trois groupes de sécurité :

  • Les utilisateurs inscrits peuvent afficher toutes les données approuvées et modifier/supprimer leurs propres données.
  • Les gestionnaires peuvent approuver ou rejeter les données de contact. Seuls les contacts approuvés sont visibles par 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 des/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é l’état sur « Approuvé ».

Screenshot showing Rick signed in

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

Screenshot showing manager@contoso.com signed in

L’image suivante montre l’affichage des détails des gestionnaires d’un contact :

Manager's view of a contact

Les boutons Approuver et Rejeter sont affichés uniquement pour les gestionnaires et les administrateurs.

Dans l’image suivante, admin@contoso.com est connecté et dans le rôle de l’administrateur :

Screenshot showing admin@contoso.com signed in

L’administrateur dispose de tous les privilèges. Elle peut lire/modifier/supprimer n’importe quel contact et modifier l’état des contacts.

L’application a été créée en créant la structure du modèle suivant Contact :

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

L’exemple contient les gestionnaires d’autorisation suivants :

  • ContactIsOwnerAuthorizationHandler: garantit qu’un utilisateur ne peut modifier ses données que.
  • ContactManagerAuthorizationHandler: permet aux gestionnaires 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 didacticiel est avancé. Vous devez être familiarisé avec les points suivants :

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 fonctionnalités 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, consultez Créer l’application de démarrage.

Sécuriser les données utilisateur

Les sections suivantes ont toutes les étapes principales pour créer l’application de données utilisateur sécurisée. Vous pouvez trouver utile de faire référence au projet terminé.

Lier les données de contact à l’utilisateur

Utilisez l’ID utilisateur ASP.NET Identity pour vous assurer que les utilisateurs peuvent modifier leurs données, mais pas d’autres données d’utilisateurs. Ajoutez OwnerID et ContactStatus à ce 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 à partir de la AspNetUser table de la Identity base de données. Le Status champ détermine si un contact est visible par les utilisateurs généraux.

Créez une migration et mettez à jour la base de données :

dotnet ef migrations add userID_Status
dotnet ef database update

Ajouter des services de rôle à Identity

Ajoutez AddRoles pour ajouter des services de rôle :

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 stratégie 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 mis en surbrillance précédent définit la stratégie d’autorisation de secours. La stratégie d’autorisation de secours nécessite que tous les utilisateurs soient authentifiés, à l’exception de Razor Pages, de contrôleurs ou de méthodes d’action avec un attribut d’autorisation. Par exemple, Razor pages, contrôleurs ou méthodes d’action avec [AllowAnonymous] ou [Authorize(PolicyName="MyPolicy")] utilisez l’attribut d’autorisation appliqué plutôt que la stratégie d’autorisation de secours.

RequireAuthenticatedUser ajoute DenyAnonymousAuthorizationRequirement à l’instance actuelle, qui applique que l’utilisateur actuel est authentifié.

Stratégie d’autorisation de secours :

  • Est appliqué à toutes les demandes qui ne spécifient pas explicitement une stratégie d’autorisation. Pour les demandes servies par le 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 intergiciels après le middleware d’autorisation, tels que les fichiers statiques, cela applique la stratégie à toutes les demandes.

La définition de la stratégie d’autorisation de secours pour exiger que les utilisateurs soient authentifiés protège les pages et les contrôleurs nouvellement ajoutés Razor . L’autorisation requise par défaut est plus sécurisée que de s’appuyer sur de nouveaux contrôleurs et Razor pages pour inclure l’attribut [Authorize] .

La AuthorizationOptions classe contient AuthorizationOptions.DefaultPolicyégalement . Il DefaultPolicy s’agit de la stratégie utilisée avec l’attribut [Authorize] lorsqu’aucune stratégie 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 l’autorisation basée sur les stratégies dans ASP.NET Core.

Une autre façon pour les contrôleurs et Razor pages MVC de demander à tous les utilisateurs d’être authentifiés est d’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 des points de terminaison. La définition de la stratégie de secours est la méthode préférée pour exiger que tous les utilisateurs soient authentifiés.

Ajoutez AllowAnonymous aux Index pages et Privacy pour 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 SeedData classe crée deux comptes : administrateur et gestionnaire. Utilisez l’outil Secret Manager pour définir un mot de passe pour ces comptes. Définissez le mot de passe à partir du répertoire du projet (répertoire contenant Program.cs) :

dotnet user-secrets set SeedUserPW <PW>

Si un mot de passe fort n’est pas spécifié, une exception est levée lorsqu’elle SeedData.Initialize est appelée.

Mettez à jour l’application pour utiliser le mot de passe de test :

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}

Créer les comptes de test et mettre à jour les contacts

Mettez à jour la Initialize méthode dans la SeedData classe 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 d’utilisateur administrateur et ContactStatus aux contacts. Faites l’un des contacts « Soumis » et un « Rejeté ». Ajoutez l’ID utilisateur et l’état à 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 ContactIsOwnerAuthorizationHandler classe dans le dossier Authorization . Il ContactIsOwnerAuthorizationHandler vérifie que l’utilisateur agissant sur une ressource possè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;
        }
    }
}

Contexte d’appel ContactIsOwnerAuthorizationHandler. Réussissez si l’utilisateur authentifié actuel est le propriétaire du contact. Les gestionnaires d’autorisation sont généralement les suivants :

  • Appelez context.Succeed lorsque les exigences sont remplies.
  • Retourne Task.CompletedTask lorsque les exigences ne sont pas remplies. Le retour Task.CompletedTask sans appel antérieur ou context.Successcontext.Fail, n’est pas un succès ou un échec, il permet à d’autres gestionnaires d’autorisation de s’exécuter.

Si vous devez échouer explicitement, appelez le contexte. Échec.

L’application permet aux propriétaires de contacts de modifier/supprimer/de créer leurs propres données. ContactIsOwnerAuthorizationHandler n’a pas besoin de vérifier l’opération passée dans le paramètre d’exigence.

Créer un gestionnaire d’autorisation de gestionnaire

Créez une ContactManagerAuthorizationHandler classe dans le dossier Authorization . L’utilisateur ContactManagerAuthorizationHandler vérifie que l’utilisateur agissant sur la ressource est un gestionnaire. Seuls les responsables 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 ContactAdministratorsAuthorizationHandler classe dans le dossier Authorization . Vérifie ContactAdministratorsAuthorizationHandler 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;
        }
    }
}

Inscrire les gestionnaires d’autorisation

Les services utilisant Entity Framework Core doivent être inscrits pour l’injection de dépendances à l’aide AddScopedde . Utilise ContactIsOwnerAuthorizationHandler ASP.NET Core Identity, qui est basé sur Entity Framework Core. Inscrivez les gestionnaires auprès de la collection de services afin qu’ils soient disponibles par le biais de l’injection ContactsController de dépendances. Ajoutez le code suivant à la fin de ConfigureServices:

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(
    options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddRazorPages();

builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// Authorization handlers.
builder.Services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

builder.Services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<ApplicationDbContext>();
    context.Database.Migrate();
    // requires using Microsoft.Extensions.Configuration;
    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>

    var testUserPw = builder.Configuration.GetValue<string>("SeedUserPW");

   await SeedData.Initialize(services, testUserPw);
}

ContactAdministratorsAuthorizationHandler et ContactManagerAuthorizationHandler sont ajoutés en tant que singletons. Ils sont singletons, car ils n’utilisent pas EF et toutes les informations nécessaires sont dans le Context paramètre de la HandleRequirementAsync méthode.

Autorisation de prise en charge

Dans cette section, vous mettez à jour les Razor pages et ajoutez une classe d’exigences en matière d’opérations.

Passer en revue la classe de configuration requise pour les opérations de contact

Passez en revue la ContactOperations classe. Cette classe contient la configuration requise prise 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 IAuthorizationService service pour accéder aux gestionnaires d’autorisation.
  • Ajoute le IdentityUserManager service.
  • Ajoutez la ApplicationDbContext.

Mettre à jour createModel

Mettez à jour le modèle de page de création :

  • Constructeur pour utiliser la classe de DI_BasePageModel base.
  • OnPostAsync pour :
    • Ajoutez l’ID utilisateur au Contact modèle.
    • Appelez le gestionnaire d’autorisation pour vérifier que l’utilisateur a l’autorisation de 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 IndexModel

Mettez à jour la OnGetAsync méthode afin que seuls les contacts approuvés soient affichés aux 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 editModel

Ajoutez un gestionnaire d’autorisation pour vérifier que l’utilisateur possède le contact. Étant donné que l’autorisation de ressource est validée, 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 deleteModel

Mettez à jour le modèle de page de suppression pour utiliser le gestionnaire d’autorisation pour vérifier que l’utilisateur dispose de l’autorisation de suppression sur le contact.

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

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact? _contact = await Context.Contact.FirstOrDefaultAsync(
                                             m => m.ContactId == id);

        if (_contact == null)
        {
            return NotFound();
        }
        Contact = _contact;

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, Contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id)
    {
        var contact = await Context
            .Contact.AsNoTracking()
            .FirstOrDefaultAsync(m => m.ContactId == id);

        if (contact == null)
        {
            return NotFound();
        }

        var isAuthorized = await AuthorizationService.AuthorizeAsync(
                                                 User, contact,
                                                 ContactOperations.Delete);
        if (!isAuthorized.Succeeded)
        {
            return Forbid();
        }

        Context.Contact.Remove(contact);
        await Context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

Injecter le service d’autorisation dans les vues

Actuellement, l’interface utilisateur affiche les liens de modification et de suppression des contacts que l’utilisateur ne peut pas modifier.

Injectez le service d’autorisation dans le Pages/_ViewImports.cshtml fichier 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 using instructions.

Mettez à jour les liens Modifier et Supprimer afin Pages/Contacts/Index.cshtml qu’ils soient rendus uniquement pour les utilisateurs disposant des autorisations appropriées :

@page
@model ContactManager.Pages.Contacts.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Address)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].City)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].State)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Zip)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Contact[0].Email)
            </th>
             <th>
                @Html.DisplayNameFor(model => model.Contact[0].Status)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Contact) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Address)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.City)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.State)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Zip)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Email)
            </td>
                           <td>
                    @Html.DisplayFor(modelItem => item.Status)
                </td>
                <td>
                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Update)).Succeeded)
                    {
                        <a asp-page="./Edit" asp-route-id="@item.ContactId">Edit</a>
                        <text> | </text>
                    }

                    <a asp-page="./Details" asp-route-id="@item.ContactId">Details</a>

                    @if ((await AuthorizationService.AuthorizeAsync(
                     User, item,
                     ContactOperations.Delete)).Succeeded)
                    {
                        <text> | </text>
                        <a asp-page="./Delete" asp-route-id="@item.ContactId">Delete</a>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

Avertissement

Le masquage des 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 appeler des opérations de modification et de suppression sur les données qu’ils ne possèdent pas. La page ou le Razor contrôleur doit appliquer des vérifications 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 les 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 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 problème pour plus d’informations sur :

  • Suppression des privilèges d’un utilisateur. Par exemple, désactiver le son d’un utilisateur dans une application de conversation.
  • Ajout de privilèges à un utilisateur.

Différences entre Défi et Interdiction

Cette application définit la stratégie par défaut pour exiger des utilisateurs authentifiés. Le code suivant permet aux utilisateurs anonymes. Les utilisateurs anonymes sont autorisés à montrer les différences entre Challenge et Interdit.

[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 retourné. Lorsqu’un ChallengeResult est retourné, l’utilisateur est redirigé vers la page de connexion.
  • Lorsque l’utilisateur est authentifié, mais qu’il n’est pas autorisé, un ForbidResult est retourné. Lorsqu’un ForbidResult utilisateur est retourné, il 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 d’utilisateur amorçage, utilisez l’outil Gestionnaire de secrets pour définir un mot de passe :

  • Choisissez un mot de passe fort : utilisez huit ou plusieurs caractères et au moins un caractère majuscule, un nombre et un symbole. Par exemple, Passw0rd! répond aux exigences de mot de passe fortes.

  • Exécutez la commande suivante à partir du dossier du projet, où <PW> se trouve le mot de passe :

    dotnet user-secrets set SeedUserPW <PW>
    

Si l’application a des contacts :

  • Supprimez tous les enregistrements de la Contact table.
  • 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 sessions incognito/InPrivate). Dans un navigateur, inscrivez un nouvel utilisateur (par exemple). test@example.com Connectez-vous à chaque navigateur avec un autre utilisateur. Vérifiez les opérations suivantes :

  • Les utilisateurs inscrits peuvent afficher toutes les données de contact approuvées.
  • Les utilisateurs inscrits peuvent modifier/supprimer leurs propres données.
  • Les responsables peuvent approuver/rejeter les données de contact. L’affichage 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 No Modifiez et supprimez leurs données.
manager@contoso.com Yes Modifiez et supprimez leurs données.
admin@contoso.com Yes Modifiez et supprimez toutes les données.

Créez un contact dans le navigateur de l’administrateur. Copiez l’URL de suppression et de modification du contact administrateur. Collez ces liens dans le navigateur de l’utilisateur de test pour vérifier que l’utilisateur de test ne peut pas effectuer ces opérations.

Créer l’application de démarrage

  • Créer une Razor application Pages nommée « ContactManager »

    • Créez l’application avec des comptes d’utilisateur individuels.
    • Nommez-le « ContactManager » pour que l’espace de noms corresponde à l’espace de noms utilisé dans l’exemple.
    • -uld spécifie LocalDB 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; }
        }
    }
    
  • Structurer le Contact modèle.

  • Créez une 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
  • Mettez à jour l’ancre ContactManager dans le Pages/Shared/_Layout.cshtml fichier :

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • Tester l’application en créant, en modifiant et en 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();
        }

    }
}

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

Testez que l’application a amorçage la base de données. S’il existe des lignes dans la base de données de contact, la méthode d’amorçage n’est pas exécutée.

Ce tutoriel montre comment créer une application web ASP.NET Core avec des données utilisateur protégées par autorisation. Il affiche la liste des contacts qui ont été authentifiés (inscrits) que les utilisateurs ont créés. Il existe trois groupes de sécurité :

  • Les utilisateurs inscrits peuvent afficher toutes les données approuvées et modifier/supprimer leurs propres données.
  • Les gestionnaires peuvent approuver ou rejeter les données de contact. Seuls les contacts approuvés sont visibles par 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 des/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é l’état sur « Approuvé ».

Screenshot showing Rick signed in

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

Screenshot showing manager@contoso.com signed in

L’image suivante montre l’affichage des détails des gestionnaires d’un contact :

Manager's view of a contact

Les boutons Approuver et Rejeter sont affichés uniquement pour les gestionnaires et les administrateurs.

Dans l’image suivante, admin@contoso.com est connecté et dans le rôle de l’administrateur :

Screenshot showing admin@contoso.com signed in

L’administrateur dispose de tous les privilèges. Elle peut lire/modifier/supprimer n’importe quel contact et modifier l’état des contacts.

L’application a été créée en créant la structure du modèle suivant Contact :

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

L’exemple contient les gestionnaires d’autorisation suivants :

  • ContactIsOwnerAuthorizationHandler: garantit qu’un utilisateur ne peut modifier ses données que.
  • ContactManagerAuthorizationHandler: permet aux gestionnaires 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 didacticiel est avancé. Vous devez être familiarisé avec les points suivants :

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 fonctionnalités 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, consultez Créer l’application de démarrage.

Sécuriser les données utilisateur

Les sections suivantes ont toutes les étapes principales pour créer l’application de données utilisateur sécurisée. Vous pouvez trouver utile de faire référence au projet terminé.

Lier les données de contact à l’utilisateur

Utilisez l’ID utilisateur ASP.NET Identity pour vous assurer que les utilisateurs peuvent modifier leurs données, mais pas d’autres données d’utilisateurs. Ajoutez OwnerID et ContactStatus à ce 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 à partir de la AspNetUser table de la Identity base de données. Le Status champ détermine si un contact est visible par les utilisateurs généraux.

Créez une migration et mettez à jour la base de données :

dotnet ef migrations add userID_Status
dotnet ef database update

Ajouter des services de rôle à Identity

Ajoutez AddRoles pour ajouter des services de rôle :

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

Exiger des utilisateurs authentifiés

Définissez la stratégie d’authentification de secours pour exiger que les utilisateurs soient authentifiés :

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(
        options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddRazorPages();

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    });

Le code mis en surbrillance précédent définit la stratégie d’authentification de secours. La stratégie d’authentification de secours nécessite que tous les utilisateurs soient authentifiés, sauf pour Razor les pages, les contrôleurs ou les méthodes d’action avec un attribut d’authentification. Par exemple, Razor pages, contrôleurs ou méthodes d’action avec [AllowAnonymous] ou [Authorize(PolicyName="MyPolicy")] utilisez l’attribut d’authentification appliqué plutôt que la stratégie d’authentification de secours.

RequireAuthenticatedUser ajoute DenyAnonymousAuthorizationRequirement à l’instance actuelle, qui applique que l’utilisateur actuel est authentifié.

Stratégie d’authentification de secours :

  • Est appliqué à toutes les demandes qui ne spécifient pas explicitement une stratégie d’authentification. Pour les demandes servies par le routage des points 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 intergiciels après le middleware d’autorisation, tels que les fichiers statiques, cela appliquerait la stratégie à toutes les demandes.

La définition de la stratégie d’authentification de secours pour exiger que les utilisateurs soient authentifiés protège les pages et les contrôleurs nouvellement ajoutés Razor . L’authentification requise par défaut est plus sécurisée que de s’appuyer sur de nouveaux contrôleurs et Razor pages pour inclure l’attribut [Authorize] .

La AuthorizationOptions classe contient AuthorizationOptions.DefaultPolicyégalement . Il DefaultPolicy s’agit de la stratégie utilisée avec l’attribut [Authorize] lorsqu’aucune stratégie 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 l’autorisation basée sur les stratégies dans ASP.NET Core.

Une autre façon pour les contrôleurs et Razor pages MVC de demander à tous les utilisateurs d’être authentifiés est d’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 des points de terminaison. La définition de la stratégie de secours est la méthode préférée pour exiger que tous les utilisateurs soient authentifiés.

Ajoutez AllowAnonymous aux Index pages et Privacy pour 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 SeedData classe crée deux comptes : administrateur et gestionnaire. Utilisez l’outil Secret Manager pour définir un mot de passe pour ces comptes. Définissez le mot de passe à partir du répertoire du projet (répertoire contenant Program.cs) :

dotnet user-secrets set SeedUserPW <PW>

Si un mot de passe fort n’est pas spécifié, une exception est levée lorsqu’elle SeedData.Initialize est appelée.

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 Initialize méthode dans la SeedData classe 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 d’utilisateur administrateur et ContactStatus aux contacts. Faites l’un des contacts « Soumis » et un « Rejeté ». Ajoutez l’ID utilisateur et l’état à 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 ContactIsOwnerAuthorizationHandler classe dans le dossier Authorization . Il ContactIsOwnerAuthorizationHandler vérifie que l’utilisateur agissant sur une ressource possè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;
        }
    }
}

Contexte d’appel ContactIsOwnerAuthorizationHandler. Réussissez si l’utilisateur authentifié actuel est le propriétaire du contact. Les gestionnaires d’autorisation sont généralement les suivants :

  • Appelez context.Succeed lorsque les exigences sont remplies.
  • Retourne Task.CompletedTask lorsque les exigences ne sont pas remplies. Le retour Task.CompletedTask sans appel antérieur ou context.Successcontext.Fail, n’est pas un succès ou un échec, il permet à d’autres gestionnaires d’autorisation de s’exécuter.

Si vous devez échouer explicitement, appelez le contexte. Échec.

L’application permet aux propriétaires de contacts de modifier/supprimer/de créer leurs propres données. ContactIsOwnerAuthorizationHandler n’a pas besoin de vérifier l’opération passée dans le paramètre d’exigence.

Créer un gestionnaire d’autorisation de gestionnaire

Créez une ContactManagerAuthorizationHandler classe dans le dossier Authorization . L’utilisateur 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 ContactAdministratorsAuthorizationHandler classe dans le dossier Authorization . L’utilisateur 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;
        }
    }
}

Inscrire les gestionnaires d’autorisation

Les services utilisant Entity Framework Core doivent être inscrits pour l’injection de dépendances à l’aide AddScopedde . L’utilisation ContactIsOwnerAuthorizationHandler ASP.NET Core Identity, qui est basée sur Entity Framework Core. Inscrivez les gestionnaires auprès de la collection de services afin qu’ils soient disponibles pour l’injection ContactsControllerde 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. Ils sont singletons, car ils n’utilisent pas EF et toutes les informations nécessaires sont dans le Context paramètre de la HandleRequirementAsync méthode.

Prise en charge de l’autorisation

Dans cette section, vous mettez à jour les Razor pages et ajoutez une classe de configuration requise pour les opérations.

Passez en revue la classe de configuration requise pour les opérations de contact

Passez en revue la ContactOperations classe. Cette classe contient les exigences que l’application prend en charge :

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 seul 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 IAuthorizationService service pour accéder aux gestionnaires d’autorisation.
  • Ajoute le IdentityUserManager service.
  • Ajoutez la ApplicationDbContext.

Mettre à jour createModel

Mettez à jour le constructeur de modèle de page pour utiliser la DI_BasePageModel classe de base :

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

Mettez à jour la CreateModel.OnPostAsync méthode pour :

  • Ajoutez l’ID utilisateur au Contact modèle.
  • Appelez le gestionnaire d’autorisation pour vérifier que l’utilisateur dispose de l’autorisation de 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 indexModel

Mettez à jour la OnGetAsync méthode afin que seuls les contacts approuvés soient affichés aux 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 editModel

Ajoutez un gestionnaire d’autorisation pour vérifier que l’utilisateur possède le contact. Étant donné que l’autorisation de ressource est validée, 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 le 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 passant 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 pour vérifier que l’utilisateur dispose de l’autorisation de suppression sur le contact.

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

    [BindProperty]
    public Contact Contact { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Contact = 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 des contacts que l’utilisateur ne peut pas modifier.

Injectez le service d’autorisation dans le Pages/_ViewImports.cshtml fichier 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 using instructions.

Mettez à jour les liens Pages/Contacts/Index.cshtmlModifier et supprimer afin qu’ils soient affichés uniquement 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

La masquage des 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 des liens valides. Les utilisateurs peuvent pirater les URL générées pour appeler les opérations de modification et de suppression sur les données qu’ils ne possèdent pas. La page ou le Razor contrôleur doit appliquer des vérifications 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 gestionnaires puissent approuver ou rejeter les 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 d’informations :

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 problème pour plus d’informations sur :

  • Suppression des privilèges d’un utilisateur. Par exemple, la désactivation d’un utilisateur dans une application de conversation.
  • Ajout de privilèges à un utilisateur.

Différences entre défi et interdiction

Cette application définit la stratégie par défaut pour exiger des utilisateurs authentifiés. Le code suivant permet aux utilisateurs anonymes. Les utilisateurs anonymes sont autorisés à montrer les différences entre Challenge et Interdit.

[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 est ChallengeResult retourné. Lorsqu’un ChallengeResult est retourné, l’utilisateur est redirigé vers la page de connexion.
  • Lorsque l’utilisateur est authentifié, mais non autorisé, un est ForbidResult retourné. Lorsqu’un ForbidResult est retourné, l’utilisateur est redirigé vers la page d’accès refusée.

Tester l’application terminée

Si vous n’avez pas déjà défini de mot de passe pour les comptes d’utilisateur amorçage, utilisez l’outil Secret Manager pour définir un mot de passe :

  • Choisissez un mot de passe fort : utilisez huit ou plusieurs caractères et au moins un caractère majuscule, un nombre et un symbole. Par exemple, Passw0rd! répond aux exigences de mot de passe fortes.

  • Exécutez la commande suivante à partir du dossier du projet, où <PW> se trouve le mot de passe :

    dotnet user-secrets set SeedUserPW <PW>
    

Si l’application a des contacts :

  • Supprimez tous les enregistrements de la Contact table.
  • Redémarrez l’application pour amorçage de 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, inscrivez 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 inscrits peuvent afficher toutes les données de contact approuvées.
  • Les utilisateurs inscrits peuvent modifier/supprimer leurs propres données.
  • Les gestionnaires peuvent approuver/rejeter les données de contact. L’affichage Details affiche les boutons Approuver et Rejeter .
  • Les administrateurs peuvent approuver/rejeter et modifier/supprimer toutes les données.
Utilisateur Amorçage par l’application Options
test@example.com No Modifiez/supprimez les propres données.
manager@contoso.com Yes Approuver/rejeter et modifier/supprimer les propres données.
admin@contoso.com Yes Approuver/rejeter et modifier/supprimer toutes les données.

Créez un contact dans le navigateur de l’administrateur. Copiez l’URL de suppression et de modification du contact administrateur. Collez ces liens dans le navigateur de l’utilisateur de test pour vérifier que l’utilisateur de test ne peut pas effectuer ces opérations.

Créer l’application de démarrage

  • Créer une Razor application Pages nommée « ContactManager »

    • Créez l’application avec des comptes d’utilisateur individuels.
    • Nommez-le « ContactManager » pour que l’espace de noms corresponde à l’espace de noms utilisé dans l’exemple.
    • -uld spécifie LocalDB 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; }
    }
    
  • Structurer le Contact modèle.

  • Créez une 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

Si vous rencontrez un bogue avec la dotnet aspnet-codegenerator razorpage commande, consultez ce problème GitHub.

  • Mettez à jour l’ancre ContactManager dans le Pages/Shared/_Layout.cshtml fichier :
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Tester l’application en créant, en modifiant et en 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();
        }

    }
}

Appel SeedData.Initialize à partir de Main:

using ContactManager.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContactManager
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<ApplicationDbContext>();
                    context.Database.Migrate();
                    SeedData.Initialize(services, "not used");
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Testez que l’application a amorçage la base de données. S’il existe des lignes dans la base de données de contact, la méthode d’amorçage n’est pas exécutée.

Ressources supplémentaires