ASP.NET Core ofrece ofertas estándar para configurar el acceso a la API utilizando atributos, es posible restringir el acceso a los usuarios con un cierto reclamo, puede definir políticas y vincular a los controladores, creando controladores para diferentes roles
Este sistema tiene desventajas, la mayor en eso, mirando este atributo:
[Authorize(Roles = "Administrator")] public class AdministrationController : Controller { }
No recibimos ninguna información sobre los derechos que tiene el administrador.
Mi tarea es mostrar todos los usuarios prohibidos para este mes (no solo ir a la base de datos y filtrar, hay ciertas reglas de conteo que se encuentran en alguna parte), hago CTRL + N en el proyecto y busco BannedUserHandler o IHasInfoAbounBannedUser o GetBannedUsersForAdmin .
Encuentro los controladores marcados con el atributo [Autorizar (Roles = "Administrador")] , puede haber dos escenarios:
Hacemos todo en el controlador
[Route("api/[controller]/[action]")] public class AdminInfoController1 : ControllerBase { private readonly IGetUserInfoService _getInfoAboutActiveUsers; private readonly ICanBanUserService _banUserService; private readonly ICanRemoveBanUserService _removeBanUserService;
Distribuimos en manejadores
[Route("api/[controller]/[action]")] public class AdminInfoController2 : ControllerBase { [HttpPatch("{id}")] public async Task<ActionResult<BanUserResult>> BanUser( [FromServices] IAsyncHandler<UserId, BanUserResult> handler, UserId userId) => await handler.Handle(userId, HttpContext.RequestAborted); [HttpPatch("{id}")] public async Task<ActionResult<RemoveBanUserResult>> RemoveBanUser( [FromServices] IAsyncHandler<UserId, RemoveBanUserResult> handler, UserId userId) => await handler.Handle(userId, HttpContext.RequestAborted); }
El primer enfoque no es malo, ya que sabemos el acceso a los recursos que tiene el administrador, las dependencias que puede usar, usaría este enfoque en aplicaciones pequeñas, sin un área temática compleja
El segundo no habla tanto, todas las dependencias se resuelven en los controladores, no puedo mirar al constructor y entender qué tipo de dependencia necesito, este enfoque se justifica cuando la aplicación es compleja y los controladores se hinchan, es imposible admitirlos. La solución clásica a este problema es la partición de la solución en carpetas / proyectos, los servicios necesarios se colocan en cada uno, son fáciles de encontrar y usar
Todo esto tiene un gran inconveniente, el código no le dice al desarrollador qué hacer, te hace pensar => pérdida de tiempo => errores de implementación
Y cuanto más tienes que pensar, más errores se cometen.
Introducción al enrutamiento suave
¿Qué pasa si el enrutamiento se construye así :
let webPart = choose [ path "/" >=> (OK "Home") path "/about" >=> (OK "About") path "/articles" >=> (OK "List of articles") path "/articles/browse" >=> (OK "Browse articles") path "/articles/details" >=> (OK "Content of an article") ]
''> => '' - ¿Qué es? Esta cosa tiene un nombre, pero su conocimiento no acercará al lector un solo gramo a comprender cómo funciona, por lo que no tiene sentido traerlo, es mejor considerar cómo funciona todo
La canalización de Suave está escrita arriba, la misma se usa en Giraffe (con una firma diferente de funciones), hay una firma:
type WebPart = HttpContext -> Async<HttpContext option>
Async en este caso no juega un papel especial (para entender cómo funciona), omítalo
HttpContext -> HttpContext option
Una función con dicha firma acepta un HttpContext , procesa (deserializa el cuerpo, mira las cookies, solicita encabezados), forma una respuesta y, si todo salió bien, lo envuelve en Some , si algo sale mal, devuelve None , por ejemplo ( función de biblioteca ):
// async let OK s : WebPart = fun ctx -> { ctx with response = { ctx.response with status = HTTP_200.status; content = Bytes s }} |> Some |> async.Return
Esta función no puede "ajustar el flujo de ejecución de la solicitud", siempre arroja una nueva respuesta más allá, con el cuerpo y el estado 200, pero esta puede:
let path (str:string) ctx = let path = ctx.request.rawPath if path.StartsWith str then ctx |> Some |> async.Return else async.Return None
La última función que necesita es elegir : obtenga una lista de diferentes funciones y seleccione la que devuelva Algunas primero:
let rec choose (webparts:(HttpContext) -> Async<HttpContext option>) list) context= async{ match webparts with | [head] -> return! head context | head::tail -> let! result = head context match result with | Some _-> return result | None -> return! choose tail context | [] -> return None }
Bueno, la función de enlace más importante (se omite Async):
type WebPartWithoutAsync = HttpContext -> HttpContext option let (>=>) (h1:WebPartWithoutAsync ) (h2:WebPartWithoutAsync) ctx : HttpContext option = let result = h1 ctx match result with | Some ctx' -> h2 ctx' | None -> None
Versión asíncrona type WebPart = HttpContext -> Async<HttpContext option> let (>=>) (h1:WebPart ) (h2:WebPart ) ctx : Async<HttpContext option>= async{ let! result = h1 ctx match result with | Some ctx' -> return! h2 ctx' | None -> return None }
"> =>" acepta dos controladores en los lados izquierdo y derecho y httpContext , cuando llega la solicitud, el servidor forma un objeto HttpContext y lo pasa a la función, "> =>" ejecuta el primer controlador (izquierdo) si devuelve Some ctx , pasa ctx a la entrada del segundo manejador.
¿Y por qué podemos escribir así (combinar varias funciones)?
GET >=> path "/api" >=> OK
¿Porque "> =>" acepta dos funciones de WebPart y devuelve una función que toma el HttpContext y devuelve Async <HttpContext option> , y qué función toma el contexto y devuelve Async <HttpContext option> ?
Webpart
Resulta que "> =>" toma WebPart para el controlador y devuelve WebPart , por lo que podemos escribir varios combinadores en una fila, y no solo dos.
Los detalles sobre el trabajo de los combinadores se pueden encontrar aquí.
¿Qué tiene que ver el rol y la restricción de acceso?
Volvamos al comienzo del artículo, ¿cómo podemos indicar explícitamente al programador a qué recursos se puede acceder para un rol en particular? Es necesario ingresar estos datos en la tubería para que los manejadores tengan acceso a los recursos correspondientes, lo hice así:

La aplicación está dividida en partes / módulos. Las funciones AdminPart y AccountPart permiten el acceso a estos módulos de varias funciones, todos los usuarios tienen acceso a AccountPart, solo el administrador accede a AdminPart, se reciben datos, preste atención a la función chooseP, tengo que agregar más funciones, porque las estándar están conectadas a tipos Suave, y Los controladores dentro de AdminPart y AccountPart ahora tienen firmas diferentes:
// AdminPart AdminInfo * HttpContext -> Async<(AdminInfo * HttpContext) option> // AccountPart AccountInfo* HttpContext -> Async<(AccountInfo * HttpContext) option>
En el interior, las nuevas características son completamente idénticas a las originales.
Ahora el controlador tiene acceso inmediato a los recursos para cada rol, solo se necesita agregar lo principal allí para que pueda navegar fácilmente, por ejemplo, en AccountPart puede agregar un apodo, correo electrónico, rol de usuario, lista de amigos si es una red social, pero hay un problema: para uno abrumador Para la mayoría de los manejadores, necesito una lista de amigos, pero para el resto no la necesito, ¿qué debo hacer? Distribuya estos controladores en diferentes módulos (preferiblemente), o haga que el acceso sea lento (ajuste en la unidad -> lista de amigos ), lo principal es no poner IQueryable <Friend> allí , porque esto no es un servicio, es un conjunto de datos que define el rol
Pongo en AdminInfo información sobre los usuarios aprobados y prohibidos por el administrador actual, en el contexto de mi "aplicación" esto define el rol del Administrador:
type AdminInfo = { ActiveUsersEmails: string list BanUsersEmails : string list } type UserInfo = { Name:string Surname:string }
¿Cuál es la diferencia de reclamo ? ¿ Es posible hacer User.Claims en el controlador y obtener lo mismo?
Al escribir y al hablar: módulos, el desarrollador no tiene que buscar ejemplos de código en controladores en el mismo contexto, crea un controlador y lo agrega al enrutamiento y hace que todo se compile
let AccountPart handler = let getUserInfo ctx = async.Return {Name="Al";Surname="Pacino"} permissionHandler [User;Admin] getUserInfo handler
getUserInfo recibe datos para el módulo Cuenta , tiene acceso al contexto para obtener datos personales (esto es exactamente este usuario'a, admin'a)
permissionHandler busca el token jwt, lo descifra y comprueba el acceso, devuelve el WebPart original para mantener la compatibilidad con Suave
El código fuente completo se puede encontrar en github
Gracias por su atencion!