Контроль доступу за ролями в Express з JWT та двома мідлвейрами
Дізнайтеся, як забезпечити доступ на основі ролей у API Express, поєднавши проміжок автентифікації JWT із багаторазово використовуваним захисником authorize(), а також коли слід повертати коди 401 та 403.
Джерела: What is RBAC in Node.js?
У більшості бекендів різним користувачам дозволяється виконувати різні дії: адміністратор може видаляти облікові записи, менеджер — редагувати їх, а звичайний користувач може лише читати. Розміщення перевірок у форматі if (user.role === ...) у контролерах швидко робить код некерованим та схильним до помилок. Цей посібник показує, як сформулювати ці правила за допомогою контролю доступу на основі ролей (RBAC) у Express за допомогою двох невеликих мідлверсів, а також як вибрати правильний код статусу при відхиленні запиту.
Типова матриця дозволів виглядає так:
Admin → Create, Read, Update, Delete
Manager → Read, Update
User → Read
Як RBAC вписується у процес обробки запитів
RBAC — це модель авторизації: доступ до кінцевої точки надається або відмовляється залежно від ролі, присвоєної користувачеві. У API Node.js на основі JWT елементи виконуються у фіксованому порядку:
Login
↓
JWT Token
↓
Authentication Middleware
↓
Check User Role
↓
Authorization Middleware
↓
Controller
У процесі відповідається на два окремі запитання. Аутентифікація визначає, хто є користувачем. Ауторизація вирішує, що цей користувач може робити. Розділення їх у окремих мідлвейрах робить кожен з них простим та піддаємим тестуванню. Це розрізнення детальніше розглядається у статті про те, де мають бути аутентифікація та ауторизація у вашому коді.
Призначення ролей ендпоїнтам
Розгляньмо стандартний набір маршрутів для керування користувачами:
GET /api/users
POST /api/users
PUT /api/users/:id
DELETE /api/users/:id
Бажана політика для них:
Admin → All APIs
Manager → GET, PUT
User → GET only
Замість повторної перевірки ролі в кожному контролері, політику можна декларативно приєднати до кожного маршруту.
Додавання ролі до токена
Кожен запис користувача містить роль:
{
name: "Ash",
email: "ash@example.com",
role: "admin"
}
Під час входу роль записується у JWT разом із ідентифікатором користувача, тож подальші запити можна авторизувати без пошуку в базі даних:
const token = jwt.sign(
{
userId: user._id,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
Компроміс: роль залишається незмінною у токені до його закінчення терміну дії. Якщо адміністратора понижують у рангу, старий токен все ще вказуватиме admin протягом години. Короткий термін дії обмежує цей проміжок часу; для конфіденційних операцій безпечніше знову читати інформацію про роль з бази даних.
Аутентифікація запиту
Перший мідлвейр отримує токен з заголовка Authorization: Bearer <token>, перевіряє його підпис та термін дії, а потім зберігає розшифрований вміст у полі req.user. Відсутні чи недійсні токени призводять до завершення запиту з кодом 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"
});
}
};
Після його виконання кожен наступний обробник може довіряти req.user. Під час перевірки також є гарною практикою вказати допустимі алгоритми через параметр algorithms у функції jwt.verify, замість того щоб приймати все, що вказано у заголовку токена.
Авторизація за роллю
Другий мідлвейр є фабрикою. Він бере список дозволених ролей та повертає обробник, який відхиляє всіх, чия роль не входить до цього списку, з кодом помилки 403:
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
message: "Access denied"
});
}
next();
};
};
Зауважте, що він читає req.user.role, тому його завжди потрібно виконувати після authenticate; інакше він винесе помилку через відсутність значення req.user.
Захист маршруту тепер полягає у поєднанні цих двох елементів:
router.delete(
"/users/:id",
authenticate,
authorize("admin"),
deleteUser
);
До функції deleteUser може отримати доступ лише автентифікований адміністратор. Дозволення кількох ролей означає лише необхідність передачі більшої кількості аргументів:
router.put(
"/users/:id",
authenticate,
authorize("admin", "manager"),
updateUser
);
Тут як адміністратори, так і менеджери можуть оновлювати користувачів.
Вибір між 401 та 403
Ці два коди часто плутають, але вони вказують на різні проблеми.
401: звернулийся не автентифікований
Сервер не знає, хто здійснює запит, зазвичай через одну з наступних причин:
No token
Invalid token
Expired token
Клієнт повинен знову увійти або оновити свій токен.
403: звернулийся відомий, але не має дозволу
Ідентичність підтверджена, проте роль не надає доступу:
User role = user
Required role = admin
Повторна спроба з тими самими обліковими даними не допоможе. Коротке правило: 401 означає «ми не знаємо, хто ви»; 403 означає «ми знаємо, і відповідь — ні».
Швидкий огляд
- Що таке RBAC? Контроль доступу, де права API визначаються на основі ролі, призначеної звернуломуся.
Підсумок
Повний процес є простим:
Client
↓
JWT
↓
Authentication
↓
Authorization / RBAC
↓
Controller
↓
Database
Аутентифікуйте того, хто звертається, прочитайте його роль, порівняйте її з тим, що дозволяє маршрут, а потім дозвольте доступ або відхиліть його. У міру зростання додатку розгляньте можливість прив’язки ролей до більш детальних дозволів, таких як user:create, user:update та user:delete, та перевірки дозволів у маршрутах замість назв ролей. Тоді додавання нової ролі означатиме лише зміну одного мапування, а не кожного маршруту.