Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP4 min czytania1024 słów

Ź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.
  • Jak to jest implementowane w Express? Jeden middleware weryfikuje JWT, a drugi sprawdza rolę względem listy dozwolonych tras.
  • Gdzie należy to egzekwować? W backendzie. Ukrywanie przycisków w interfejsie to kwestia estetyczna, ponieważ każdy może bezpośrednio wywołać API.
  • Dlaczego middleware? Umożliwia ponowne użycie mechanizmów autoryzacji i oddziela je od logiki biznesowej, dzięki czemu jeden mechanizm ochrony chroni wiele tras.
  • Czego RBAC nie obejmuje? Zasady własności, takie jak „użytkownicy mogą edytować tylko swój własny profil”, wymagają dodatkowej weryfikacji na podstawie samego zasobu.
  • 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.