Контроль доступа на основе ролей в 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, а также проверку прав в маршрутах вместо имён ролей. Таким образом, добавление новой роли будет означать изменение лишь одного соответствия, а не всех маршрутов.