Пошук уроків, статей та іншого контенту
Реалізуєте автентифікацію користувачів і розмежування доступу до ресурсів за ролями та дозволами.
У застосунках потрібно розв’язати дві різні задачі:
Автентифікація — перевірка, хто саме виконує запит.
Авторизація — перевірка, що автентифікований користувач має право виконати цю дію.
Наприклад:
Користувач надсилає email і пароль.
Сервер перевіряє облікові дані та видає токен.
Користувач додає токен до наступних запитів.
Сервер перевіряє токен.
Сервер перевіряє роль або дозвіл користувача.
Якщо доступ дозволено, сервер виконує операцію.
Паролі не можна зберігати у відкритому вигляді:
password123Якщо база даних буде скомпрометована, зловмисник одразу отримає всі паролі.
Замість цього пароль перетворюють на хеш за допомогою алгоритму, призначеного для паролів, наприклад bcrypt. Під час входу введений пароль порівнюють із хешем:
введений пароль → bcrypt.compare() → результат true або falseХешування є одностороннім: із хешу не можна відновити початковий пароль.
Після успішного входу сервер може видати клієнту JWT-токен. Токен містить ідентифікатор користувача та строк дії й підписаний секретним ключем сервера.
Клієнт передає токен у заголовку:
Authorization: Bearer <token>Сервер перевіряє підпис і строк дії токена перед обробкою захищеного маршруту.
JWT не повинен містити пароль або інші секретні дані. Дані всередині JWT можна прочитати, тому підпис токена гарантує цілісність, а не шифрування.
Після автентифікації сервер знає, хто виконує запит, але цього недостатньо. Потрібно перевірити права користувача.
Поширена модель доступу:
роль — набір пов’язаних прав, наприклад admin або editor;
дозвіл — конкретна дія над ресурсом, наприклад articles:read або users:delete.
Наприклад:
адміністратор може керувати користувачами;
редактор може створювати та редагувати статті;
звичайний користувач може лише читати статті.
У Node.js middleware може перевірити токен до виконання основного обробника маршруту.
Типовий порядок middleware:
отримати токен із заголовка;
перевірити його підпис;
знайти користувача;
додати користувача до req;
передати керування наступному middleware;
повернути 401, якщо користувач не автентифікований.
Код 401 Unauthorized означає, що автентифікація відсутня або невірна.
Код 403 Forbidden означає, що користувач автентифікований, але не має потрібних прав.
Приклад використовує:
express для HTTP-сервера;
bcryptjs для хешування та перевірки паролів;
jsonwebtoken для створення і перевірки JWT.
Встановіть залежності:
npm init -y
npm install express bcryptjs jsonwebtokenСтворіть файл server.js:
const express = require("express");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const app = express();
app.use(express.json());
const port = 3000;
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error("Задайте змінну середовища JWT_SECRET");
}
// У реальному застосунку користувачі зберігаються в базі даних.
let users = [];
function createToken(user) {
return jwt.sign(
{
sub: user.id
},
jwtSecret,
{
expiresIn: "15m"
}
);
}
async function authenticate(req, res, next) {
const authorization = req.headers.authorization;
if (!authorization || !authorization.startsWith("Bearer ")) {
return res.status(401).json({
error: "Потрібен токен доступу"
});
}
const token = authorization.slice("Bearer ".length);
try {
const payload = jwt.verify(token, jwtSecret);
const user = users.find((item) => item.id === payload.sub);
if (!user) {
return res.status(401).json({
error: "Користувача не знайдено"
});
}
const { passwordHash, ...safeUser } = user;
// Зберігаємо автентифікованого користувача для наступних middleware.
req.user = safeUser;
next();
} catch (error) {
return res.status(401).json({
error: "Недійсний або прострочений токен"
});
}
}
function authorize({ roles = [], permissions = [] } = {}) {
return (req, res, next) => {
const user = req.user;
const hasRequiredRole =
roles.length === 0 ||
roles.some((role) => user.roles.includes(role));
const hasRequiredPermissions = permissions.every((permission) =>
user.permissions.includes(permission)
);
if (!hasRequiredRole || !hasRequiredPermissions) {
return res.status(403).json({
error: "Недостатньо прав для виконання операції"
});
}
next();
};
}
app.post("/login", async (req, res) => {
const { email, password } = req.body;
if (typeof email !== "string" || typeof password !== "string") {
return res.status(400).json({
error: "Email і пароль є обов'язковими"
});
}
const user = users.find((item) => item.email === email);
// Не повідомляємо, чи існує такий email.
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({
error: "Невірний email або пароль"
});
}
const token = createToken(user);
res.json({
token,
user: {
id: user.id,
email: user.email,
roles: user.roles,
permissions: user.permissions
}
});
});
app.get("/profile", authenticate, (req, res) => {
res.json({
user: req.user
});
});
app.get(
"/articles",
authenticate,
authorize({ permissions: ["articles:read"] }),
(req, res) => {
res.json({
articles: [
{ id: 1, title: "Вступ до Node.js" },
{ id: 2, title: "Middleware в Express" }
]
});
}
);
app.post(
"/articles",
authenticate,
authorize({ permissions: ["articles:create"] }),
(req, res) => {
res.status(201).json({
message: "Статтю створено",
title: req.body.title
});
}
);
app.get(
"/admin/users",
authenticate,
authorize({ roles: ["admin"] }),
(req, res) => {
res.json({
users: users.map(({ passwordHash, ...user }) => user)
});
}
);
async function start() {
users = [
{
id: "user-1",
email: "user@example.com",
passwordHash: await bcrypt.hash("user-password", 12),
roles: ["user"],
permissions: ["articles:read"]
},
{
id: "editor-1",
email: "editor@example.com",
passwordHash: await bcrypt.hash("editor-password", 12),
roles: ["editor"],
permissions: ["articles:read", "articles:create"]
},
{
id: "admin-1",
email: "admin@example.com",
passwordHash: await bcrypt.hash("admin-password", 12),
roles: ["admin"],
permissions: [
"articles:read",
"articles:create",
"users:read"
]
}
];
app.listen(port, () => {
console.log(`Сервер запущено на http://localhost:${port}`);
});
}
start().catch((error) => {
console.error("Не вдалося запустити сервер:", error);
process.exit(1);
});Запустіть сервер:
JWT_SECRET="дуже-довгий-випадковий-секрет" node server.jsУ Windows PowerShell:
$env:JWT_SECRET="дуже-довгий-випадковий-секрет"
node server.jsНадішліть запит із даними користувача:
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email":"editor@example.com","password":"editor-password"}'Сервер поверне приблизно такий результат:
{
"token": "eyJ...",
"user": {
"id": "editor-1",
"email": "editor@example.com",
"roles": ["editor"],
"permissions": ["articles:read", "articles:create"]
}
}Токен потрібно зберегти на стороні клієнта та передати в наступному запиті:
curl http://localhost:3000/articles \
-H "Authorization: Bearer eyJ..."У цьому прикладі редактор отримає список статей, оскільки має дозвіл articles:read.
Маршрут /admin/users захищений роллю admin:
app.get(
"/admin/users",
authenticate,
authorize({ roles: ["admin"] }),
handler
);Користувач із роллю editor буде автентифікований, але отримає відповідь 403, оскільки не має ролі admin.
Маршрут може вимагати конкретний дозвіл:
authorize({
permissions: ["articles:create"]
});У прикладі метод every означає, що для доступу користувач повинен мати всі перелічені дозволи:
const hasRequiredPermissions = permissions.every((permission) =>
user.permissions.includes(permission)
);Якщо потрібно дозволити дію за наявності хоча б одного з кількох дозволів, можна використати some:
const hasAnyPermission =
permissions.length === 0 ||
permissions.some((permission) =>
user.permissions.includes(permission)
);Обирайте логіку перевірки залежно від правил конкретного ресурсу.
Секретний ключ JWT потрібно передавати через змінну середовища:
const jwtSecret = process.env.JWT_SECRET;Не додавайте файл із секретами до системи контролю версій.
Для bcrypt параметр salt rounds впливає на складність обчислення хешу. Значення 12 є прикладом налаштування, але в конкретному проєкті його слід обирати з урахуванням продуктивності сервера та актуальних рекомендацій.
Навіть хеш пароля не потрібно включати у відповіді API. У прикладі для цього використовується деструктуризація:
const { passwordHash, ...safeUser } = user;Короткоживучий access token зменшує наслідки його викрадення. У прикладі токен дійсний 15 хвилин:
expiresIn: "15m"Механізм оновлення токенів має бути окремо продуманий для конкретного застосунку.
Роль потрібно отримувати з перевіреного джерела, наприклад із бази даних. Не можна дозволяти клієнту надсилати:
{
"role": "admin"
}і без додаткової перевірки використовувати це значення для авторизації.
У production-застосунку після перевірки JWT зазвичай знаходять користувача в базі даних і перевіряють його актуальний статус, ролі та дозволи.
401 і 403401 — токен відсутній, неправильний або прострочений.
403 — токен правильний, але прав недостатньо.
Навіть внутрішня база даних не є безпечним місцем для відкритих паролів. Використовуйте bcrypt або інший спеціалізований алгоритм для хешування паролів.
JWT не призначений для зберігання секретів. Клієнт або інша сторона може декодувати його payload.
Перевірка ролей повинна виконуватися після middleware, яке встановлює req.user:
app.get(
"/admin/users",
authenticate,
authorize({ roles: ["admin"] }),
handler
);Якщо поміняти порядок middleware, авторизація не матиме перевірених даних про користувача.
Відповіді на кшталт «такого email не існує» допомагають перевіряти наявність акаунтів. Для входу краще повертати загальне повідомлення:
Невірний email або парольЯкщо ключ потрапить до репозиторію або журналів, зловмисник зможе створювати дійсні токени. Зберігайте ключ у змінних середовища та замінюйте його за потреби.
Автентифікація визначає особу користувача.
Авторизація визначає дозволені дії.
Паролі потрібно зберігати лише у вигляді безпечних хешів.
JWT можна використовувати як токен доступу між запитами.
Захищені маршрути реалізуються через middleware.
Ролі описують групи прав, а дозволи — конкретні операції.
401 використовується для проблем з автентифікацією, 403 — для недостатніх прав.
Секрети не можна зберігати у вихідному коді або передавати клієнту.