Creación de una aplicación web de ASP.NET Core con datos de usuario protegidos por autorización
Por Rick Anderson y Joe Audette
En este tutorial se muestra cómo crear una aplicación web de ASP.NET Core con datos de usuario protegidos por autorización. Muestra una lista de contactos que han creado los usuarios autenticados (registrados). Hay tres grupos de seguridad:
- Los usuarios registrados pueden ver todos los datos aprobados y pueden editar o eliminar sus propios datos.
- Los administradores pueden aprobar o rechazar los datos de contacto. Solo los contactos aprobados son visibles para los usuarios.
- Los administradores pueden aprobar, rechazar y editar o eliminar los datos.
Las imágenes de este documento no coinciden exactamente con las plantillas más recientes.
En la imagen siguiente, el usuario Rick (rick@example.com
) ha iniciado sesión. Rick solo puede ver los contactos aprobados y Editar/Eliminar/Crear nuevos vínculos para sus contactos. Solo el último registro, creado por Rick, muestra los vínculos Editar y Eliminar. Otros usuarios no verán el último registro hasta que un administrador o responsable cambie el estado a "Aprobado".
En la imagen siguiente, manager@contoso.com
ha iniciado sesión y se encuentra en el rol del administrador:
En la imagen siguiente se muestra la vista de detalles de los administradores de un contacto:
Los botones Aprobar y Rechazar solo se muestran para administradores y responsables.
En la imagen siguiente, admin@contoso.com
ha iniciado sesión y se encuentra en el rol del administrador:
El administrador tiene todos los privilegios. Puede leer, editar o eliminar cualquier contacto y cambiar el estado de los contactos.
La aplicación se creó mediante scaffolding del modelo siguiente 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; }
}
El ejemplo contiene los siguientes controladores de autorización:
ContactIsOwnerAuthorizationHandler
: garantiza que un usuario solo pueda editar sus datos.ContactManagerAuthorizationHandler
: permite a los administradores aprobar o rechazar contactos.ContactAdministratorsAuthorizationHandler
: permite a los administradores aprobar o rechazar contactos y editar o eliminar contactos.
Prerrequisitos
Este tutorial está avanzado. Debería estar familiarizado con lo siguiente:
- ASP.NET Core
- Autenticación
- Confirmación de cuentas y recuperación de contraseñas
- Autorización
- Entity Framework Core
La aplicación de inicio y completada
Descargue la aplicación completada. Pruebe la aplicación completada para familiarizarse con sus características de seguridad.
La aplicación de inicio
Descargue la aplicación de inicio.
Ejecute la aplicación, pulse el vínculo ContactManager y compruebe que puede crear, editar y eliminar un contacto. Para crear la aplicación de inicio, consulte Creación de la aplicación de inicio.
Protección de datos de usuario
Las secciones siguientes tienen todos los pasos principales para crear la aplicación de datos de usuario segura. Es posible que le resulte útil hacer referencia al proyecto completado.
Vinculación de los datos de contacto al usuario
Use el identificador de usuario de ASP.NET Identity para asegurarse de que los usuarios pueden editar sus datos, pero no otros datos de usuarios. Agregue OwnerID
y ContactStatus
al 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
es el identificador del usuario de la tabla AspNetUser
de la base de datos de Identity. El campo Status
determina si los usuarios generales pueden ver un contacto.
Cree una nueva migración y actualice la base de datos:
dotnet ef migrations add userID_Status
dotnet ef database update
Agregar servicios de rol a Identity
Anexe AddRoles para agregar servicios de rol:
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>();
Requerir usuarios autenticados
Establezca la directiva de autorización de reserva para requerir que los usuarios se autentiquen:
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();
});
El código resaltado anterior establece la directiva de autorización de reserva. La directiva de autorización de reserva requiere que todos los usuarios se autentiquen, excepto para Razor Pages, controladores o métodos de acción con un atributo de autorización. Por ejemplo, Razor Pages, controladores o métodos de acción con [AllowAnonymous]
o [Authorize(PolicyName="MyPolicy")]
usan el atributo de autorización aplicado en lugar de la directiva de autorización de reserva.
RequireAuthenticatedUser agrega DenyAnonymousAuthorizationRequirement a la instancia actual, lo que exige que el usuario actual se autentique.
La directiva de autorización de reserva:
- Se aplica a todas las solicitudes que no especifican explícitamente una directiva de autorización. En el caso de las solicitudes atendidas por el enrutamiento de puntos de conexión, esto incluye cualquier punto de conexión que no especifique un atributo de autorización. Para las solicitudes atendidas por otro middleware después del middleware de autorización, como los archivos estáticos, esto aplica la directiva a todas las solicitudes.
Establecer la directiva de autorización de reserva para requerir que los usuarios se autentiquen protege Razor Pages y los controladores recién agregados. Tener la autorización necesaria de forma predeterminada es más seguro que confiar en los nuevos controladores y Razor Pages para incluir el atributo [Authorize]
.
La clase AuthorizationOptions también contiene AuthorizationOptions.DefaultPolicy. DefaultPolicy
es la directiva que se usa con el atributo [Authorize]
cuando no se especifica ninguna directiva. [Authorize]
no contiene una directiva con nombre, a diferencia de [Authorize(PolicyName="MyPolicy")]
.
Para obtener más información sobre las directivas, vea Autorización basada en directivas en ASP.NET Core.
Una manera alternativa de que los controladores MVC y Razor Pages requieran que todos los usuarios se autentiquen es agregar un filtro de autorización:
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();
El código anterior usa un filtro de autorización, la configuración de la directiva de reserva usa el enrutamiento de puntos de conexión. Establecer la directiva de reserva es la manera preferida de requerir que todos los usuarios se autentiquen.
Agregue AllowAnonymous a las páginas Index
y Privacy
para que los usuarios anónimos puedan obtener información sobre el sitio antes de registrarse:
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()
{
}
}
Configuración de la cuenta de prueba
La clase SeedData
crea dos cuentas: administrador y responsable. Use la Herramienta Administrador de secretos para establecer una contraseña para estas cuentas. Establezca la contraseña del directorio del proyecto (el directorio que contiene Program.cs
):
dotnet user-secrets set SeedUserPW <PW>
Si se especifica una contraseña no segura, se produce una excepción cuando se llama a SeedData.Initialize
.
Actualice la aplicación para usar la contraseña de prueba:
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);
}
Crear las cuentas de prueba y actualizar los contactos
Actualice el método Initialize
de la clase SeedData
para crear las cuentas de prueba:
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;
}
Agregue el identificador de usuario de administrador y ContactStatus
a los contactos. Haga que uno de los contactos sea "Enviados" y el otro "Rechazado". Agregue el identificador de usuario y el estado a todos los contactos. Solo se muestra un contacto:
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
},
Creación de controladores de autorización de propietario, administrador y responsable
Cree una clase ContactIsOwnerAuthorizationHandler
en la carpeta Autorización. ContactIsOwnerAuthorizationHandler
Comprueba que el usuario que actúa en un recurso posee el 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;
}
}
}
El ContactIsOwnerAuthorizationHandler
llama a context.Succeed si el usuario autenticado actual es el propietario del contacto. Los controladores de autorización generalmente:
- Llaman a
context.Succeed
cuando se cumplen los requisitos. - Devuelven
Task.CompletedTask
cuando no se cumplen los requisitos. DevolverTask.CompletedTask
sin una llamada anterior acontext.Success
ocontext.Fail
, no es correcto o incorrecto, y permite que se ejecuten otros controladores de autorización.
Si necesita producir un error explícito, llame al context.Fail.
La aplicación permite a los propietarios de contactos editar, eliminar o crear sus propios datos. ContactIsOwnerAuthorizationHandler
no es necesario comprobar la operación pasada en el parámetro de requisito.
Creación de un controlador de autorización de administrador
Cree una clase ContactManagerAuthorizationHandler
en la carpeta Autorización. El ContactManagerAuthorizationHandler
Comprueba que el usuario que actúa en el recurso es un administrador. Solo los administradores pueden aprobar o rechazar los cambios de contenido (nuevos o modificados).
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;
}
}
}
Creación de un controlador de autorización de administrador
Cree una clase ContactAdministratorsAuthorizationHandler
en la carpeta Autorización. El ContactAdministratorsAuthorizationHandler
Comprueba que el usuario que actúa en el recurso es un administrador. El administrador puede realizar todas las operaciones.
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;
}
}
}
Registro de los controladores de autorización
Los servicios que usan Entity Framework Core deben registrarse para la inserción de dependencias mediante AddScoped. El ContactIsOwnerAuthorizationHandler
usa ASP.NET Core Identity, que se basa en Entity Framework Core. Registre los controladores con la colección de servicios para que estén disponibles para el ContactsController
mediante la inserción de dependencias. Agregue el código siguiente al 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
y ContactManagerAuthorizationHandler
se agregan como singletons. Son singletons porque no usan EF y toda la información necesaria se encuentra en el parámetro Context
del método HandleRequirementAsync
.
Autorización de soporte técnico
En esta sección, actualizará Razor Pages y agregará una clase de requisitos de operaciones.
Revisar la clase de requisitos de operaciones de contacto
Revise la clase ContactOperations
. Esta clase contiene los requisitos que admite la aplicación:
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";
}
}
Crear una clase base para los contactos de Razor Pages
Cree una clase base que contenga los servicios usados en los contactos de Razor Pages. La clase base coloca el código de inicialización en una ubicación:
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;
}
}
}
El código anterior:
- Agrega el servicio
IAuthorizationService
para acceder a los controladores de autorización. - Agrega el servicio Identity
UserManager
. - Agregue la
ApplicationDbContext
.
Actualización de CreateModel
Actualice el modelo de creación de páginas:
- Constructor para usar la clase base
DI_BasePageModel
. - Método
OnPostAsync
para:- Agregue el identificador de usuario al modelo
Contact
. - Llame al controlador de autorización para comprobar que el usuario tiene permiso para crear contactos.
- Agregue el identificador de usuario al 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");
}
}
}
Actualización de IndexModel
Actualice el método OnGetAsync
para que solo los contactos aprobados se muestren a los usuarios generales:
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();
}
}
Actualización de EditModel
Agregue un controlador de autorización para comprobar que el usuario posee el contacto. Dado que se está validando la autorización de recursos, el atributo [Authorize]
no es suficiente. La aplicación no tiene acceso al recurso cuando se evalúan los atributos. La autorización basada en recursos debe ser imperativa. Las comprobaciones se deben realizar una vez que la aplicación tenga acceso al recurso, ya sea cargándola en el modelo de página o cargándola dentro del propio controlador. Para acceder con frecuencia al recurso, pase la clave 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");
}
}
Actualización de DeleteModel
Actualice el modelo de página de eliminación para usar el controlador de autorización para comprobar que el usuario tiene permiso para eliminar el contacto.
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");
}
}
Inserción del servicio de autorización en las vistas
Actualmente, la interfaz de usuario muestra los vínculos de edición y eliminación de los contactos que el usuario no puede modificar.
Inserte el servicio de autorización en el archivo Pages/_ViewImports.cshtml
para que esté disponible para todas las vistas:
@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
El marcado anterior agrega varias instrucciones using
.
Actualice los vínculos Editar y Eliminar en Pages/Contacts/Index.cshtml
para que solo se representen para los usuarios con los permisos adecuados:
@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>
Advertencia
Oculte los vínculos de los usuarios que no tienen permiso para cambiar los datos no protege la aplicación. Ocultar vínculos hace que la aplicación sea más fácil de usar mostrando solo vínculos válidos. Los usuarios pueden hackear las direcciones URL generadas para invocar operaciones de edición y eliminación en los datos que no poseen. Razor Page o el controlador deben aplicar comprobaciones de acceso para proteger los datos.
Detalles de la actualización
Actualice la vista de detalles para que los administradores puedan aprobar o rechazar contactos:
@*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>
Actualización del modelo de página de detalles
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");
}
}
Agregar o quitar un usuario a un rol
Consulte este problema para obtener información sobre:
- Quitar privilegios de un usuario. Por ejemplo, silenciar a un usuario en una aplicación de chat.
- Agregar privilegios a un usuario.
Diferencias entre Desafío y Prohibido
Esta aplicación establece la directiva predeterminada para requerir usuarios autenticados. El código siguiente permite a los usuarios anónimos. Los usuarios anónimos pueden mostrar las diferencias entre Desafío y Prohibido.
[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();
}
}
En el código anterior:
- Cuando el usuario no está autenticado, se devuelve
ChallengeResult
. Cuando se devuelve unChallengeResult
, se redirige al usuario a la página de inicio de sesión. - Cuando el usuario se autentica, pero no está autorizado, se devuelve
ForbidResult
. Cuando se devuelve unForbidResult
, se redirige al usuario a la página de acceso denegado.
Prueba de la aplicación completada
Advertencia
En este artículo se usa la herramienta Secret Manager para almacenar la contraseña de las cuentas de usuario de inicialización. La herramienta Secret Manager se utiliza para almacenar información confidencial durante el desarrollo local. Para obtener información sobre los procedimientos de autenticación que se pueden usar cuando se implementa una aplicación en un entorno de prueba o producción, consulta Flujos de autenticación seguros.
Si aún no ha establecido una contraseña para las cuentas de usuario de inicialización, use la Herramienta de Administrador de secretos para establecer una contraseña:
Elige una contraseña segura:
- Al menos 12 caracteres, pero 14 o más es mejor.
- Combinación de letras mayúsculas, minúsculas, números y símbolos.
- No es una palabra que se pueda encontrar en un diccionario o en el nombre de una persona, un carácter, un producto o una organización.
- Presenta diferencias notables con respecto a contraseñas anteriores.
- Fácil de recordar para ti, pero difícil de adivinar para los demás. Considera la posibilidad de usar una frase fácil de recordar como "6MonkeysRLooking^".
Ejecute el siguiente comando desde la carpeta del proyecto, donde
<PW>
es la contraseña:dotnet user-secrets set SeedUserPW <PW>
Si la aplicación tiene contactos:
- Elimine todos los registros de la tabla
Contact
. - Reinicie la aplicación para inicializar la base de datos.
Una manera sencilla de probar la aplicación completada es iniciar tres exploradores diferentes (o sesiones de incógnito/InPrivate). En un explorador, registre un nuevo usuario (por ejemplo, test@example.com
). Inicie sesión en cada explorador con un usuario diferente. Compruebe las siguientes operaciones:
- Los usuarios registrados pueden ver todos los datos de contacto aprobados.
- Los usuarios registrados pueden editar o eliminar sus propios datos.
- Los administradores pueden aprobar o rechazar los datos de contacto. La vista
Details
muestra los botones Aprobar y Rechazar. - Los administradores pueden aprobar, rechazar y editar o eliminar todos los datos.
Usuario | Aprobar o rechazar contactos | Opciones |
---|---|---|
test@example.com | No | Edite y elimine sus datos. |
manager@contoso.com | Sí | Edite y elimine sus datos. |
admin@contoso.com | Sí | Edite y elimine todos los datos. |
Cree un contacto en el explorador del administrador. Copie la dirección URL para eliminar y editar desde el contacto del administrador. Pegue estos vínculos en el explorador del usuario de prueba para comprobar que el usuario de prueba no puede realizar estas operaciones.
Crear la aplicación de inicio
Creación de una aplicación de Razor Pages denominada "ContactManager"
- Cree la aplicación con cuentas de usuario individuales.
- Asígnele el nombre "ContactManager" para que el espacio de nombres coincida con el espacio de nombres usado en el ejemplo.
-uld
especifica LocalDB en lugar de SQLite
dotnet new webapp -o ContactManager -au Individual -uld
Agregar
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; } } }
Aplicar scaffolding al modelo
Contact
.Creación de una migración inicial y actualización de la base de datos:
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
Nota:
De manera predeterminada, la arquitectura de los binarios .NET que se van a instalar representa la arquitectura del sistema operativo que se está ejecutando en ese momento. Para especificar una arquitectura de SO diferente, consulta dotnet tool install, --arch option. Para más información, consulte la incidencia de GitHub dotnet/AspNetCore.Docs #29262.
Actualice el delimitador ContactManager en el archivo
Pages/Shared/_Layout.cshtml
:<a class="nav-link text-dark" asp-area="" asp-page="/Contacts/Index">Contact Manager</a>
Probar la aplicación mediante la creación, edición y eliminación de un contacto
Inicializar la base de datos
Agregue la clase SeedData a la carpeta Data:
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries
namespace ContactManager.Data
{
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw="")
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
SeedDB(context, testUserPw);
}
}
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com"
},
new Contact
{
Name = "Thorsten Weinrich",
Address = "5678 1st Ave W",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "thorsten@example.com"
},
new Contact
{
Name = "Yuhong Li",
Address = "9012 State st",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "yuhong@example.com"
},
new Contact
{
Name = "Jon Orton",
Address = "3456 Maple St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "jon@example.com"
},
new Contact
{
Name = "Diliana Alexieva-Bosseva",
Address = "7890 2nd Ave E",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "diliana@example.com"
}
);
context.SaveChanges();
}
}
}
Llame a SeedData.Initialize
desde 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();
Pruebe que la aplicación ha inicializado la base de datos. Si hay filas en la base de datos de contacto, el método de inicialización no se ejecuta.
En este tutorial se muestra cómo crear una aplicación web de ASP.NET Core con datos de usuario protegidos por autorización. Muestra una lista de contactos que han creado los usuarios autenticados (registrados). Hay tres grupos de seguridad:
- Los usuarios registrados pueden ver todos los datos aprobados y pueden editar o eliminar sus propios datos.
- Los administradores pueden aprobar o rechazar los datos de contacto. Solo los contactos aprobados son visibles para los usuarios.
- Los administradores pueden aprobar, rechazar y editar o eliminar los datos.
Las imágenes de este documento no coinciden exactamente con las plantillas más recientes.
En la imagen siguiente, el usuario Rick (rick@example.com
) ha iniciado sesión. Rick solo puede ver los contactos aprobados y Editar/Eliminar/Crear nuevos vínculos para sus contactos. Solo el último registro, creado por Rick, muestra los vínculos Editar y Eliminar. Otros usuarios no verán el último registro hasta que un administrador o responsable cambie el estado a "Aprobado".
En la imagen siguiente, manager@contoso.com
ha iniciado sesión y se encuentra en el rol del administrador:
En la imagen siguiente se muestra la vista de detalles de los administradores de un contacto:
Los botones Aprobar y Rechazar solo se muestran para administradores y responsables.
En la imagen siguiente, admin@contoso.com
ha iniciado sesión y se encuentra en el rol del administrador:
El administrador tiene todos los privilegios. Puede leer, editar o eliminar cualquier contacto y cambiar el estado de los contactos.
La aplicación se creó mediante scaffolding del modelo siguiente 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; }
}
El ejemplo contiene los siguientes controladores de autorización:
ContactIsOwnerAuthorizationHandler
: garantiza que un usuario solo pueda editar sus datos.ContactManagerAuthorizationHandler
: permite a los administradores aprobar o rechazar contactos.ContactAdministratorsAuthorizationHandler
: permite a los administradores:- Aprobar o rechazar contactos
- Editar y eliminar contactos
Prerrequisitos
Este tutorial está avanzado. Debería estar familiarizado con lo siguiente:
- ASP.NET Core
- Autenticación
- Confirmación de cuentas y recuperación de contraseñas
- Autorización
- Entity Framework Core
La aplicación de inicio y completada
Descargue la aplicación completada. Pruebe la aplicación completada para familiarizarse con sus características de seguridad.
La aplicación de inicio
Descargue la aplicación de inicio.
Ejecute la aplicación, pulse el vínculo ContactManager y compruebe que puede crear, editar y eliminar un contacto. Para crear la aplicación de inicio, consulte Creación de la aplicación de inicio.
Protección de datos de usuario
Las secciones siguientes tienen todos los pasos principales para crear la aplicación de datos de usuario segura. Es posible que le resulte útil hacer referencia al proyecto completado.
Vinculación de los datos de contacto al usuario
Use el identificador de usuario de ASP.NET Identity para asegurarse de que los usuarios pueden editar sus datos, pero no otros datos de usuarios. Agregue OwnerID
y ContactStatus
al 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
es el identificador del usuario de la tabla AspNetUser
de la base de datos de Identity. El campo Status
determina si los usuarios generales pueden ver un contacto.
Cree una nueva migración y actualice la base de datos:
dotnet ef migrations add userID_Status
dotnet ef database update
Agregar servicios de rol a Identity
Anexe AddRoles para agregar servicios de rol:
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>();
Requerir usuarios autenticados
Establecer la directiva de autenticación de reserva para requerir que los usuarios se autentiquen:
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();
});
El código resaltado anterior establece la directiva de autenticación de reserva. La directiva de autenticación de reserva requiere que todos los usuarios se autentiquen, excepto para Razor Pages, controladores o métodos de acción con un atributo de autenticación. Por ejemplo, Razor Pages, los controladores o métodos de acción con [AllowAnonymous]
o [Authorize(PolicyName="MyPolicy")]
usan el atributo de autenticación aplicado en lugar de la directiva de autenticación de reserva.
RequireAuthenticatedUser agrega DenyAnonymousAuthorizationRequirement a la instancia actual, lo que exige que el usuario actual se autentique.
La directiva de autenticación de reserva:
- Se aplica a todas las solicitudes que no especifican explícitamente una directiva de autenticación. En el caso de las solicitudes atendidas por el enrutamiento de puntos de conexión, esto incluiría cualquier punto de conexión que no especifique un atributo de autorización. Para las solicitudes atendidas por otro middleware después del middleware de autorización, como los archivos estáticos, esto aplicaría la directiva a todas las solicitudes.
Establecer la directiva de autenticación de reserva para requerir que los usuarios se autentiquen protege a Razor Pages y los controladores recién agregados. Tener la autenticación necesaria de forma predeterminada es más seguro que confiar en los nuevos controladores y Razor Pages para incluir el atributo [Authorize]
.
La clase AuthorizationOptions también contiene AuthorizationOptions.DefaultPolicy. DefaultPolicy
es la directiva que se usa con el atributo [Authorize]
cuando no se especifica ninguna directiva. [Authorize]
no contiene una directiva con nombre, a diferencia de [Authorize(PolicyName="MyPolicy")]
.
Para obtener más información sobre las directivas, vea Autorización basada en directivas en ASP.NET Core.
Una manera alternativa de que los controladores MVC y Razor Pages requieran que todos los usuarios se autentiquen es agregar un filtro de autorización:
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));
});
El código anterior usa un filtro de autorización, la configuración de la directiva de reserva usa el enrutamiento de puntos de conexión. Establecer la directiva de reserva es la manera preferida de requerir que todos los usuarios se autentiquen.
Agregue AllowAnonymous a las páginas Index
y Privacy
para que los usuarios anónimos puedan obtener información sobre el sitio antes de registrarse:
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()
{
}
}
}
Configuración de la cuenta de prueba
La clase SeedData
crea dos cuentas: administrador y responsable. Use la Herramienta Administrador de secretos para establecer una contraseña para estas cuentas. Establezca la contraseña del directorio del proyecto (el directorio que contiene Program.cs
):
dotnet user-secrets set SeedUserPW <PW>
Si no se especifica una contraseña segura, se produce una excepción cuando se llama a SeedData.Initialize
.
Actualice Main
para usar la contraseña de prueba:
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>();
});
}
Crear las cuentas de prueba y actualizar los contactos
Actualice el método Initialize
de la clase SeedData
para crear las cuentas de prueba:
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;
}
Agregue el identificador de usuario de administrador y ContactStatus
a los contactos. Haga que uno de los contactos sea "Enviados" y el otro "Rechazado". Agregue el identificador de usuario y el estado a todos los contactos. Solo se muestra un contacto:
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
},
Creación de controladores de autorización de propietario, administrador y responsable
Cree una clase ContactIsOwnerAuthorizationHandler
en la carpeta Autorización. ContactIsOwnerAuthorizationHandler
Comprueba que el usuario que actúa en un recurso posee el 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;
}
}
}
El ContactIsOwnerAuthorizationHandler
llama a context.Succeed si el usuario autenticado actual es el propietario del contacto. Los controladores de autorización generalmente:
- Llaman a
context.Succeed
cuando se cumplen los requisitos. - Devuelven
Task.CompletedTask
cuando no se cumplen los requisitos. DevolverTask.CompletedTask
sin una llamada anterior acontext.Success
ocontext.Fail
, no es correcto o incorrecto, y permite que se ejecuten otros controladores de autorización.
Si necesita producir un error explícito, llame al context.Fail.
La aplicación permite a los propietarios de contactos editar, eliminar o crear sus propios datos. ContactIsOwnerAuthorizationHandler
no es necesario comprobar la operación pasada en el parámetro de requisito.
Creación de un controlador de autorización de administrador
Cree una clase ContactManagerAuthorizationHandler
en la carpeta Autorización. El ContactManagerAuthorizationHandler
Comprueba que el usuario que actúa en el recurso es un administrador. Solo los administradores pueden aprobar o rechazar los cambios de contenido (nuevos o modificados).
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;
}
}
}
Creación de un controlador de autorización de administrador
Cree una clase ContactAdministratorsAuthorizationHandler
en la carpeta Autorización. El ContactAdministratorsAuthorizationHandler
Comprueba que el usuario que actúa en el recurso es un administrador. El administrador puede realizar todas las operaciones.
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;
}
}
}
Registro de los controladores de autorización
Los servicios que usan Entity Framework Core deben registrarse para la inserción de dependencias mediante AddScoped. El ContactIsOwnerAuthorizationHandler
usa ASP.NET Core Identity, que se basa en Entity Framework Core. Registre los controladores con la colección de servicios para que estén disponibles para el ContactsController
mediante la inserción de dependencias. Agregue el código siguiente al 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
y ContactManagerAuthorizationHandler
se agregan como singletons. Son singletons porque no usan EF y toda la información necesaria se encuentra en el parámetro Context
del método HandleRequirementAsync
.
Autorización de soporte técnico
En esta sección, actualizará Razor Pages y agregará una clase de requisitos de operaciones.
Revisar la clase de requisitos de operaciones de contacto
Revise la clase ContactOperations
. Esta clase contiene los requisitos que admite la aplicación:
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";
}
}
Crear una clase base para los contactos de Razor Pages
Cree una clase base que contenga los servicios usados en los contactos de Razor Pages. La clase base coloca el código de inicialización en una ubicación:
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;
}
}
}
El código anterior:
- Agrega el servicio
IAuthorizationService
para acceder a los controladores de autorización. - Agrega el servicio Identity
UserManager
. - Agregue la
ApplicationDbContext
.
Actualización de CreateModel
Actualice el constructor del modelo de página de creación para usar la clase base DI_BasePageModel
:
public class CreateModel : DI_BasePageModel
{
public CreateModel(
ApplicationDbContext context,
IAuthorizationService authorizationService,
UserManager<IdentityUser> userManager)
: base(context, authorizationService, userManager)
{
}
Actualice el método CreateModel.OnPostAsync
a:
- Agregue el identificador de usuario al modelo
Contact
. - Llame al controlador de autorización para comprobar que el usuario tiene permiso para crear contactos.
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");
}
Actualización de IndexModel
Actualice el método OnGetAsync
para que solo los contactos aprobados se muestren a los usuarios generales:
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();
}
}
Actualización de EditModel
Agregue un controlador de autorización para comprobar que el usuario posee el contacto. Dado que se está validando la autorización de recursos, el atributo [Authorize]
no es suficiente. La aplicación no tiene acceso al recurso cuando se evalúan los atributos. La autorización basada en recursos debe ser imperativa. Las comprobaciones se deben realizar una vez que la aplicación tenga acceso al recurso, ya sea cargándola en el modelo de página o cargándola dentro del propio controlador. Para acceder con frecuencia al recurso, pase la clave 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");
}
}
Actualización de DeleteModel
Actualice el modelo de página de eliminación para usar el controlador de autorización para comprobar que el usuario tiene permiso para eliminar el contacto.
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");
}
}
Inserción del servicio de autorización en las vistas
Actualmente, la interfaz de usuario muestra los vínculos de edición y eliminación de los contactos que el usuario no puede modificar.
Inserte el servicio de autorización en el archivo Pages/_ViewImports.cshtml
para que esté disponible para todas las vistas:
@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
El marcado anterior agrega varias instrucciones using
.
Actualice los vínculos Editar y Eliminar en Pages/Contacts/Index.cshtml
para que solo se representen para los usuarios con los permisos adecuados:
@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>
Advertencia
Oculte los vínculos de los usuarios que no tienen permiso para cambiar los datos no protege la aplicación. Ocultar vínculos hace que la aplicación sea más fácil de usar mostrando solo vínculos válidos. Los usuarios pueden hackear las direcciones URL generadas para invocar operaciones de edición y eliminación en los datos que no poseen. Razor Page o el controlador deben aplicar comprobaciones de acceso para proteger los datos.
Detalles de la actualización
Actualice la vista de detalles para que los administradores puedan aprobar o rechazar contactos:
@*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>
Actualice el modelo de página de detalles:
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");
}
}
Agregar o quitar un usuario a un rol
Consulte este problema para obtener información sobre:
- Quitar privilegios de un usuario. Por ejemplo, silenciar a un usuario en una aplicación de chat.
- Agregar privilegios a un usuario.
Diferencias entre Desafío y Prohibido
Esta aplicación establece la directiva predeterminada para requerir usuarios autenticados. El código siguiente permite a los usuarios anónimos. Los usuarios anónimos pueden mostrar las diferencias entre Desafío y Prohibido.
[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();
}
}
En el código anterior:
- Cuando el usuario no está autenticado, se devuelve
ChallengeResult
. Cuando se devuelve unChallengeResult
, se redirige al usuario a la página de inicio de sesión. - Cuando el usuario se autentica, pero no está autorizado, se devuelve
ForbidResult
. Cuando se devuelve unForbidResult
, se redirige al usuario a la página de acceso denegado.
Prueba de la aplicación completada
Si aún no ha establecido una contraseña para las cuentas de usuario de inicialización, use la Herramienta de Administrador de secretos para establecer una contraseña:
Elija una contraseña segura: use ocho o más caracteres y al menos un carácter de mayúscula, un número y un símbolo. Por ejemplo,
Passw0rd!
cumple los requisitos de contraseña segura.Ejecute el siguiente comando desde la carpeta del proyecto, donde
<PW>
es la contraseña:dotnet user-secrets set SeedUserPW <PW>
Si la aplicación tiene contactos:
- Elimine todos los registros de la tabla
Contact
. - Reinicie la aplicación para inicializar la base de datos.
Una manera sencilla de probar la aplicación completada es iniciar tres exploradores diferentes (o sesiones de incógnito/InPrivate). En un explorador, registre un nuevo usuario (por ejemplo, test@example.com
). Inicie sesión en cada explorador con un usuario diferente. Compruebe las siguientes operaciones:
- Los usuarios registrados pueden ver todos los datos de contacto aprobados.
- Los usuarios registrados pueden editar o eliminar sus propios datos.
- Los administradores pueden aprobar o rechazar los datos de contacto. La vista
Details
muestra los botones Aprobar y Rechazar. - Los administradores pueden aprobar, rechazar y editar o eliminar todos los datos.
Usuario | Inicialización por la aplicación | Opciones |
---|---|---|
test@example.com | No | Editar o eliminar los propios datos. |
manager@contoso.com | Sí | Apruebe, rechace y edite o elimine datos propios. |
admin@contoso.com | Sí | Apruebe, rechace y edite o elimine todos los datos. |
Cree un contacto en el explorador del administrador. Copie la dirección URL para eliminar y editar desde el contacto del administrador. Pegue estos vínculos en el explorador del usuario de prueba para comprobar que el usuario de prueba no puede realizar estas operaciones.
Crear la aplicación de inicio
Creación de una aplicación de Razor Pages denominada "ContactManager"
- Cree la aplicación con cuentas de usuario individuales.
- Asígnele el nombre "ContactManager" para que el espacio de nombres coincida con el espacio de nombres usado en el ejemplo.
-uld
especifica LocalDB en lugar de SQLite
dotnet new webapp -o ContactManager -au Individual -uld
Agregue
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; } }
Aplicar scaffolding al modelo
Contact
.Creación de una migración inicial y actualización de la base de datos:
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
Nota:
De manera predeterminada, la arquitectura de los binarios .NET que se van a instalar representa la arquitectura del sistema operativo que se está ejecutando en ese momento. Para especificar una arquitectura de SO diferente, consulta dotnet tool install, --arch option. Para más información, consulte la incidencia de GitHub dotnet/AspNetCore.Docs #29262.
Si experimenta un error con el comando dotnet aspnet-codegenerator razorpage
, consulte este problema de GitHub.
- Actualice el delimitador ContactManager en el archivo
Pages/Shared/_Layout.cshtml
:
<a class="navbar-brand" asp-area="" asp-page="/Contacts/Index">ContactManager</a>
- Probar la aplicación mediante la creación, edición y eliminación de un contacto
Inicializar la base de datos
Agregue la clase SeedData a la carpeta Data:
using ContactManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
// dotnet aspnet-codegenerator razorpage -m Contact -dc ApplicationDbContext -udl -outDir Pages\Contacts --referenceScriptLibraries
namespace ContactManager.Data
{
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
{
using (var context = new ApplicationDbContext(
serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
{
SeedDB(context, "0");
}
}
public static void SeedDB(ApplicationDbContext context, string adminID)
{
if (context.Contact.Any())
{
return; // DB has been seeded
}
context.Contact.AddRange(
new Contact
{
Name = "Debra Garcia",
Address = "1234 Main St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "debra@example.com"
},
new Contact
{
Name = "Thorsten Weinrich",
Address = "5678 1st Ave W",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "thorsten@example.com"
},
new Contact
{
Name = "Yuhong Li",
Address = "9012 State st",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "yuhong@example.com"
},
new Contact
{
Name = "Jon Orton",
Address = "3456 Maple St",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "jon@example.com"
},
new Contact
{
Name = "Diliana Alexieva-Bosseva",
Address = "7890 2nd Ave E",
City = "Redmond",
State = "WA",
Zip = "10999",
Email = "diliana@example.com"
}
);
context.SaveChanges();
}
}
}
Llame a SeedData.Initialize
desde 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>();
});
}
}
Pruebe que la aplicación ha inicializado la base de datos. Si hay filas en la base de datos de contacto, el método de inicialización no se ejecuta.
Recursos adicionales
- Tutorial: Compilación de una aplicación ASP.NET Core y Azure SQL Database en Azure App Service
- Laboratorio de autorización de ASP.NET Core. Este laboratorio incluye más detalles sobre las características de seguridad introducidas en este tutorial.
- Introducción a la autorización en ASP.NET Core
- Autorización personalizada basada en directivas