Compartilhar via


Criar um aplicativo com os dados do usuário protegidos por autorização

Por Rick Anderson e Joe Audette

Este tutorial mostra como criar um aplicativo Web ASP.NET Core com dados do usuário protegidos por autorização. Ele exibe uma lista de contatos que os usuários autenticados (registrados) criaram. Há três grupos de segurança:

  • Os usuários registrados podem exibir todos os dados aprovados e podem editar/excluir seus próprios dados.
  • Os gerentes podem aprovar ou rejeitar dados de contato. Somente os contatos aprovados são visíveis para os usuários.
  • Os administradores podem aprovar/rejeitar e editar/excluir quaisquer dados.

As imagens neste documento não correspondem exatamente aos modelos mais recentes.

Na imagem a seguir, o usuário Rick (rick@example.com) está conectado. Rick só pode exibir contatos aprovados e Editar/ Excluir/Criar Novos links para seus contatos. Somente o último registro, criado por Rick, exibe links Editar e Excluir. Outros usuários não verão o último registro até que um gerente ou administrador altere o status para "Aprovado".

Captura de tela mostrando Rick conectado

Na imagem a seguir, manager@contoso.com está conectado e na função do gerente:

Captura de tela mostrando manager@contoso.com conectado

A imagem a seguir mostra a exibição de detalhes dos gerentes de um contato:

Visão do gerente de um contato

Os botões Aprovar e Rejeitar são exibidos apenas para gerentes e administradores.

Na imagem a seguir, admin@contoso.com está conectado e na função do administrador:

Captura de tela mostrando admin@contoso.com conectado

O administrador tem todos os privilégios. Ela pode ler, editar ou excluir qualquer contato e alterar a status de contatos.

O aplicativo foi criado por scaffolding do seguinte modelo 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; }
}

O exemplo contém os seguintes manipuladores de autorização:

  • ContactIsOwnerAuthorizationHandler: garante que um usuário só possa editar seus dados.
  • ContactManagerAuthorizationHandler: permite que os gerentes aprovem ou rejeitem contatos.
  • ContactAdministratorsAuthorizationHandler: permite que os administradores aprovem ou rejeitem contatos e editem/excluam contatos.

Pré-requisitos

Este tutorial é avançado. Você deve estar familiarizado com:

O aplicativo inicial e concluído

Baixar o aplicativo concluído. Teste o aplicativo concluído para que você se familiarize com seus recursos de segurança.

O aplicativo inicial

Baixar o aplicativo iniciador.

Execute o aplicativo, toque no link ContactManager e verifique se você pode criar, editar e excluir um contato. Para criar o aplicativo inicial, consulte Criar o aplicativo inicial.

Proteger dados do usuário

As seções a seguir têm todas as principais etapas para criar o aplicativo de dados de usuário seguro. Talvez seja útil consultar o projeto concluído.

Vincular os dados de contato ao usuário

Use a ID de usuário ASP.NET Identity para garantir que os usuários possam editar seus dados, mas não outros dados de usuários. Adicione OwnerID e ContactStatus ao modelo Contact:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string? OwnerID { get; set; }

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

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID é a ID do usuário da AspNetUser tabela no Identity banco de dados. O Status campo determina se um contato pode ser exibido por usuários gerais.

Crie uma nova migração e atualize o banco de dados:

dotnet ef migrations add userID_Status
dotnet ef database update

Adicionar serviços de função ao Identity

Acrescente AddRoles para adicionar serviços de função:

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

Exigir usuários autenticados

Defina a política de autorização de fallback para exigir que os usuários sejam autenticados:

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

O código realçado anterior define a política de autorização de fallback. A política de autorização de fallback exige que todos os usuários sejam autenticados, exceto páginas Razor, controladores ou métodos de ação com um atributo de autorização. Por exemplo, Páginas do Razor, controladores ou métodos de ação com [AllowAnonymous] ou [Authorize(PolicyName="MyPolicy")] usam o atributo de autorização aplicado em vez da política de autorização de fallback.

RequireAuthenticatedUser adiciona DenyAnonymousAuthorizationRequirement à instância atual, que impõe que o usuário atual seja autenticado.

A política de autorização de fallback:

  • É aplicado a todas as solicitações que não especificam explicitamente uma política de autorização. Para solicitações atendidas pelo roteamento de ponto de extremidade, isso inclui qualquer ponto de extremidade que não especifique um atributo de autorização. Para solicitações atendidas por outro middleware após o middleware de autorização, como arquivos estáticos, isso aplica a política a todas as solicitações.

Definir a política de autorização de fallback para exigir que os usuários sejam autenticados protege páginas e controladores recém-adicionados Razor. Ter a autorização exigida por padrão é mais seguro do que depender de novos controladores e Razor Páginas para incluir o atributo [Authorize].

A classe AuthorizationOptions também contém AuthorizationOptions.DefaultPolicy. A DefaultPolicy é a política usada com o [Authorize] atributo quando nenhuma política é especificada. [Authorize] não contém uma política nomeada, ao contrário de [Authorize(PolicyName="MyPolicy")].

Para obter mais informações sobre as políticas de autorização, consulte Autorização baseada em política no ASP.NET Core.

Uma maneira alternativa para controladores MVC e Razor Páginas exigirem que todos os usuários sejam autenticados é adicionar um filtro de autorização:

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

O código anterior usa um filtro de autorização, definindo que a política de fallback usa o roteamento de ponto de extremidade. Definir a política de fallback é a maneira preferencial de exigir que todos os usuários sejam autenticados.

Adicione AllowAnonymous às páginas e Index para Privacy que os usuários anônimos possam obter informações sobre o site antes de se registrarem:

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()
    {

    }
}

Configurar a conta de teste

A SeedData classe cria duas contas: administrador e gerente. Use a ferramenta Gerenciador de Segredos para definir uma senha para essas contas. Defina a senha do diretório do projeto (o diretório que contém Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Se uma senha fraca não for especificada, uma exceção será gerada quando SeedData.Initialize for chamada.

Atualize o aplicativo para usar a senha de teste:

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);
}

Criar as contas de teste e atualizar os contatos

Atualize o método Initialize na classe SeedData para criar as contas de teste:

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;
}

Adicione a ID de usuário do administrador e ContactStatus aos contatos. Faça um dos contatos "Enviado" e um "Rejeitado". Adicione a ID de usuário e status a todos os contatos. Apenas um contato é mostrado:

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
        },

Criar manipuladores de autorização de proprietário, gerente e administrador

Crie uma classe ContactIsOwnerAuthorizationHandler na pasta Autorização. O ContactIsOwnerAuthorizationHandler verifica se o usuário que está atuando em um recurso é o proprietário do recurso.

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;
        }
    }
}

O ContactIsOwnerAuthorizationHandler contexto de chamadas . Êxito se o usuário autenticado atual for o proprietário do contato. Manipuladores de autorização em geral:

  • Chame context.Succeed quando os requisitos forem atendidos.
  • Retornar Task.CompletedTask quando os requisitos não forem atendidos. Task.CompletedTask Retornar sem uma chamada anterior para context.Success ou context.Fail, não é um êxito ou falha, ele permite que outros manipuladores de autorização sejam executados.

Se você precisar falhar explicitamente, chame context.Fail.

O aplicativo permite que os proprietários de contato editem/excluam/criem seus próprios dados. ContactIsOwnerAuthorizationHandler não precisa marcar a operação passada no parâmetro de requisito.

Criar um manipulador de autorização de gerente

Crie uma classe ContactManagerAuthorizationHandler na pasta Autorização. O ContactManagerAuthorizationHandler verifica se o usuário que está atuando no recurso é um gerente. Somente os gerentes podem aprovar ou rejeitar alterações de conteúdo (novas ou alteradas).

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;
        }
    }
}

Criar um manipulador de autorização de administrador

Crie uma classe ContactAdministratorsAuthorizationHandler na pasta Autorização. O ContactAdministratorsAuthorizationHandler verifica se o usuário que está atuando no recurso é um administrador. O administrador pode fazer todas as operações.

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;
        }
    }
}

Registrar os manipuladores de autorização

Os serviços que usam o Entity Framework Core devem ser registrados para injeção de dependência usando AddScoped. O ContactIsOwnerAuthorizationHandler usa ASP.NET Core Identity, que é criado no Entity Framework Core. Registre os manipuladores com a coleção de serviços para que eles estejam disponíveis para o ContactsController por meio da injeção de dependência. Adicione o seguinte código ao final 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 e ContactManagerAuthorizationHandler são adicionados como singletons. Eles são singletons porque não usam EF e todas as informações necessárias estão no parâmetro Context do método HandleRequirementAsync.

Autorização de suporte

Nesta seção, você atualizará as Razor Páginas e adicionará uma classe de requisitos de operações.

Examinar a classe de requisitos de operações de contato

Examine a classeContactOperations . Essa classe contém os requisitos compatíveis com o aplicativo:

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";
    }
}

Criar uma classe base para as Páginas de Contatos Razor

Crie uma classe base que contenha os serviços usados nas Páginas de contatos Razor. A classe base coloca o código de inicialização em um único local:

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;
        } 
    }
}

O código anterior:

  • Adiciona o serviço IAuthorizationService para acessar os manipuladores de autorização.
  • Adiciona o serviço IdentityUserManager.
  • Adicione a ApplicationDbContext.

Atualizar o CreateModel

Atualize o modelo de página de criação:

  • Construtor para usar a classe base DI_BasePageModel.
  • OnPostAsync método para:
    • Adicione a ID de usuário ao modelo Contact.
    • Chame o manipulador de autorização para verificar se o usuário tem permissão para criar contatos.
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");
        }
    }
}

Atualizar o IndexModel

Atualize o método OnGetAsync para que somente os contatos aprovados sejam mostrados aos usuários gerais:

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

Atualizar o EditModel

Adicione um manipulador de autorização para verificar se o usuário é o proprietário do contato. Como a autorização de recurso está sendo validada, o atributo [Authorize] não é suficiente. O aplicativo não tem acesso ao recurso quando os atributos são avaliados. A autorização baseada em recursos deve ser imperativa. As verificações devem ser executadas quando o aplicativo tem acesso ao recurso, seja carregando-o no modelo de página ou carregando-o dentro do próprio manipulador. Você acessa o recurso com frequência passando a chave de recurso.

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");
    }
}

Atualizar o DeleteModel

Atualize o modelo de página de exclusão para usar o manipulador de autorização para verificar se o usuário tem permissão de exclusão no contato.

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");
    }
}

Injetar o serviço de autorização nos modos de exibição

Atualmente, a interface do usuário mostra links de edição e exclusão para contatos que o usuário não pode modificar.

Injete o serviço de autorização no arquivo Pages/_ViewImports.cshtml para que ele esteja disponível para todos os modos de exibição:

@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

A marcação anterior adiciona várias using instruções.

Atualize os links Editar e Excluir no Pages/Contacts/Index.cshtml para que eles sejam renderizados apenas para usuários com as permissões apropriadas:

@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>

Aviso

Ocultar links de usuários que não têm permissão para alterar dados não protege o aplicativo. Ocultar links torna o aplicativo mais amigável exibindo apenas links válidos. Os usuários podem invadir as URLs geradas para invocar operações de edição e exclusão em dados que não possuem. A Página Razor ou o controlador deve impor verificações de acesso para proteger os dados.

Detalhes da Atualização

Atualize a exibição de detalhes para que os gerentes possam aprovar ou rejeitar contatos:

        @*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>

Atualizar o modelo de página de detalhes

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");
    }
}

Adicionar ou remover um usuário a uma função

Confira este problema para obter informações sobre:

  • Removendo privilégios de um usuário. Por exemplo, ativar o mudo de um usuário em um aplicativo de chat.
  • Adicionando privilégios a um usuário.

Diferenças entre desafio e proibição

Esse aplicativo define a política padrão para exigir usuários autenticados. O código a seguir permite usuários anônimos. Os usuários anônimos têm permissão para mostrar as diferenças entre Desafio versus Proibição.

[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();
    }
}

No código anterior:

  • Quando o usuário não é autenticado, um ChallengeResult é retornado. Quando um ChallengeResult é retornado, o usuário é redirecionado para a página de entrada.
  • Quando o usuário é autenticado, mas não autorizado, um ForbidResult é retornado. Quando um ForbidResult é retornado, o usuário é redirecionado para a página de acesso negado.

Teste o aplicativo finalizado

Aviso

Este artigo usa a ferramenta Gerenciador de Segredos para armazenar a senha das contas de usuário propagadas. A ferramenta Gerenciador de Segredos é usada para armazenar dados confidenciais durante o desenvolvimento local. Para obter informações sobre procedimentos de autenticação que podem ser usados quando um aplicativo é implantado em um ambiente de teste ou produção, consulte Fluxos de autenticação seguros.

Se você ainda não definiu uma senha para contas de usuário propagadas, use a ferramenta Gerenciador de Segredos para definir uma senha:

  • Escolha uma senha forte:

    • Pelo menos com 12 caracteres, mas 14 ou mais é melhor.
    • Uma combinação de letras maiúsculas, letras minúsculas, números e símbolos.
    • Não deve ser uma palavra que pode ser encontrada em um dicionário ou o nome de uma pessoa, personagem, produto ou organização.
    • É significativamente diferente de suas senhas anteriores.
    • Fácil para você lembrar, mas difícil para os outros adivinharem. Considere usar uma frase memorável como "6MacacosTaumOlhandu^".
  • Execute o seguinte comando na pasta do projeto, em que <PW> é a senha:

    dotnet user-secrets set SeedUserPW <PW>
    

Se o aplicativo tiver contatos:

  • Exclua todos os registros da tabela Contact.
  • Reinicie o aplicativo para propagar o banco de dados.

Uma maneira fácil de testar o aplicativo concluído é iniciar três navegadores diferentes (ou sessões anônimas/InPrivate). Em um navegador, registre um novo usuário (por exemplo, test@example.com). Entre em cada navegador com um usuário diferente. Verifique as seguintes operações:

  • Os usuários registrados podem exibir todos os dados de contato aprovados.
  • Os usuários registrados podem editar/excluir seus próprios dados.
  • Os gerentes podem aprovar/rejeitar dados de contato. O modo de exibição Details mostra os botões Aprovar e Rejeitar.
  • Os administradores podem aprovar/rejeitar e editar/excluir todos os dados.
Usuário Aprovar ou rejeitar contatos Opções
test@example.com No Edite e exclua seus dados.
manager@contoso.com Sim Edite e exclua seus dados.
admin@contoso.com Yes Edite e exclua todos os dados.

Crie um contato no navegador do administrador. Copie a URL para excluir e editar do contato do administrador. Cole esses links no navegador do usuário de teste para verificar se o usuário de teste não pode executar essas operações.

Criar o aplicativo inicial

  • Criar um Razor aplicativo Pages chamado "ContactManager"

    • Crie o aplicativo com contas de usuário individuais.
    • Nomeie-o como "ContactManager" para que o namespace corresponda ao namespace usado no exemplo.
    • -uld especifica o LocalDB em vez do SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Adicionar 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; }
        }
    }
    
  • Faça um andaime do modelo Contact.

  • Crie a migração inicial e atualize o banco de dados:

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

Observação

Por padrão, a arquitetura dos binários do .NET a serem instalados representa a arquitetura do SO sendo executado no momento. Para especificar uma arquitetura de SO diferente, consulte instalação da ferramenta dotnet, opção --arch. Para obter mais informações, consulte o problema dotnet/AspNetCore.Docs #29262 do GitHub.

  • Atualize a âncora ContactManager no Pages/Shared/_Layout.cshtml arquivo:

    <a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
    
  • Testar o aplicativo criando, editando e excluindo um contato

Propagar o banco de dados

Adicione a classe SeedData à pasta Dados:

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

    }
}

Chamada SeedData.Initialize 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();

Teste se o aplicativo semeou o banco de dados. Se houver linhas no banco de dados de contato, o método de semente não será executado.

Este tutorial mostra como criar um aplicativo Web ASP.NET Core com dados do usuário protegidos por autorização. Ele exibe uma lista de contatos que os usuários autenticados (registrados) criaram. Há três grupos de segurança:

  • Os usuários registrados podem exibir todos os dados aprovados e podem editar/excluir seus próprios dados.
  • Os gerentes podem aprovar ou rejeitar dados de contato. Somente os contatos aprovados são visíveis para os usuários.
  • Os administradores podem aprovar/rejeitar e editar/excluir quaisquer dados.

As imagens neste documento não correspondem exatamente aos modelos mais recentes.

Na imagem a seguir, o usuário Rick (rick@example.com) está conectado. Rick só pode exibir contatos aprovados e Editar/ Excluir/Criar Novos links para seus contatos. Somente o último registro, criado por Rick, exibe links Editar e Excluir. Outros usuários não verão o último registro até que um gerente ou administrador altere o status para "Aprovado".

Captura de tela mostrando Rick conectado

Na imagem a seguir, manager@contoso.com está conectado e na função do gerente:

Captura de tela mostrando manager@contoso.com conectado

A imagem a seguir mostra a exibição de detalhes dos gerentes de um contato:

Visão do gerente de um contato

Os botões Aprovar e Rejeitar são exibidos apenas para gerentes e administradores.

Na imagem a seguir, admin@contoso.com está conectado e na função do administrador:

Captura de tela mostrando admin@contoso.com conectado

O administrador tem todos os privilégios. Ela pode ler/editar/excluir qualquer contato e alterar a status de contatos.

O aplicativo foi criado por scaffolding do seguinte modelo 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; }
}

O exemplo contém os seguintes manipuladores de autorização:

  • ContactIsOwnerAuthorizationHandler: garante que um usuário só possa editar seus dados.
  • ContactManagerAuthorizationHandler: permite que os gerentes aprovem ou rejeitem contatos.
  • ContactAdministratorsAuthorizationHandler: permite que os administradores:
    • Aprovar ou rejeitar contatos
    • Editar e excluir contatos

Pré-requisitos

Este tutorial é avançado. Você deve estar familiarizado com:

O aplicativo inicial e concluído

Baixar o aplicativo concluído. Teste o aplicativo concluído para que você se familiarize com seus recursos de segurança.

O aplicativo inicial

Baixar o aplicativo iniciador.

Execute o aplicativo, toque no link ContactManager e verifique se você pode criar, editar e excluir um contato. Para criar o aplicativo inicial, consulte Criar o aplicativo inicial.

Proteger dados do usuário

As seções a seguir têm todas as principais etapas para criar o aplicativo de dados de usuário seguro. Talvez seja útil consultar o projeto concluído.

Vincular os dados de contato ao usuário

Use a ID de usuário ASP.NET Identity para garantir que os usuários possam editar seus dados, mas não outros dados de usuários. Adicione OwnerID e ContactStatus ao modelo Contact:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table.
    public string OwnerID { get; set; }

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

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID é a ID do usuário da AspNetUser tabela no Identity banco de dados. O Status campo determina se um contato pode ser exibido por usuários gerais.

Crie uma nova migração e atualize o banco de dados:

dotnet ef migrations add userID_Status
dotnet ef database update

Adicionar serviços de função ao Identity

Acrescente AddRoles para adicionar serviços de função:

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

Exigir usuários autenticados

Defina a política de autenticação de fallback para exigir que os usuários sejam autenticados:

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

O código realçado anterior define a política de autenticação de fallback. A política de autenticação de fallback exige que todos os usuários sejam autenticados, exceto páginas Razor, controladores ou métodos de ação com um atributo de autenticação. Por exemplo, Razor Páginas, controladores ou métodos de ação com [AllowAnonymous] ou [Authorize(PolicyName="MyPolicy")] usam o atributo de autenticação aplicado em vez da política de autenticação de fallback.

RequireAuthenticatedUser adiciona DenyAnonymousAuthorizationRequirement à instância atual, que impõe que o usuário atual seja autenticado.

A política de autenticação de fallback:

  • É aplicado a todas as solicitações que não especificam explicitamente uma política de autenticação. Para solicitações atendidas pelo roteamento de ponto de extremidade, isso incluiria qualquer ponto de extremidade que não especifique um atributo de autorização. Para solicitações atendidas por outro middleware após o middleware de autorização, como arquivos estáticos, isso aplicaria a política a todas as solicitações.

Definir a política de autenticação de fallback para exigir que os usuários sejam autenticados protege páginas e controladores recém-adicionados Razor. Ter a autenticação exigida por padrão é mais seguro do que depender de novos controladores e Razor Páginas para incluir o atributo [Authorize].

A classe AuthorizationOptions também contém AuthorizationOptions.DefaultPolicy. A DefaultPolicy é a política usada com o [Authorize] atributo quando nenhuma política é especificada. [Authorize] não contém uma política nomeada, ao contrário de [Authorize(PolicyName="MyPolicy")].

Para obter mais informações sobre as políticas de autorização, consulte Autorização baseada em política no ASP.NET Core.

Uma maneira alternativa para controladores MVC e Razor Páginas exigirem que todos os usuários sejam autenticados é adicionar um filtro de autorização:

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));
    });

O código anterior usa um filtro de autorização, definindo que a política de fallback usa o roteamento de ponto de extremidade. Definir a política de fallback é a maneira preferencial de exigir que todos os usuários sejam autenticados.

Adicione AllowAnonymous às páginas e Index para Privacy que os usuários anônimos possam obter informações sobre o site antes de se registrarem:

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()
        {

        }
    }
}

Configurar a conta de teste

A SeedData classe cria duas contas: administrador e gerente. Use a ferramenta Gerenciador de Segredos para definir uma senha para essas contas. Defina a senha do diretório do projeto (o diretório que contém Program.cs):

dotnet user-secrets set SeedUserPW <PW>

Se uma senha forte não for especificada, uma exceção será gerada quando SeedData.Initialize for chamada.

Atualize Main para usar a senha de teste:

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

Criar as contas de teste e atualizar os contatos

Atualize o método Initialize na classe SeedData para criar as contas de teste:

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;
}

Adicione a ID de usuário do administrador e ContactStatus aos contatos. Faça um dos contatos "Enviado" e um "Rejeitado". Adicione a ID de usuário e status a todos os contatos. Apenas um contato é mostrado:

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
        },

Criar manipuladores de autorização de proprietário, gerente e administrador

Crie uma classe ContactIsOwnerAuthorizationHandler na pasta Autorização. O ContactIsOwnerAuthorizationHandler verifica se o usuário que está atuando em um recurso é o proprietário do recurso.

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;
        }
    }
}

O ContactIsOwnerAuthorizationHandler contexto de chamadas . Êxito se o usuário autenticado atual for o proprietário do contato. Manipuladores de autorização em geral:

  • Chame context.Succeed quando os requisitos forem atendidos.
  • Retornar Task.CompletedTask quando os requisitos não forem atendidos. Task.CompletedTask Retornar sem uma chamada anterior para context.Success ou context.Fail, não é um êxito ou falha, ele permite que outros manipuladores de autorização sejam executados.

Se você precisar falhar explicitamente, chame context.Fail.

O aplicativo permite que os proprietários de contato editem/excluam/criem seus próprios dados. ContactIsOwnerAuthorizationHandler não precisa marcar a operação passada no parâmetro de requisito.

Criar um manipulador de autorização de gerente

Crie uma classe ContactManagerAuthorizationHandler na pasta Autorização. O ContactManagerAuthorizationHandler verifica se o usuário que está atuando no recurso é um gerente. Somente os gerentes podem aprovar ou rejeitar alterações de conteúdo (novas ou alteradas).

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;
        }
    }
}

Criar um manipulador de autorização de administrador

Crie uma classe ContactAdministratorsAuthorizationHandler na pasta Autorização. O ContactAdministratorsAuthorizationHandler verifica se o usuário que está atuando no recurso é um administrador. O administrador pode fazer todas as operações.

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;
        }
    }
}

Registrar os manipuladores de autorização

Os serviços que usam o Entity Framework Core devem ser registrados para injeção de dependência usando AddScoped. O ContactIsOwnerAuthorizationHandler usa ASP.NET Core Identity, que é criado no Entity Framework Core. Registre os manipuladores com a coleção de serviços para que eles estejam disponíveis para o ContactsController por meio da injeção de dependência. Adicione o seguinte código ao final 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 e ContactManagerAuthorizationHandler são adicionados como singletons. Eles são singletons porque não usam EF e todas as informações necessárias estão no parâmetro Context do método HandleRequirementAsync.

Autorização de suporte

Nesta seção, você atualizará as Razor Páginas e adicionará uma classe de requisitos de operações.

Examinar a classe de requisitos de operações de contato

Examine a classeContactOperations . Essa classe contém os requisitos compatíveis com o aplicativo:

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";
    }
}

Criar uma classe base para as Páginas de Contatos Razor

Crie uma classe base que contenha os serviços usados nas Páginas de contatos Razor. A classe base coloca o código de inicialização em um único local:

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;
        } 
    }
}

O código anterior:

  • Adiciona o serviço IAuthorizationService para acessar os manipuladores de autorização.
  • Adiciona o serviço IdentityUserManager.
  • Adicione a ApplicationDbContext.

Atualizar o CreateModel

Atualize o construtor criar modelo de página para usar a DI_BasePageModel classe base:

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

Atualize o método CreateModel.OnPostAsync para:

  • Adicione a ID de usuário ao modelo Contact.
  • Chame o manipulador de autorização para verificar se o usuário tem permissão para criar contatos.
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");
}

Atualizar o IndexModel

Atualize o método OnGetAsync para que somente os contatos aprovados sejam mostrados aos usuários gerais:

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

Atualizar o EditModel

Adicione um manipulador de autorização para verificar se o usuário é o proprietário do contato. Como a autorização de recurso está sendo validada, o atributo [Authorize] não é suficiente. O aplicativo não tem acesso ao recurso quando os atributos são avaliados. A autorização baseada em recursos deve ser imperativa. As verificações devem ser executadas quando o aplicativo tem acesso ao recurso, seja carregando-o no modelo de página ou carregando-o dentro do próprio manipulador. Você acessa o recurso com frequência passando a chave de recurso.

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");
    }
}

Atualizar o DeleteModel

Atualize o modelo de página de exclusão para usar o manipulador de autorização para verificar se o usuário tem permissão de exclusão no contato.

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");
    }
}

Injetar o serviço de autorização nos modos de exibição

Atualmente, a interface do usuário mostra links de edição e exclusão para contatos que o usuário não pode modificar.

Injete o serviço de autorização no arquivo Pages/_ViewImports.cshtml para que ele esteja disponível para todos os modos de exibição:

@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

A marcação anterior adiciona várias using instruções.

Atualize os links Editar e Excluir no Pages/Contacts/Index.cshtml para que eles sejam renderizados apenas para usuários com as permissões apropriadas:

@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>

Aviso

Ocultar links de usuários que não têm permissão para alterar dados não protege o aplicativo. Ocultar links torna o aplicativo mais amigável exibindo apenas links válidos. Os usuários podem invadir as URLs geradas para invocar operações de edição e exclusão em dados que não possuem. A Página Razor ou o controlador deve impor verificações de acesso para proteger os dados.

Detalhes da Atualização

Atualize a exibição de detalhes para que os gerentes possam aprovar ou rejeitar contatos:

        @*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>

Atualize o modelo de página de detalhes:

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");
    }
}

Adicionar ou remover um usuário a uma função

Confira este problema para obter informações sobre:

  • Removendo privilégios de um usuário. Por exemplo, ativar o mudo de um usuário em um aplicativo de chat.
  • Adicionando privilégios a um usuário.

Diferenças entre desafio e proibição

Esse aplicativo define a política padrão para exigir usuários autenticados. O código a seguir permite usuários anônimos. Os usuários anônimos têm permissão para mostrar as diferenças entre Desafio versus Proibição.

[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();
    }
}

No código anterior:

  • Quando o usuário não é autenticado, um ChallengeResult é retornado. Quando um ChallengeResult é retornado, o usuário é redirecionado para a página de entrada.
  • Quando o usuário é autenticado, mas não autorizado, um ForbidResult é retornado. Quando um ForbidResult é retornado, o usuário é redirecionado para a página de acesso negado.

Teste o aplicativo finalizado

Se você ainda não definiu uma senha para contas de usuário propagadas, use a ferramenta Gerenciador de Segredos para definir uma senha:

  • Escolha uma senha forte: use oito ou mais caracteres e pelo menos um caractere maiúsculo, número e símbolo. Por exemplo, Passw0rd! atende aos requisitos de senha fortes.

  • Execute o seguinte comando na pasta do projeto, em que <PW> é a senha:

    dotnet user-secrets set SeedUserPW <PW>
    

Se o aplicativo tiver contatos:

  • Exclua todos os registros da tabela Contact.
  • Reinicie o aplicativo para propagar o banco de dados.

Uma maneira fácil de testar o aplicativo concluído é iniciar três navegadores diferentes (ou sessões anônimas/InPrivate). Em um navegador, registre um novo usuário (por exemplo, test@example.com). Entre em cada navegador com um usuário diferente. Verifique as seguintes operações:

  • Os usuários registrados podem exibir todos os dados de contato aprovados.
  • Os usuários registrados podem editar/excluir seus próprios dados.
  • Os gerentes podem aprovar/rejeitar dados de contato. O modo de exibição Details mostra os botões Aprovar e Rejeitar.
  • Os administradores podem aprovar/rejeitar e editar/excluir todos os dados.
Usuário Propagado pelo aplicativo Opções
test@example.com No Edite/exclua os próprios dados.
manager@contoso.com Sim Aprovar/rejeitar e editar/excluir dados próprios.
admin@contoso.com Sim Aprovar/rejeitar e editar/excluir todos os dados.

Crie um contato no navegador do administrador. Copie a URL para excluir e editar do contato do administrador. Cole esses links no navegador do usuário de teste para verificar se o usuário de teste não pode executar essas operações.

Criar o aplicativo inicial

  • Criar um Razor aplicativo Pages chamado "ContactManager"

    • Crie o aplicativo com contas de usuário individuais.
    • Nomeie-o como "ContactManager" para que o namespace corresponda ao namespace usado no exemplo.
    • -uld especifica o LocalDB em vez do SQLite
    dotnet new webapp -o ContactManager -au Individual -uld
    
  • Adicionar 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; }
    }
    
  • Faça um andaime do modelo Contact.

  • Crie a migração inicial e atualize o banco de dados:

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

Observação

Por padrão, a arquitetura dos binários do .NET a serem instalados representa a arquitetura do SO sendo executado no momento. Para especificar uma arquitetura de SO diferente, consulte instalação da ferramenta dotnet, opção --arch. Para obter mais informações, consulte o problema dotnet/AspNetCore.Docs #29262 do GitHub.

Se você tiver um bug com o comando dotnet aspnet-codegenerator razorpage, consulte este problema do GitHub.

  • Atualize a âncora ContactManager no Pages/Shared/_Layout.cshtml arquivo:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
  • Testar o aplicativo criando, editando e excluindo um contato

Propagar o banco de dados

Adicione a classe SeedData à pasta Dados:

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

    }
}

Chamada SeedData.Initialize 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>();
                });
    }
}

Teste se o aplicativo semeou o banco de dados. Se houver linhas no banco de dados de contato, o método de semente não será executado.

Recursos adicionais