Proteger las aplicaciones mediante la autenticación y la autorización
por Microsoft
Este es el paso 9 de un tutorial de la aplicación "NerdDinner" gratuito que le guía durante el proceso de compilación de una aplicación web pequeña, pero completa, con ASP.NET MVC 1.
El paso 9 muestra cómo agregar autenticación y autorización para proteger la aplicación NerdDinner, de modo que los usuarios tengan que registrarse e iniciar sesión en el sitio web para crear nuevas cenas, y que solo el usuario que es el anfitrión de una cena puede editarla más adelante.
Si utiliza ASP.NET MVC 3, le recomendamos que siga los tutoriales Introducción a MVC 3 o Tienda de música de MVC.
Paso 9 de NerdDinner: Autenticación y autorización
Ahora mismo, la aplicación NerdDinner concede a cualquier persona que visite el sitio web la capacidad de crear y editar los detalles de cualquier cena. Vamos a cambiar esto de modo que los usuarios tengan que registrarse e iniciar sesión en el sitio web para crear nuevas cenas y agregar una restricción, y que solo el usuario que es el anfitrión de una cena pueda editarla más adelante.
Para habilitar esto, vamos a utilizar la autenticación y autorización para proteger la aplicación.
Entender la autenticación y autorización
La autenticación es el proceso de identificación y validación de la identidad de un cliente que accede a una aplicación. En pocas palabras, se trata de identificar "quién" es el usuario final cuando visita un sitio web. ASP.NET admite varias maneras de autenticar a los usuarios de un explorador. Para las aplicaciones web de Internet, el enfoque de autenticación más común que se utiliza se denomina "Autenticación de formularios". La autenticación de formularios permite a un desarrollador crear un formulario de inicio de sesión HTML dentro de su aplicación y validar después el nombre de usuario o la contraseña que envía un usuario final con una base de datos u otro almacén de credenciales de contraseña. Si la combinación de nombre de usuario y contraseña es correcta, el desarrollador puede pedir a ASP.NET que emita una cookie HTTP cifrada para identificar al usuario en futuras solicitudes. Vamos a utilizar la autenticación de formularios con la aplicación NerdDinner.
La autorización es el proceso para determinar si un usuario tiene permiso para acceder a un recurso o una dirección URL determinados o realizar alguna acción. Por ejemplo, en la aplicación NerdDinner, queremos autorizar que solo los usuarios que inicien sesión puedan acceder a la dirección URL /Dinners/Create y crear cenas nuevas. También queremos agregar lógica de autorización para que solo el usuario que es el anfitrión de una cena pueda editarla y denegar el acceso de edición a todos los demás usuarios.
Autenticación de formularios y AccountController
La plantilla de proyecto predeterminada de Visual Studio para ASP.NET MVC habilita automáticamente la autenticación de formularios cuando se crean aplicaciones ASP.NET MVC nuevas. También agrega automáticamente una implementación de página de inicio de sesión de cuenta precompilada al proyecto, lo que facilita la integración de la seguridad dentro de un sitio web.
La página Site.master predeterminada muestra un vínculo "Iniciar sesión" en la parte superior derecha del sitio cuando el usuario que accede a él no está autenticado:
Al hacer clic en el vínculo "Iniciar sesión", se dirige al usuario a la dirección URL /Account/LogOn:
Los visitantes que no se hayan registrado aún tienen que hacer clic en el vínculo "Registrar", que les llevará a la dirección URL /Account/Register y les permitirá especificar los detalles de la cuenta:
Al hacer clic en el botón "Registrar", se creará un nuevo usuario en el sistema de pertenencia a ASP.NET y se le autenticará en el sitio mediante la autenticación de formularios.
Cuando un usuario inicia sesión, Site.master cambia la parte superior derecha de la página donde aparece un mensaje "¡Bienvenido [nombredeusuario]!" y muestra un vínculo "Cerrar sesión" en lugar de "Iniciar sesión". Al hacer clic en el vínculo "Cerrar sesión", se cierra la sesión del usuario:
La funcionalidad anterior de inicio de sesión, cierre de sesión y registro se implementa dentro de la clase AccountController que Visual Studio agregó al proyecto cuando lo creó. La interfaz de usuario de AccountController se implementa mediante plantillas de vista dentro del directorio \Views\Account:
La clase AccountController usa el sistema de autenticación de formularios de ASP.NET para emitir cookies de autenticación cifradas y la API de pertenencia a ASP.NET para almacenar y validar nombres de usuario y contraseñas. La API de pertenencia a ASP.NET es extensible y permite usar cualquier almacén de credenciales de contraseñas. ASP.NET incluye implementaciones integradas de proveedores de pertenencia que almacenan los nombres de usuario o las contraseñas dentro de una base de datos SQL o en Active Directory.
Podemos configurar qué proveedor de pertenencia debe usar la aplicación NerdDinner abriendo el archivo "web.config" en la raíz del proyecto y buscando la sección de <pertenencia> dentro de él. El archivo web.config predeterminado agregado al crear el proyecto registra el proveedor de pertenencia a SQL y lo configura para usar una cadena de conexión denominada "ApplicationServices" a fin de especificar la ubicación de la base de datos.
La cadena de conexión predeterminada "ApplicationServices" (que se especifica en la sección <connectionStrings> del archivo web.config) está configurada para utilizar SQL Express. Apunta a una base de datos de SQL Express denominada "ASPNETDB. MDF" en el directorio "App_Data" de la aplicación. Si esta base de datos no existe la primera vez que se utiliza la API de pertenencia en la aplicación, ASP.NET creará automáticamente la base de datos y aprovisionará el esquema de base de datos de pertenencia adecuado dentro de ella:
Si, en lugar de usar SQL Express, quisiéramos utilizar una instancia completa de SQL Server (o conectarnos a una base de datos remota), todo lo que tendríamos que hacer es actualizar la cadena de conexión "ApplicationServices" dentro del archivo web.config y asegurarnos de que se haya agregado el esquema de pertenencia adecuado a la base de datos a la que apunta. Puede ejecutar la utilidad "aspnet_regsql.exe" en el directorio \Windows\Microsoft.NET\Framework\v2.0.50727\ para agregar el esquema adecuado a la pertenencia y los demás servicios de aplicaciones ASP.NET a una base de datos.
Autorización de la dirección URL /Dinners/Create mediante el filtro [Authorize]
No hemos tenido que escribir ningún código para habilitar una implementación segura de autenticación y administración de cuentas para la aplicación NerdDinner. Los usuarios pueden registrar nuevas cuentas en nuestra aplicación e iniciar o cerrar sesión en el sitio.
Ahora podemos agregar lógica de autorización a la aplicación y usar el estado de autenticación y el nombre de usuario de los visitantes para controlar lo que pueden y no pueden hacer en el sitio. Comencemos por agregar lógica de autorización a los métodos de acción "Create" de la clase DinnersController. En concreto, es necesario que los usuarios que accedan a la dirección URL /Dinners/Create hayan iniciado sesión. Si no lo han hecho aún, les redirigiremos a la página de inicio de sesión para que puedan hacerlo.
La implementación de esta lógica resulta bastante fácil. Todo lo que necesitamos hacer es agregar un atributo de filtro [Authorize] a los métodos de acción Create de la siguiente manera:
//
// GET: /Dinners/Create
[Authorize]
public ActionResult Create() {
...
}
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(Dinner dinnerToCreate) {
...
}
ASP.NET MVC admite la capacidad de crear "filtros de acción" que se pueden usar para implementar una lógica reutilizable, aplicable mediante declaración a los métodos de acción. El filtro [Authorize] es uno de los filtros de acción integrados proporcionados por ASP.NET MVC y permite al desarrollador aplicar mediante declaración reglas de autorización a métodos de acción y clases de controlador.
Cuando se aplica sin ningún parámetro (tal como aparece anteriormente), el filtro [Authorize] exige que el usuario que realiza la solicitud del método de acción haya iniciado sesión y redirigirá automáticamente el explorador a la dirección URL de inicio de sesión en caso de que no lo haya hecho aún. Al realizar esta redirección, la dirección URL solicitada originalmente se pasa como un argumento querystring (por ejemplo, /Account/LogOn?ReturnUrl=%2fDinners%2fCreate). A continuación, AccountController redirigirá al usuario a la dirección URL solicitada originalmente una vez que haya iniciado sesión.
El filtro [Authorize] admite opcionalmente la capacidad de especificar una propiedad "Users" o "Roles" que se puede utilizar para exigir tanto que el usuario haya iniciado sesión como que esté en una lista de usuarios permitidos o sea miembro de un rol de seguridad permitido. Por ejemplo, el código siguiente permite que solo dos usuarios concretos, "scottgu" y "billg", accedan a la dirección URL /Dinners/Create:
[Authorize(Users="scottgu,billg")]
public ActionResult Create() {
...
}
La inserción de nombres de usuario específicos dentro del código tiende a ser bastante difícil de mantener. Un mejor enfoque consiste en definir "roles" de nivel superior en los que se comprueba el código y asignar luego usuarios al rol mediante una base de datos o un sistema de Active Directory (lo que permite almacenar la lista de asignación de usuarios real de forma externa desde el código). ASP.NET incluye una API de administración de roles integrada así como un conjunto integrado de proveedores de roles (incluidos los de SQL y Active Directory) que pueden ayudar a realizar esta asignación de roles o usuarios. Después, podríamos actualizar el código para permitir que solo los usuarios de un rol "admin" específico accedan a la dirección URL /Dinners/Create:
[Authorize(Roles="admin")]
public ActionResult Create() {
...
}
Uso de la propiedad User.Identity.Name para crear cenas
Podemos recuperar el nombre del usuario que ha iniciado sesión actualmente de una solicitud mediante la propiedad User.Identity.Name expuesta en la clase base Controller.
Anteriormente, cuando implementamos la versión HTTP-POST del método de acción Create(), codificamos de forma rígida la propiedad "HostedBy" de la cena en una cadena estática. Ahora podemos actualizar este código para usar la propiedad User.Identity.Name, así como agregar automáticamente un RSVP para el anfitrión que crea la cena:
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(Dinner dinner) {
if (ModelState.IsValid) {
try {
dinner.HostedBy = User.Identity.Name;
RSVP rsvp = new RSVP();
rsvp.AttendeeName = User.Identity.Name;
dinner.RSVPs.Add(rsvp);
dinnerRepository.Add(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
}
}
return View(new DinnerFormViewModel(dinner));
}
Como hemos agregado un atributo [Authorize] al método Create(), ASP.NET MVC garantiza que el método de acción solo se ejecute si el usuario que visita la dirección URL /Dinners/Create ha iniciado sesión en el sitio. Por lo tanto, el valor de la propiedad User.Identity.Name siempre contendrá un nombre de usuario válido.
Uso de la propiedad User.Identity.Name para editar cenas
Ahora vamos a agregar cierta lógica de autorización que restrinja a los usuarios para que solo puedan editar las propiedades de las cenas sus propios anfitriones.
Para ello, primero vamos a agregar un método de asistente "IsHostedBy(username)" al objeto Dinner (dentro de la clase parcial Dinner.cs que compilamos anteriormente). Este método de asistente devuelve true o false en función de si un nombre de usuario proporcionado coincide con la propiedad Dinner HostedBy y encapsula la lógica necesaria para realizar una comparación de cadenas que no distingue mayúsculas de minúsculas:
public partial class Dinner {
public bool IsHostedBy(string userName) {
return HostedBy.Equals(userName, StringComparison.InvariantCultureIgnoreCase);
}
}
A continuación, agregaremos un atributo [Authorize] a los métodos de acción Edit() dentro de la clase DinnersController. Se garantizará así que los usuarios deban iniciar sesión para solicitar una dirección URL /Dinners/Edit/[id].
A continuación, podemos agregar código a los métodos Edit que utilizan el método de asistente Dinner.IsHostedBy(username) para comprobar que el usuario que ha iniciado sesión coincide con el anfitrión de la cena. Si el usuario no es el anfitrión, mostraremos una vista "InvalidOwner" y pondremos fin a la solicitud. El código para hacer esto tiene el siguiente aspecto:
//
// GET: /Dinners/Edit/5
[Authorize]
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (!dinner.IsHostedBy(User.Identity.Name))
return View("InvalidOwner");
return View(new DinnerFormViewModel(dinner));
}
//
// POST: /Dinners/Edit/5
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit(int id, FormCollection collection) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (!dinner.IsHostedBy(User.Identity.Name))
return View("InvalidOwner");
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new {id = dinner.DinnerID});
}
catch {
ModelState.AddModelErrors(dinnerToEdit.GetRuleViolations());
return View(new DinnerFormViewModel(dinner));
}
}
Luego podemos hacer clic con el botón derecho en el directorio \Views\Dinners y elegir el comando de menú Add->View para crear una nueva vista "InvalidOwner". Lo rellenaremos con el siguiente mensaje de error:
<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
You Don't Own This Dinner
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">
<h2>Error Accessing Dinner</h2>
<p>Sorry - but only the host of a Dinner can edit or delete it.</p>
</asp:Content>
Y ahora cuando un usuario intente editar una cena de la que no es propietario, recibirá un mensaje de error:
Podemos repetir los mismos pasos para los métodos de acción Delete() dentro de nuestro controlador a fin de bloquear también el permiso para eliminar cenas y asegurarnos de que solo el anfitrión de una cena pueda eliminarla.
Visualización u ocultación de vínculos Edit y Delete
Nos estamos vinculando al método de acción Edit y Delete de la clase DinnersController desde nuestra dirección URL de detalles:
Actualmente se muestran los vínculos de acción Edit y Delete, independientemente de si el visitante de la dirección URL de detalles es el anfitrión de la cena. Vamos a modificarlo para que los vínculos solo se muestren si el usuario que visita es el propietario de la cena.
El método de acción Details() dentro de DinnersController recupera un objeto Dinner y, a continuación, lo pasa como objeto de modelo a la plantilla de vista:
//
// GET: /Dinners/Details/5
public ActionResult Details(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (dinner == null)
return View("NotFound");
return View(dinner);
}
Podemos actualizar la plantilla de vista para mostrar u ocultar condicionalmente los vínculos Edit y Delete mediante el método de asistente Dinner.IsHostedBy() tal como se indica a continuación:
<% if (Model.IsHostedBy(Context.User.Identity.Name)) { %>
<%= Html.ActionLink("Edit Dinner", "Edit", new { id=Model.DinnerID }) %> |
<%= Html.ActionLink("Delete Dinner", "Delete", new {id=Model.DinnerID}) %>
<% } %>
Pasos siguientes
Veamos ahora cómo podemos habilitar usuarios autenticados en RSVP para cenas mediante AJAX.