Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 25 de septiembre de 2018
ASP.NET Core MVC Hace unos días, un alumno del curso online de ASP.NET Core en CampusMVP (por cierto, desde hace unas semanas actualizado a la versión 2.1) me planteaba una cuestión que quizás podría interesar a alguien más, por lo que vamos a comentarla por aquí.

Básicamente, la duda era la siguiente:
¿Hay alguna forma sencilla de añadir el atributo [Authorize], pero sólo a los controladores que se encuentren en una carpeta determinada del proyecto, sin tener que aplicarlo uno a uno?
La respuesta rápida es: sí. Bueno, con matices ;) Incluso hay varias opciones, así que vamos a ver algunas de ellas, y de paso repasamos conceptos de ASP.NET Core MVC :)

Opción 1: Usar clases base

La primera forma que se me ocurre, y quizás la más sencilla, es hacer los controladores presentes en la carpeta hereden de una clase base y aplicar los filtros sobre ella, de forma que todos sus descendientes se vean afectados.
[Authorize(Roles="admin")]
public abstract class AdminController: Controller
{
}

...

// En Controllers/DashboardController.cs
public class DashboardController: AdminController
{
    // Esto ya estará protegido 
    // por el [Authorize] de AdminController
}
Ya, me diréis que recorrer todas las clases que nos interesen y hacer que hereden de AdminController nos llevará el mismo tiempo que aplicarles directamente el atributo [Authorize], y es cierto. Sin embargo, si más adelante tuviéramos que modificar los criterios de admisión para estos controladores (por ejemplo, añadir otros roles o policies), sólo tendríamos que tocar en este punto del código.

Opción 2: Usar atributos personalizados

Otra posibilidad sería crear un authorization filter personalizado e introducirlo como filtro global de la aplicación.

El siguiente código muestra un filtro que, establecido de forma global, observaría todas las peticiones entrantes y comprobaría que el usuario esté logado y ostente el rol "admin", pero sólo en aquellas acciones cuyos controladores hayan sido definidos en el espacio de nombres que comience por "MyApp.Controllers.Admin":
public class AdminAuthorizeAttribute : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var actionName = context.ActionDescriptor.DisplayName;
        if(actionName.StartsWith("MyApp.Controllers.Admin."))
        {
            var user = context.HttpContext.User;
            if (!user.Identity.IsAuthenticated || !user.IsInRole("admin")) {
                context.Result = new ForbidResult();
            }
        }
    }
}
Obviamente, no estamos teniendo en cuenta las carpetas físicas en las que se encuentran los controladores, puesto que son simplemente una forma de organizar el código fuente, sino sus namespaces, que es la forma en que se organizan los componentes.
Con esto, ya sólo habría que establecer el filtro como global, algo que conseguimos retocando ligeramente el método ConfigureServices() de la clase Startup:
public void ConfigureServices(IServiceCollection services)
{
    [...]
    services.AddMvc(opt =>
    {
        opt.Filters.Add(new AdminAuthorizeAttribute());
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
Fijaos que, quizás de una forma extraña, pero acabamos de definir una convención en nuestra aplicación que podríamos definir así: "todos los controladores del espacio de nombres MyApp.Controllers.Admin" deben estar decorados con el atributo [Authorize]".

Hey, ¿y no traía MVC Core algo para facilitar la implementación de convenciones? Pues sí, y ya hablamos de ello hace bastante tiempo...

Opción 3: Usar convenciones personalizadas

Si echáis un vistazo al enlace anterior, seguro que veis rápidamente que en esta feature del framework MVC es donde tenemos la solución más elegante a nuestro problema.

Básicamente, la idea consiste en introducir código que modifique el ApplicationModel, una estructura en memoria creada por ASP.NET Core MVC en el momento del arranque, que describe todas las "piezas" que componen la aplicación: controladores, acciones, filtros, parámetros, rutas, constraints, etc.

El código que modifica el modelo de aplicación es lo que en el framework llamamos una convención. Por ejemplo, podríamos crear una convención que hiciera que las acciones cuyo nombre acabe por "Post" sean sólo accesibles mediante el verbo HTTP post, o añadir un filtro que compruebe el token JWT a todos los controladores que hereden de una clase determinada, o modificar las rutas en función de cualquier criterio... En definitiva, podemos hacer casi cualquier cosa :)

Suena complejo, ¿verdad? Pues nada más lejos de la realidad. En la práctica, estas convenciones son clases que implementan las interfaces IApplicationModelConvention, IControllerModelConvention, IActionModelConvention o IParameterModelConvention, dependiendo del ámbito en el que vayamos a aplicar nuestra convención.

En nuestro caso, dado que queremos aplicar una convención a los controladores, la interfaz que nos interesa es IControllerModelConvention, cuya signatura es la siguiente:
public interface IControllerModelConvention
{
    void Apply(ControllerModel controller);
}
Durante el arranque de la aplicación ASP.NET Core MVC irá invocando sucesivamente este método por cada controlador de nuestra aplicación, enviándonos en el objeto ControllerModel toda la información disponible sobre el mismo. Y, como comentaba antes, lo mejor es que gran parte de esta información puede ser modificada al vuelo.

El siguiente código muestra la convención AdminControllersAreOnlyForAdminsConvention que añade automáticamente el filtro Authorize, configurado apropiadamente, a todos los controladores cuyo espacio de nombres sea el que nos interesa:
public class AdminControllersAreOnlyForAdminsConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        if (controller.DisplayName.StartsWith("MyApp.Controllers.Admin."))
        {
            var policy = new AuthorizationPolicyBuilder()
                            .RequireRole("admin")
                            .Build();
            controller.Filters.Add(new AuthorizeFilter(policy));
        }
    }
}
Para que sea tenida en cuenta, esta convención se debe añadir al registro de servicios de la clase de inicialización Startup como una opción de configuración del _framework MVC:
services.AddMvc(opt =>
{
    opt.Conventions.Add(new AdminControllersAreOnlyForAdminsConvention());
});
Y de esta forma tan sencilla, de nuevo hemos llegado a nuestro objetivo :)

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!