
In modernen Anwendungen, die die Authentifizierung unterstützen, möchten wir häufig ändern, was für den Benutzer sichtbar ist, abhängig von seiner Rolle. Beispielsweise kann ein Gastbenutzer einen Artikel sehen, aber nur ein registrierter Benutzer oder Administrator sieht eine Schaltfläche zum Löschen dieses Artikels.
Das Verwalten dieser Sichtbarkeit kann mit zunehmenden Rollen ein Albtraum sein. Sie haben wahrscheinlich Code wie diesen geschrieben oder gesehen:
if (user.role === ADMIN || user.auth && post.author === user.id) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) }
Dieser Code wird in der gesamten Anwendung verteilt und wird normalerweise zu einem großen Problem, wenn der Kunde die Anforderungen ändert oder zusätzliche Rollen hinzufügen möchte. Am Ende müssen Sie alle diese if-s durchgehen und zusätzliche Prüfungen hinzufügen.
In diesem Artikel werde ich eine alternative Möglichkeit zum Implementieren der Berechtigungsverwaltung in der Expressjs-API mithilfe einer Bibliothek namens CASL zeigen . Dies vereinfacht die Zugriffskontrolle erheblich und ermöglicht es Ihnen, das vorherige Beispiel folgendermaßen umzuschreiben:
if (req.ability.can('read', post)) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) }
Neu bei CASL? Ich empfehle zu lesen Was ist CASL?
Hinweis: Dieser Artikel wurde ursprünglich auf Medium veröffentlicht.
Demo-App
Als Testanwendung habe ich eine ziemlich einfache REST-API für das Blog erstellt . Die Anwendung besteht aus 3 Entitäten ( User
, Post
und Comment
) und 4 Modulen (ein Modul für jede Entität und ein anderes für die Berechtigungsüberprüfung). Alle Module befinden sich im Ordner src/modules
.
Die Anwendung verwendet Mungomodelle , Passauthentifizierung und CASL-basierte Autorisierung (oder Zugriffskontrolle). Mit der API kann der Benutzer:
- Lesen Sie alle Artikel und Kommentare
- einen Benutzer erstellen (d. h. registrieren)
- Verwalten Sie Ihre eigenen Artikel (erstellen, bearbeiten, löschen), sofern dies autorisiert ist
- Aktualisieren Sie persönliche Informationen, wenn Sie dazu berechtigt sind
- Verwalten Sie Ihre eigenen Kommentare, wenn Sie dazu berechtigt sind
Um diese Anwendung zu installieren, klonen Sie sie einfach von github und führen Sie npm install
und npm start
. Sie müssen auch den MongoDB-Server starten. Die Anwendung stellt eine Verbindung zu mongodb://localhost:27017/blog
. Nachdem alles fertig ist, können Sie ein wenig spielen. Um es noch lustiger zu machen, importieren Sie die Basisdaten aus dem Ordner db/
:
mongorestore ./db
Alternativ können Sie den Anweisungen in der README-Projektdatei folgen oder meine Postman-Sammlung verwenden .
Was ist der Trick?
Erstens besteht der große Vorteil von CASL darin, dass Sie Zugriffsrechte für alle Benutzer an einem Ort festlegen können! Zweitens konzentriert sich CASL nicht darauf, wer der Benutzer ist, sondern darauf, was er tun kann, d. H. auf seine Fähigkeiten. Auf diese Weise können Sie diese Funktionen ohne unnötigen Aufwand auf verschiedene Rollen oder Benutzergruppen verteilen. Dies bedeutet, dass wir Zugriffsrechte für autorisierte und nicht autorisierte Benutzer registrieren können:
const { AbilityBuilder, Ability } = require('casl') function defineAbilitiesFor(user) { const { rules, can } = AbilityBuilder.extract() can('read', ['Post', 'Comment']) can('create', 'User') if (user) { can(['update', 'delete', 'create'], ['Post', 'Comment'], { author: user._id }) can(['read', 'update'], 'User', { _id: user._id }) } return new Ability(rules) } const ANONYMOUS_ABILITY = defineAbilitiesFor(null) module.exports = function createAbilities(req, res, next) { req.ability = req.user.email ? defineAbilitiesFor(req.user) : ANONYMOUS_ABILITY next() }
Lassen Sie uns nun den oben geschriebenen Code analysieren. Eine Instanz von AbilityBuilder
-a wird in der Funktion defineAbilitiesFor(user)
Extraktionsmethode teilt dieses Objekt in zwei einfache Funktionen auf, die can
und cannot
und ein Array von rules
( cannot
in diesem Code cannot
verwendet werden). Als Nächstes bestimmen wir mithilfe der Aufrufe der Funktion can
, was der Benutzer tun kann: Das erste Argument übergibt die Aktion (oder ein Array von Aktionen), das zweite Argument ist der Typ des Objekts, über das die Aktion (oder ein Array von Typen) ausgeführt wird, und das Bedingungsobjekt kann als drittes optionales Argument übergeben werden. Das Bedingungsobjekt wird verwendet, wenn Zugriffsrechte für eine Instanz der Klasse überprüft werden, d. H. Es wird geprüft, ob die author
Eigenschaft des post
Objekts und der user._id
gleich sind. Wenn dies der true
wird true
zurückgegeben, andernfalls false
. Der Klarheit halber werde ich ein Beispiel geben:
// Post is a mongoose model const post = await Post.findOne() const user = await User.findOne() const ability = defineAbilitiesFor(user) console.log(ability.can('update', post)) // post.author === user._id, true
Als nächstes bestimmen wir mit if (user)
die Zugriffsrechte für den autorisierten Benutzer (wenn der Benutzer nicht autorisiert ist, wissen wir nicht, wer er ist, und wir haben kein Objekt mit Informationen über den Benutzer). Am Ende geben wir eine Instanz der Ability
, mit deren Hilfe wir die Zugriffsrechte überprüfen.
Als Nächstes erstellen wir die Konstante ANONYMOUS_ABILITY
, eine Instanz der Ability
Klasse für nicht autorisierte Benutzer. Am Ende exportieren wir Express-Middleware, die für die Erstellung einer Ability
Instanz für einen bestimmten Benutzer verantwortlich ist.
API testen
Lassen Sie uns testen, was mit Postman passiert ist. Zuerst müssen Sie accessToken erhalten, um eine Anfrage zu senden:
POST /session { "session": { "email": "casl@medium.com", "password": "password" } }
Sie erhalten so etwas als Antwort:
{ "accessToken": "...." }
Dieses Token muss in den Authorization header
eingefügt und mit allen nachfolgenden Anforderungen gesendet werden.
Versuchen wir nun, den Artikel zu aktualisieren.
PATCH http://localhost:3030/posts/597649a88679237e6f411ae6 { "post": { "title": "[UPDATED] my post title" } } 200 Ok { "post": { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } }
Alles funktioniert gut. Aber was ist, wenn wir den Artikel eines anderen aktualisieren?
PATCH http://localhost:3030/posts/59761ba80203fb638e9bd85c { "post": { "title": "[EVIL ACTION] my post title" } } 403 { "status": "forbidden", "message": "Cannot execute \"update\" on \"Post\"" }
Habe einen Fehler! Wie erwartet :)
Stellen wir uns nun vor, dass wir für die Autoren unseres Blogs eine Seite erstellen möchten, auf der sie alle Beiträge sehen können, die sie aktualisieren können. Aus Sicht der spezifischen Logik ist dies nicht schwierig. Sie müssen lediglich alle Artikel auswählen, in denen der Autor gleich user._id
. Aber wir haben diese Logik bereits mit Hilfe von CASL registriert. Es wäre sehr praktisch, alle diese Artikel aus der Datenbank abzurufen, ohne zusätzliche Anfragen zu schreiben. Wenn sich die Rechte ändern, müssen Sie die Anfrage ändern - zusätzliche Arbeit :).
Glücklicherweise hat CASL ein zusätzliches npm-Paket - @ casl / mongoose . Mit diesem Paket können Sie Datensätze aus MongoDB nach bestimmten Berechtigungen abfragen! Für Mungos bietet dieses Paket ein Plugin, das dem Modell die Methode " accessibleBy(ability, action)
hinzufügt. Mit dieser Methode fordern wir auch Datensätze aus der Datenbank an (mehr dazu in der CASL-Dokumentation und in der README-Paketdatei ).
Genau so wird der handler
für /posts
implementiert (ich habe auch die Möglichkeit hinzugefügt, anzugeben, für welche Aktion Sie Berechtigungen überprüfen müssen):
Post.accessibleBy(req.ability, req.query.action)
Um das zuvor beschriebene Problem zu lösen, fügen Sie einfach den Parameter action=update
:
GET http://localhost:3030/posts?action=update 200 Ok { "posts": [ { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } ] }
Abschließend
Dank CASL haben wir eine wirklich gute Möglichkeit, Zugriffsrechte zu verwalten. Ich bin mehr als sicher, dass der Typ konstruiert
if (ability.can('read', post)) ...
viel klarer und einfacher als
if (user.role === ADMIN || user.auth && todo.author === user.id) ...
Mit CASL können wir klarer machen, was unser Code tut. Darüber hinaus werden solche Überprüfungen sicherlich an anderer Stelle in unserer Anwendung verwendet, und hier hilft CASL dabei, Codeduplikationen zu vermeiden.
Ich hoffe, Sie waren genauso daran interessiert, über CASL zu lesen, wie ich daran interessiert war, es zu erstellen. CASL hat eine ziemlich gute Dokumentation , Sie werden dort sicherlich viele nützliche Informationen finden, aber Sie können gerne Fragen stellen, wenn Sie im Gitter-Chat sind, und ein Sternchen auf dem Github hinzufügen ;)