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".
Na imagem a seguir, manager@contoso.com
está conectado e na função do gerente:
A imagem a seguir mostra a exibição de detalhes dos gerentes 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:
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:
- ASP.NET Core
- Autenticação
- Confirmação de conta e recuperação de senha
- Autorização
- Entity Framework Core
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 paracontext.Success
oucontext.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 Identity
UserManager
. - 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.
- Adicione a ID de usuário ao modelo
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 umChallengeResult
é retornado, o usuário é redirecionado para a página de entrada. - Quando o usuário é autenticado, mas não autorizado, um
ForbidResult
é retornado. Quando umForbidResult
é 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.csusing System.ComponentModel.DataAnnotations; namespace ContactManager.Models { public class Contact { public int ContactId { get; set; } public string? Name { get; set; } public string? Address { get; set; } public string? City { get; set; } public string? State { get; set; } public string? Zip { get; set; } [DataType(DataType.EmailAddress)] public string? Email { get; set; } } }
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".
Na imagem a seguir, manager@contoso.com
está conectado e na função do gerente:
A imagem a seguir mostra a exibição de detalhes dos gerentes 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:
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:
- ASP.NET Core
- Autenticação
- Confirmação de conta e recuperação de senha
- Autorização
- Entity Framework Core
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 paracontext.Success
oucontext.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 Identity
UserManager
. - 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 umChallengeResult
é retornado, o usuário é redirecionado para a página de entrada. - Quando o usuário é autenticado, mas não autorizado, um
ForbidResult
é retornado. Quando umForbidResult
é 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
- Tutorial: Criar um aplicativo ASP.NET Core e do Banco de Dados SQL do Azure no Serviço de Aplicativo do Azure
- Laboratório de autorização do ASP.NET Core. Este laboratório entra em mais detalhes sobre os recursos de segurança introduzidos neste tutorial.
- Introdução à autorização no ASP.NET Core
- Autorização baseada em política personalizada