Kontrola dostępu oparta na rolach w Express z JWT i dwoma middleware’ami
Dowiedz się, jak wdrożyć dostęp oparty na rolach w API Express poprzez połączenie middleware do autoryzacji JWT z wielokrotnie używalną funkcją obronną authorize(), oraz kiedy należy zwrócić błąd 401, a kiedy 403.
Źródła: What is RBAC in Node.js?
W większości systemów backendowych różnym użytkownikom pozwalane jest wykonywanie różnych działań: administrator może usuwać konta, menedżer może je edytować, a zwykły użytkownik może jedynie czytać. Rozproszenie sprawdzeń typu if (user.role === ...) w kontrolerach szybko staje się trudne do utrzymania i podatne na błędy. Ten przewodnik pokazuje, jak sformułować te zasady jako kontrolę dostępu opartą na rolach (RBAC) w Express przy użyciu dwóch prostych middleware’ów, oraz jak wybrać odpowiedni kod stanu w przypadku odrzucenia żądania.
Typowa macierz uprawnień wygląda następująco:
Admin → Create, Read, Update, Delete
Manager → Read, Update
User → Read
Jak RBAC wpisuje się w proces obsługi żądań
RBAC to model autoryzacji: dostęp do punktu końcowego jest przyznawany lub odrzucany w zależności od roli przypisanej do osoby wysyłającej żądanie. W API Node.js opartym na JWT elementy te są uporządkowane w stałej kolejności:
Login
↓
JWT Token
↓
Authentication Middleware
↓
Check User Role
↓
Authorization Middleware
↓
Controller
W tym tekście odpowiadane są na dwa oddzielne pytania. Autoryzacja określa, kim jest osoba wykonująca żądanie. Uprawnienia decydują o tym, co ta osoba może robić. Rozdzielenie ich w osobnych middleware’ach sprawia, że każdy z nich jest prosty i poddawalny testom. Różnica ta jest omówiona bardziej szczegółowo w temacie, gdzie znajdują się autoryzacja i uprawnienia.
Łączenie ról z punktami końcowymi
Rozważmy standardowy zestaw tras do zarządzania użytkownikami:
GET /api/users
POST /api/users
PUT /api/users/:id
DELETE /api/users/:id
Żądana polityka dla nich:
Admin → All APIs
Manager → GET, PUT
User → GET only
Zamiast ponownie sprawdzać rolę w każdym kontrolerze, politykę można deklaratywnie przypisać do każdej trasy.
Umieszczanie role w tokenie
Każda rekord użytkownika zawiera rolę:
{
name: "Ash",
email: "ash@example.com",
role: "admin"
}
Podczas logowania rola jest umieszczana w tokenie JWT razem z identyfikatorem użytkownika, dzięki czemu późniejsze żądania mogą być autoryzowane bez konieczności wyszukiwania w bazie danych:
const token = jwt.sign(
{
userId: user._id,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
Kompromis polega na tym, że rola pozostaje zamrożona w tokenie aż do jego wygaśnięcia. Jeśli administrator zostanie degradowany, stary token nadal będzie wskazywał admin przez maksymalnie godzinę. Krótkie terminy ważności ograniczają ten okres; przy operacjach wrażliwych bezpieczniej jest ponownie odczytać rolę z bazy danych.
Autoryzacja żądania
Pierwszy middleware pobiera token z nagłówka Authorization: Bearer <token>, weryfikuje jego podpis i termin ważności, a następnie przechowuje zdekodowany treść na req.user. Brakujące lub nieważne tokeny kończą żądanie błędem 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"
});
}
};
Po jego uruchomieniu każdy kolejny obsługiwacz może ufać req.user. Podczas weryfikacji dobrą praktyką jest również określenie akceptowanych algorytmów za pomocą opcji algorithms w funkcji jwt.verify, zamiast akceptować wszystko, co podaje nagłówek tokena.
Uprawnianie według roli
Drugi middleware to fabryka. Bierze dozwolone role i zwraca obsługiwacz, który odrzuca wszystkich, których rola nie znajduje się na liście, z kodem błędu 403:
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
message: "Access denied"
});
}
next();
};
};
Należy pamiętać, że czyta on req.user.role, więc musi zawsze być uruchamiany po authenticate; sam w sobie wywołałby błąd z powodu niezdefiniowanego req.user.
Ochrona ścieżki polega teraz na połączeniu tych dwóch elementów:
router.delete(
"/users/:id",
authenticate,
authorize("admin"),
deleteUser
);
Tylko zalogowany administrator ma dostęp do deleteUser. Pozwolenie kilku ról oznacza jedynie konieczność przekazania większej liczby argumentów:
router.put(
"/users/:id",
authenticate,
authorize("admin", "manager"),
updateUser
);
Tutaj zarówno administratorzy, jak i menedżerowie mogą aktualizować użytkowników.
Wybór między 401 a 403
Te dwa kody są często mylone, ale wskazują na różne problemy.
401: osoba wykonująca żądanie nie jest uwierzytelniona
Serwer nie wie, kto wysyła żądanie, zazwyczaj z powodu jednej z następujących przyczyn:
No token
Invalid token
Expired token
Klient powinien ponownie się zalogować lub odświeżyć swój token.
403: osoba wykonująca żądanie jest znana, ale nie ma uprawnień
Tożsamość została potwierdzona, ale role nie zapewniają dostępu:
User role = user
Required role = admin
Ponowna próba z tymi samymi danymi dostępowymi nie pomoże. Krótka zasada: 401 oznacza „nie wiemy, kim jesteś”; 403 oznacza „wiemy i odpowiedź brzmi nie”.
Szybki przegląd
- Czym jest RBAC? Kontrola dostępu, w której uprawnienia API wynikają z przydzielonej roli osoby wykonującej żądanie.
Podsumowanie
Cały proces jest prosty:
Client
↓
JWT
↓
Authentication
↓
Authorization / RBAC
↓
Controller
↓
Database
Zautentykuj osobę wykonującą żądanie, odczytaj jej rolę, porównaj ją z tym, co dozwala określona trasa, a następnie zatwierdź lub odrzuć żądanie. W miarę rozwoju aplikacji rozważ powiązanie ról z bardziej szczegółowymi uprawnieniami, takimi jak user:create, user:update i user:delete, oraz sprawdzanie uprawnień w trasach zamiast nazw ról. Dodawanie nowej roli oznacza wtedy edycję tylko jednego powiązania, a nie każdej trasy.