ASP.NET Core par standard propose de configurer l'accès à l'api en utilisant des attributs, il est possible de restreindre l'accès aux utilisateurs avec une certaine revendication, vous pouvez définir des politiques et vous lier aux contrôleurs, en créant des contrôleurs pour différents rôles
Ce système a des inconvénients, le plus grand en ce sens, en regardant cet attribut:
[Authorize(Roles = "Administrator")] public class AdministrationController : Controller { }
Nous ne recevons aucune information sur les droits de l'administrateur.
Ma tâche est d'afficher tous les utilisateurs bannis pour ce mois (pas seulement d'aller à la base de données et au filtre, il y a certaines règles de comptage qui se trouvent quelque part), je fais CTRL + N sur le projet et cherche BannedUserHandler ou IHasInfoAbounBannedUser ou GetBannedUsersForAdmin .
Je trouve les contrôleurs marqués de l'attribut [Authorize (Roles = "Administrator")] , il peut y avoir deux scénarios:
Nous faisons tout dans le contrôleur
[Route("api/[controller]/[action]")] public class AdminInfoController1 : ControllerBase { private readonly IGetUserInfoService _getInfoAboutActiveUsers; private readonly ICanBanUserService _banUserService; private readonly ICanRemoveBanUserService _removeBanUserService;
Nous distribuons sur les gestionnaires
[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); }
La première approche n'est pas mauvaise dans la mesure où nous connaissons l'accès aux ressources dont dispose l'administrateur, les dépendances qu'il peut utiliser, j'utiliserais cette approche dans les petites applications, sans sujet complexe
La seconde n'est pas si parlante, toutes les dépendances sont résolues dans les gestionnaires, je ne peux pas regarder le constructeur et comprendre de quel type de dépendance j'ai besoin, cette approche se justifie lorsque l'application est complexe et que les contrôleurs gonflent, il devient impossible de les prendre en charge. La solution classique à ce problème est le partitionnement de la solution en dossiers / projets, les services nécessaires sont mis dans chacun, ils sont faciles à trouver et à utiliser
Tout cela a un gros inconvénient, le code ne dit pas au développeur quoi faire, il vous fait penser => perte de temps => erreurs d'implémentation
Et plus vous devez penser, plus vous faites d'erreurs.
Introduction au routage Suave
Et si le routage est construit comme ceci :
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'est-ce que c'est? Cette chose a un nom, mais ses connaissances n'amèneront pas le lecteur, même un gramme, à comprendre comment cela fonctionne, il est donc inutile de l'apporter, il vaut mieux considérer comment tout fonctionne
Le pipeline de Suave est écrit ci-dessus, le même est utilisé dans Giraffe (avec une signature de fonctions différente), il y a une signature:
type WebPart = HttpContext -> Async<HttpContext option>
Async dans ce cas ne joue pas un rôle spécial (pour comprendre comment cela fonctionne), omettez-le
HttpContext -> HttpContext option
Une fonction avec une telle signature accepte un HttpContext , traite (désérialise le corps, regarde les cookies, demande les en-têtes), forme une réponse, et si tout s'est bien passé, l'enveloppe dans Some , si quelque chose ne va pas, retourne None , par exemple ( fonction de bibliothèque ):
// async let OK s : WebPart = fun ctx -> { ctx with response = { ctx.response with status = HTTP_200.status; content = Bytes s }} |> Some |> async.Return
Cette fonction ne peut pas "encapsuler le flux d'exécution des requêtes", elle lance toujours une nouvelle réponse, avec le corps et le statut 200, mais celle-ci peut:
let path (str:string) ctx = let path = ctx.request.rawPath if path.StartsWith str then ctx |> Some |> async.Return else async.Return None
La dernière fonction dont vous avez besoin est de choisir - il obtient une liste de différentes fonctions et sélectionne celle qui renvoie Some first:
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 }
Eh bien, la fonction de liaison la plus importante (Async omis):
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
Version asynchrone 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 }
"> =>" accepte deux gestionnaires sur les côtés gauche et droit et httpContext , lorsque la demande arrive, le serveur forme un objet HttpContext et le transmet à la fonction, "> =>" exécute le premier gestionnaire (gauche) s'il renvoie Some ctx , passe ctx à l'entrée du deuxième gestionnaire.
Et pourquoi peut-on écrire comme ça (combiner plusieurs fonctions)?
GET >=> path "/api" >=> OK
Parce que "> =>" accepte deux fonctions WebPart et renvoie une fonction qui prend HttpContext et renvoie Async <option HttpContext> , et quelle fonction prend le contexte et retourne Async <option HttpContext> ?
Webpart .
Il s'avère que "> =>" prend WebPart pour le gestionnaire et retourne WebPart , nous pouvons donc écrire plusieurs combinateurs d'affilée, et pas seulement deux.
Des détails sur le travail des combinateurs peuvent être trouvés ici.
Qu'est-ce que le rôle et la restriction d'accès ont à voir avec cela?
Revenons au début de l'article, comment pouvons-nous indiquer explicitement au programmeur quelles ressources sont accessibles pour un rôle particulier? Il est nécessaire d'entrer ces données dans le pipeline pour que les gestionnaires aient accès aux ressources correspondantes, je l'ai fait comme ceci:

L'application est divisée en parties / modules. Les fonctions AdminPart et AccountPart permettent d'accéder à ces modules de différents rôles, tous les utilisateurs ont accès à AccountPart, seul l'administrateur accède à AdminPart, les données sont reçues, faites attention à la fonction chooseP, je dois ajouter plus de fonctions, car les standards sont attachés aux types Suave, et les gestionnaires dans AdminPart et AccountPart ont désormais des signatures différentes:
// AdminPart AdminInfo * HttpContext -> Async<(AdminInfo * HttpContext) option> // AccountPart AccountInfo* HttpContext -> Async<(AccountInfo * HttpContext) option>
À l'intérieur, les nouvelles fonctionnalités sont totalement identiques à celles d'origine.
Maintenant, le gestionnaire a immédiatement accès aux ressources pour chaque rôle, seule la chose principale doit y être ajoutée afin que vous puissiez facilement naviguer, par exemple, dans AccountPart, vous pouvez ajouter un surnom, un e-mail, un rôle d'utilisateur, une liste d'amis s'il s'agit d'un réseau social, mais il y a un problème: pour un écrasant la plupart des gestionnaires ont besoin d'une liste d'amis, mais pour le reste, je n'en ai pas du tout besoin, que dois-je faire? Soit distribuer ces gestionnaires dans différents modules (de préférence), soit rendre l'accès paresseux (envelopper dans l' unité -> liste d'amis ), l'essentiel n'est pas de mettre IQueryable <Ami> là , car ce n'est pas un service - c'est un ensemble de données qui définit le rôle
Je mets dans AdminInfo des informations sur les utilisateurs approuvés et interdits par l'administrateur actuel, dans le cadre de mon "application" cela définit le rôle de l'administrateur:
type AdminInfo = { ActiveUsersEmails: string list BanUsersEmails : string list } type UserInfo = { Name:string Surname:string }
Quelle est la différence avec Claim ? Est- il possible de faire des User.Claims dans le contrôleur et d'obtenir la même chose?
En tapant et en parlant: modules, le développeur n'a pas à chercher d'exemples de code sur des gestionnaires dans le même contexte, il crée un gestionnaire et l'ajoute au routage et le fait tout compiler
let AccountPart handler = let getUserInfo ctx = async.Return {Name="Al";Surname="Pacino"} permissionHandler [User;Admin] getUserInfo handler
getUserInfo reçoit des données pour le module Compte , a accès au contexte pour obtenir des données personnelles (c'est exactement cet utilisateur'a, admin'a)
permissionHandler vérifie le jeton jwt, le déchiffre et vérifie l'accès, renvoie le WebPart d'origine pour maintenir la compatibilité avec Suave
Le code source complet peut être trouvé sur github
Merci de votre attention!