Пошук уроків, статей та іншого контенту
Налаштуємо доступ до ресурсів за ролями та реалізуємо перевірку ролі в маршрутах Node.js API.
Role-Based Access Control (RBAC) — це модель керування доступом, у якій права користувача визначаються його роллю.
Наприклад:
admin може виконувати будь-які операції;
editor може переглядати та редагувати ресурси;
viewer може лише переглядати ресурси.
RBAC допомагає не дублювати перевірки в кожному обробнику маршруту. Замість цього перевірку ролі можна винести в middleware і повторно використовувати в API.
Важливо розділяти два поняття:
автентифікація — хто користувач;
авторизація — що цьому користувачеві дозволено.
Наприклад, перевірка JWT визначає користувача, а перевірка його ролі визначає доступ до конкретного маршруту.
Типовий запит проходить такі етапи:
Користувач надсилає токен доступу.
Middleware автентифікації перевіряє токен.
Дані користувача додаються до req.user.
Middleware авторизації перевіряє роль.
Якщо роль дозволена, виконується обробник маршруту.
Схематично:
HTTP-запит
↓
authMiddleware
↓
req.user
↓
requireRole(...)
↓
обробник маршрутуЯкщо токен відсутній або недійсний, сервер має повернути 401 Unauthorized.
Якщо користувач автентифікований, але не має потрібної ролі, сервер має повернути 403 Forbidden.
Створимо невеликий Express API з JWT-автентифікацією та перевіркою ролей.
Встановіть залежності:
npm init -y
npm install express jsonwebtokenСтворіть файл server.js.
У прикладі користувачі зберігаються в масиві. У реальному застосунку їхні дані мають зберігатися в базі даних, а паролі — у хешованому вигляді.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const PORT = 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'development-secret';
// Демонстраційні дані.
// У production-проєкті користувачів потрібно зберігати в базі даних,
// а паролі — у хешованому вигляді.
const users = [
{
id: 1,
email: 'admin@example.com',
password: 'admin-password',
role: 'admin'
},
{
id: 2,
email: 'editor@example.com',
password: 'editor-password',
role: 'editor'
},
{
id: 3,
email: 'viewer@example.com',
password: 'viewer-password',
role: 'viewer'
}
];
const articles = [
{
id: 1,
title: 'Вступ до Node.js',
content: 'Навчальний матеріал'
}
];
app.use(express.json());
// Вхід користувача та створення JWT
app.post('/login', (req, res) => {
const { email, password } = req.body;
const user = users.find(
(item) => item.email === email && item.password === password
);
if (!user) {
return res.status(401).json({
message: 'Неправильна електронна пошта або пароль'
});
}
const token = jwt.sign(
{
sub: user.id,
role: user.role
},
JWT_SECRET,
{
expiresIn: '1h'
}
);
res.json({ token });
});
// Автентифікація: перевіряє Bearer-токен
function authenticate(req, res, next) {
const authorization = req.headers.authorization;
if (!authorization || !authorization.startsWith('Bearer ')) {
return res.status(401).json({
message: 'Потрібен токен доступу'
});
}
const token = authorization.slice('Bearer '.length);
try {
const payload = jwt.verify(token, JWT_SECRET);
// Дані з перевіреного токена доступні наступним middleware
req.user = {
id: payload.sub,
role: payload.role
};
next();
} catch (error) {
return res.status(401).json({
message: 'Недійсний або прострочений токен'
});
}
}
// Авторизація: дозволяє доступ лише вказаним ролям
function requireRole(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
message: 'Користувач не автентифікований'
});
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
message: 'Недостатньо прав для виконання цієї операції'
});
}
next();
};
}
// Перегляд статей доступний усім автентифікованим користувачам
app.get('/articles', authenticate, (req, res) => {
res.json({
user: req.user,
articles
});
});
// Створення статті доступне адміністраторам і редакторам
app.post(
'/articles',
authenticate,
requireRole('admin', 'editor'),
(req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({
message: 'Поля title та content є обов’язковими'
});
}
const article = {
id: articles.length + 1,
title,
content
};
articles.push(article);
res.status(201).json(article);
}
);
// Видалення статті доступне лише адміністраторам
app.delete(
'/articles/:id',
authenticate,
requireRole('admin'),
(req, res) => {
const articleId = Number(req.params.id);
const articleIndex = articles.findIndex(
(article) => article.id === articleId
);
if (articleIndex === -1) {
return res.status(404).json({
message: 'Статтю не знайдено'
});
}
const deletedArticle = articles.splice(articleIndex, 1)[0];
res.json({
message: 'Статтю видалено',
article: deletedArticle
});
}
);
app.listen(PORT, () => {
console.log(`API запущено на http://localhost:${PORT}`);
});Запустіть сервер:
node server.jsФункція requireRole приймає довільну кількість дозволених ролей:
requireRole('admin', 'editor')Усередині middleware використовується перевірка:
allowedRoles.includes(req.user.role)Якщо роль користувача входить до списку дозволених, викликається next() і запит передається наступному обробнику.
Якщо роль не дозволена, сервер завершує запит відповіддю 403:
return res.status(403).json({
message: 'Недостатньо прав для виконання цієї операції'
});Порядок middleware має значення:
app.post(
'/articles',
authenticate,
requireRole('admin', 'editor'),
createArticle
);Спочатку виконується authenticate, який створює req.user. Тільки після цього requireRole може перевірити роль користувача.
Увійдемо як редактор:
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email":"editor@example.com","password":"editor-password"}'У відповіді буде JWT:
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}Збережіть токен у змінну середовища:
export TOKEN="вставте_сюди_отриманий_токен"Цей маршрут доступний будь-якій автентифікованій ролі:
curl http://localhost:3000/articles \
-H "Authorization: Bearer $TOKEN"Редактор має дозвіл на створення статей:
curl -X POST http://localhost:3000/articles \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Нова стаття","content":"Текст статті"}'Редактор не має права видаляти статті:
curl -X DELETE http://localhost:3000/articles/1 \
-H "Authorization: Bearer $TOKEN"Сервер поверне:
{
"message": "Недостатньо прав для виконання цієї операції"
}Щоб виконати видалення, потрібно увійти як admin і використати токен адміністратора.
У простому RBAC кожен маршрут містить список ролей, яким дозволена операція:
requireRole('admin')
requireRole('admin', 'editor')
requireRole('admin', 'editor', 'viewer')Це зручно для невеликої кількості ролей. Наприклад:
app.get(
'/reports',
authenticate,
requireRole('admin', 'editor'),
getReports
);Тут звіти можуть переглядати адміністратори та редактори, але не звичайні читачі.
Важливо, що роль потрібно перевіряти на сервері. Приховування кнопки в клієнтському інтерфейсі не є захистом, оскільки користувач може вручну надіслати HTTP-запит до API.
У прикладі роль записується до payload токена:
const token = jwt.sign(
{
sub: user.id,
role: user.role
},
JWT_SECRET,
{
expiresIn: '1h'
}
);Після перевірки підпису JWT роль можна отримати з payload:
const payload = jwt.verify(token, JWT_SECRET);
req.user = {
id: payload.sub,
role: payload.role
};Не можна просто довіряти значенням, отриманим від клієнта, наприклад:
// Небезпечно
const role = req.headers['x-user-role'];Клієнт може самостійно встановити такий заголовок. Роль повинна надходити з перевіреного джерела: підписаного токена або даних користувача, отриманих із бази даних після автентифікації.
Іноді потрібна перевірка не конкретної ролі, а наявності однієї з кількох ролей. Для цього requireRole вже приймає список аргументів:
app.put(
'/articles/:id',
authenticate,
requireRole('admin', 'editor'),
(req, res) => {
res.json({ message: 'Статтю оновлено' });
}
);Якщо ролей стане більше, їх можна зберігати в константах:
const ROLES = {
ADMIN: 'admin',
EDITOR: 'editor',
VIEWER: 'viewer'
};
app.post(
'/articles',
authenticate,
requireRole(ROLES.ADMIN, ROLES.EDITOR),
(req, res) => {
res.status(201).json({ message: 'Статтю створено' });
}
);Так зменшується ризик помилок у рядках ролей.
Перевірка ролі не завжди достатня. Наприклад, роль editor може дозволяти редагування статей, але користувач повинен мати доступ лише до статей своєї команди.
У такому випадку потрібні дві перевірки:
користувач має потрібну роль;
користувач має доступ саме до цього ресурсу.
Приклад загальної структури:
app.put(
'/articles/:id',
authenticate,
requireRole('admin', 'editor'),
(req, res, next) => {
// Тут можна перевірити, чи має користувач доступ до конкретної статті
next();
},
(req, res) => {
res.json({ message: 'Статтю оновлено' });
}
);Перша перевірка відповідає на питання «чи може ця роль редагувати статті?», а друга — «чи може цей користувач редагувати саме цю статтю?».
Неправильний порядок:
app.post(
'/articles',
requireRole('admin'),
authenticate,
handler
);У момент виконання requireRole властивість req.user ще не встановлена.
Правильний порядок:
app.post(
'/articles',
authenticate,
requireRole('admin'),
handler
);401 для недостатніх прав401 означає, що користувач не пройшов автентифікацію:
немає токена;
токен недійсний;
токен прострочений.
403 означає, що користувач відомий, але його роль не дозволяє виконати операцію.
Не можна брати роль із:
тіла запиту;
query-параметра;
довільного HTTP-заголовка;
даних, які користувач може змінити на клієнті.
Роль має надходити з перевіреного JWT або з бази даних.
Навіть якщо кнопка «Видалити» прихована для viewer, користувач все одно може вручну надіслати DELETE-запит.
Кожен захищений маршрут API повинен самостійно виконувати перевірку доступу.
Якщо маршрут потребує лише ролі admin, не слід дозволяти доступ усім автентифікованим користувачам:
// Надто широкий доступ для адміністративної операції
app.delete('/articles/:id', authenticate, handler);Краще явно вказати роль:
app.delete(
'/articles/:id',
authenticate,
requireRole('admin'),
handler
);У навчальному прикладі використовується значення за замовчуванням:
const JWT_SECRET = process.env.JWT_SECRET || 'development-secret';У production потрібно передавати секрет через змінну середовища:
JWT_SECRET="складний-секрет" node server.jsСекрет не слід додавати до репозиторію або публікувати разом із кодом.
RBAC визначає доступ до API на основі ролей користувача.
Автентифікація встановлює req.user, а авторизація перевіряє його роль.
Middleware requireRole дає змогу повторно використовувати логіку перевірки.
Для відсутнього або недійсного токена використовується 401.
Для автентифікованого користувача без потрібної ролі використовується 403.
Перевірки доступу повинні виконуватися на сервері.
Роль не можна приймати без перевірки з даних, які контролює клієнт.
Окрім ролі, для конкретних ресурсів може знадобитися перевірка власника або належності до команди.