Rollenbasierte Zugriffskontrolle in Express mit JWT und zwei Middleware-Komponenten
Erfahren Sie, wie Sie rollenbasierten Zugriff in einer Express-API durch die Kombination eines JWT-Authentifizierungs-Middlewares mit einem wiederverwendbaren authorize()-Guard durchsetzen können, sowie wann Sie auf 401 statt auf 403 antworten sollten.
Quellen: What is RBAC in Node.js?
In den meisten Backend-Systemen dürfen verschiedene Benutzer unterschiedliche Aufgaben ausführen: Ein Administrator kann Konten löschen, ein Manager kann sie bearbeiten, während ein gewöhnlicher Benutzer nur lesen darf. Das Verwenden mehrerer if (user.role === ...)-Prüfungen in den Controllern wird schnell unverwaltbar und führt leicht zu Fehlern. Dieser Leitfaden zeigt, wie solche Regeln als rollenbasierte Zugriffskontrolle (RBAC) in Express mithilfe von zwei kleinen Middlewares umgesetzt werden können, sowie wie der richtige Statuscode bei einer abgelehnten Anfrage gewählt wird.
Eine typische Berechtigungsmatrix sieht so aus:
Admin → Create, Read, Update, Delete
Manager → Read, Update
User → Read
Wie RBAC in den Anfragenfluss passt
RBAC ist ein Autorisierungsmodell: Der Zugriff auf einen Endpunkt wird je nach der dem Aufrufer zugeordneten Rolle gewährt oder verweigert. In einer auf JWT basierenden Node.js-API sind die Komponenten in einer festgelegten Reihenfolge angeordnet:
Login
↓
JWT Token
↓
Authentication Middleware
↓
Check User Role
↓
Authorization Middleware
↓
Controller
Zwei getrennte Fragen werden im Laufe des Textes beantwortet. Die Authentifizierung bestimmt, wer der Aufrufer ist. Die Autorisierung entscheidet darüber, was dieser Aufrufer tun darf. Durch die Trennung in separate Middleware-Komponenten bleibt jede davon einfach und testbar. Die Unterscheidung wird ausführlicher in dort behandelt, wo Authentifizierung und Autorisierung in Ihrem Code ihren Platz haben.
Rollen zu Endpunkten zuordnen
Betrachten Sie eine Standardmenge an Benutzerverwaltungs-Routen:
GET /api/users
POST /api/users
PUT /api/users/:id
DELETE /api/users/:id
Die gewünschte Richtlinie für sie:
Admin → All APIs
Manager → GET, PUT
User → GET only
Anstatt die Rolle in jedem Controller erneut zu überprüfen, kann die Richtlinie deklarativ jeder Route zugeordnet werden.
Die Rolle im Token speichern
Jeder Benutzerdatensatz enthält eine Rolle:
{
name: "Ash",
email: "ash@example.com",
role: "admin"
}
Beim Anmelden wird die Rolle zusammen mit der Benutzer-ID im JWT gespeichert, sodass spätere Anfragen ohne Datenbankabfrage autorisiert werden können:
const token = jwt.sign(
{
userId: user._id,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
Der Kompromiss: Die Rolle bleibt bis zum Ablauf des Tokens festgelegt. Wenn ein Administrator degradiert wird, zeigt das alte Token bis zu einer Stunde lang weiterhin admin an. Kurze Ablaufzeiten begrenzen diesen Zeitraum; für sensible Operationen ist es sicherer, die Rolle erneut aus der Datenbank zu lesen.
Autorisierung der Anfrage
Das erste Middleware-Modul holt das Token aus dem Header Authorization: Bearer <token> ab, überprüft seine Signatur und Gültigkeitsdauer sowie speichert die decodierte Nutzdatenmenge in req.user. Fehlende oder ungültige Tokens beenden die Anfrage mit 401:
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({
message: "Authentication required"
});
}
try {
const decoded = jwt.verify(
token,
process.env.JWT_SECRET
);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({
message: "Invalid or expired token"
});
}
};
Nachdem es ausgeführt wurde, kann jeder nachfolgende Handler req.user vertrauen. Bei der Überprüfung ist es außerdem eine gute Praxis, die akzeptierten Algorithmen über die algorithms-Option von jwt.verify festzulegen, anstatt alles zu akzeptieren, was im Token-Header angegeben wird.
Autorisierung nach Rolle
Das zweite Middleware ist eine Fabrikfunktion. Sie nimmt die erlaubten Rollen entgegen und gibt einen Handler zurück, der jeden mit einer nicht auf der Liste stehenden Rolle mit 403 abweist:
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
message: "Access denied"
});
}
next();
};
};
Beachten Sie, dass es req.user.role liest, daher muss es immer nach authenticate ausgeführt werden; ansonsten würde es aufgrund eines undefinierten req.user einen Fehler werfen.
Der Schutz einer Route besteht nun darin, diese beiden Komponenten miteinander zu kombinieren:
router.delete(
"/users/:id",
authenticate,
authorize("admin"),
deleteUser
);
Nur ein authentifizierter Administrator kann auf deleteUser zugreifen. Das Erlauben mehrerer Rollen bedeutet lediglich, weitere Argumente mitzugeben:
router.put(
"/users/:id",
authenticate,
authorize("admin", "manager"),
updateUser
);
Hier können sowohl Admins als auch Manager Benutzer aktualisieren.
Auswahl zwischen 401 und 403
Diese beiden Codes werden oft verwechselt, signalisieren aber unterschiedliche Probleme.
401: Der Aufrufer ist nicht authentifiziert
Der Server weiß nicht, wer aufruft, typischerweise aus einem der folgenden Gründe:
No token
Invalid token
Expired token
Der Client sollte sich erneut anmelden oder sein Token aktualisieren.
403: Der Aufrufer ist bekannt, aber nicht berechtigt
Die Identität wurde festgestellt, doch die zugewiesene Rolle gewährt keinen Zugriff:
User role = user
Required role = admin
Ein erneuter Versuch mit denselben Anmeldedaten hilft nicht. Kurze Regel: 401 bedeutet „Wir wissen nicht, wer Sie sind“; 403 bedeutet „Wir wissen es, und die Antwort ist nein“.
Kurze Übersicht
- Was ist RBAC? Zugriffskontrolle, bei der API-Berechtigungen aus der dem Aufrufer zugewiesenen Rolle abgeleitet werden.
Zusammenfassung
Der gesamte Ablauf ist kurz:
Client
↓
JWT
↓
Authentication
↓
Authorization / RBAC
↓
Controller
↓
Database
Authentifizieren Sie den Aufrufer, lesen Sie die Rolle ein, vergleichen Sie sie mit dem, was der Route erlaubt ist, und erlauben oder ablehnen Sie den Zugriff. Wenn die Anwendung wächst, sollten Sie in Erwägung ziehen, Rollen mit feingranulierten Berechtigungen wie user:create, user:update und user:delete zu verknüpfen sowie die Berechtigungen direkt in den Routen statt anhand der Rollennamen zu überprüfen. Dadurch bedeutet das Hinzufügen einer neuen Rolle lediglich die Anpassung einer einzigen Zuordnung statt aller Routen.