ASP.NET Core bietet standardmäßig die Möglichkeit, den Zugriff auf API mithilfe von Attributen zu konfigurieren. Es ist möglich, den Zugriff auf Benutzer mit einem bestimmten Anspruch zu beschränken. Sie können Richtlinien definieren und an Controller binden und Controller für verschiedene Rollen erstellen
Dieses System hat die größten Minuspunkte, wenn man sich dieses Attribut ansieht:
[Authorize(Roles = "Administrator")] public class AdministrationController : Controller { }
Wir erhalten keine Informationen darüber, welche Rechte der Administrator hat.
Meine Aufgabe ist es, alle gesperrten Benutzer für diesen Monat anzuzeigen (nicht nur zur Datenbank gehen und filtern, es gibt bestimmte Zählregeln, die irgendwo liegen), ich mache STRG + N für das Projekt und suche nach BannedUserHandler oder IHasInfoAbounBannedUser oder GetBannedUsersForAdmin .
Ich finde die Controller mit dem Attribut [Authorize (Roles = "Administrator")] gekennzeichnet . Es kann zwei Szenarien geben:
Wir machen alles im Controller
[Route("api/[controller]/[action]")] public class AdminInfoController1 : ControllerBase { private readonly IGetUserInfoService _getInfoAboutActiveUsers; private readonly ICanBanUserService _banUserService; private readonly ICanRemoveBanUserService _removeBanUserService;
Wir verteilen auf Handler
[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); }
Der erste Ansatz ist nicht schlecht, da wir wissen, auf welche Ressourcen der Administrator verfügt und welche Abhängigkeiten er verwenden kann. Ich würde diesen Ansatz in kleinen Anwendungen ohne einen komplexen Themenbereich verwenden
Der zweite ist nicht so sprechend, alle Abhängigkeiten werden in Handlern aufgelöst. Ich kann nicht auf den Konstruktor schauen und verstehen, welche Art von Abhängigkeit ich benötige. Dieser Ansatz rechtfertigt sich, wenn die Anwendung komplex ist und die Controller anschwellen. Es wird unmöglich, sie zu unterstützen. Die klassische Lösung für dieses Problem ist die Aufteilung der Lösung in Ordner / Projekte. Die erforderlichen Dienste sind in jedem Ordner enthalten. Sie sind leicht zu finden und zu verwenden
All dies hat einen großen Nachteil, der Code sagt dem Entwickler nicht, was er tun soll, er lässt Sie denken => Zeitverschwendung => Implementierungsfehler
Und je mehr Sie nachdenken müssen, desto mehr Fehler werden gemacht.
Einführung in Suave Routing
Was ist, wenn das Routing so aufgebaut ist:
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") ]
''> => '' - was ist das? Dieses Ding hat einen Namen, aber sein Wissen bringt den Leser nicht einmal ein Gramm näher an das Verständnis, wie es funktioniert. Es macht also keinen Sinn, es zu bringen. Es ist besser zu überlegen, wie alles funktioniert
Die Pipeline von Suave ist oben geschrieben, die gleiche wird in Giraffe verwendet (mit einer anderen Signatur von Funktionen), es gibt eine Signatur:
type WebPart = HttpContext -> Async<HttpContext option>
Async spielt in diesem Fall keine besondere Rolle (um zu verstehen, wie es funktioniert), lassen Sie es weg
HttpContext -> HttpContext option
Eine Funktion mit einer solchen Signatur akzeptiert einen HttpContext , verarbeitet (deserialisiert den Körper, betrachtet Cookies, fordert Header an), bildet eine Antwort und gibt sie, wenn alles gut gelaufen ist, in Some ein . Wenn etwas schief geht, gibt sie beispielsweise None zurück ( Bibliotheksfunktion ):
// async let OK s : WebPart = fun ctx -> { ctx with response = { ctx.response with status = HTTP_200.status; content = Bytes s }} |> Some |> async.Return
Diese Funktion kann den Anforderungsausführungsfluss nicht "umbrechen", sondern wirft immer eine neue Antwort mit dem Text und dem Status 200 weiter aus, aber diese kann:
let path (str:string) ctx = let path = ctx.request.rawPath if path.StartsWith str then ctx |> Some |> async.Return else async.Return None
Die letzte Funktion, die Sie benötigen, ist select - sie ruft eine Liste verschiedener Funktionen ab und wählt die aus, die zuerst Some zurückgibt:
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 }
Nun, die wichtigste Bindungsfunktion (Async weggelassen):
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
Asynchrone Version 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 }
"> =>" akzeptiert zwei Handler auf der linken und rechten Seite und httpContext . Wenn die Anforderung eintrifft, bildet der Server ein HttpContext-Objekt und übergibt es an die Funktion. "> =>" führt den ersten (linken) Handler aus, wenn er Some ctx zurückgibt, übergibt ctx zur Eingabe des zweiten Handlers.
Und warum können wir so schreiben (mehrere Funktionen kombinieren)?
GET >=> path "/api" >=> OK
Weil "> =>" zwei WebPart-Funktionen akzeptiert und eine Funktion zurückgibt, die den HttpContext verwendet und Async <HttpContext-Option> zurückgibt , und welche Funktion den Kontext verwendet und Async <HttpContext-Option> zurückgibt ?
Webpart .
Es stellt sich heraus, dass "> =>" WebPart für den Handler verwendet und WebPart zurückgibt , sodass wir mehrere Kombinatoren hintereinander schreiben können und nicht nur zwei.
Details zur Arbeit von Kombinatoren finden Sie hier.
Was hat die Rolle und die Zugriffsbeschränkung damit zu tun?
Kehren wir zum Anfang des Artikels zurück. Wie können wir dem Programmierer explizit angeben, auf welche Ressourcen für eine bestimmte Rolle zugegriffen werden kann? Es ist notwendig, diese Daten in die Pipeline einzugeben, damit Handler Zugriff auf die entsprechenden Ressourcen haben. Ich habe es so gemacht:

Die Anwendung ist in Teile / Module unterteilt. Die Funktionen AdminPart und AccountPart ermöglichen den Zugriff auf diese Module mit verschiedenen Rollen. Alle Benutzer haben Zugriff auf AccountPart. Nur der Administrator greift auf AdminPart zu. Daten werden empfangen. Achten Sie auf die Funktion selectP. Ich muss weitere Funktionen hinzufügen, da die Standardfunktionen an Suave-Typen angehängt sind Handler in AdminPart und AccountPart haben jetzt unterschiedliche Signaturen:
// AdminPart AdminInfo * HttpContext -> Async<(AdminInfo * HttpContext) option> // AccountPart AccountInfo* HttpContext -> Async<(AccountInfo * HttpContext) option>
Im Inneren sind die neuen Funktionen völlig identisch mit dem Original
Jetzt hat der Handler sofort Zugriff auf Ressourcen für jede Rolle. Dort muss nur noch die Hauptsache hinzugefügt werden, damit Sie problemlos navigieren können. In AccountPart können Sie beispielsweise einen Spitznamen, eine E-Mail- Adresse , eine Benutzerrolle und eine Freundesliste hinzufügen, wenn es sich um ein soziales Netzwerk handelt, aber es gibt ein Problem: Zum einen überwältigend Für die meisten Handler brauche ich eine Freundesliste, aber für den Rest brauche ich sie überhaupt nicht. Was soll ich tun? Verteilen Sie diese Handler entweder auf verschiedene Module (vorzugsweise) oder machen Sie den Zugriff verzögert (Wrap in Unit -> Freundesliste ). Die Hauptsache ist, IQueryable <Friend> nicht dort abzulegen , da dies kein Dienst ist - es ist ein Datensatz, der die Rolle definiert
Ich habe AdminInfo- Informationen zu genehmigten und gesperrten Benutzern vom aktuellen Administrator eingegeben. Im Kontext meiner "Anwendung" definiert dies die Rolle des Administrators:
type AdminInfo = { ActiveUsersEmails: string list BanUsersEmails : string list } type UserInfo = { Name:string Surname:string }
Was ist der Unterschied zu Claim ? Ist es möglich , User.Claims im Controller zu erstellen und das Gleiche zu bekommen?
Beim Tippen und Sprechen: Modulen muss der Entwickler nicht nach Codebeispielen für Handler im selben Kontext suchen, sondern erstellt einen Handler, fügt ihn dem Routing hinzu und lässt alles kompilieren
let AccountPart handler = let getUserInfo ctx = async.Return {Name="Al";Surname="Pacino"} permissionHandler [User;Admin] getUserInfo handler
getUserInfo empfängt Daten für das Kontomodul und hat Zugriff auf den Kontext, um persönliche Daten abzurufen (genau dies ist user'a, admin'a).
erlaubnisHandler sucht nach jwt-Token, entschlüsselt es und prüft den Zugriff. Er gibt das ursprüngliche WebPart zurück, um die Kompatibilität mit Suave aufrechtzuerhalten
Den vollständigen Quellcode finden Sie auf github
Vielen Dank für Ihre Aufmerksamkeit!