Пошук уроків, статей та іншого контенту
Побудуєте рольову модель доступу та обмежите функції застосунку для адміністратора, редактора й користувача.
Role-Based Access Control (RBAC) — це модель, у якій права призначаються не окремим користувачам, а ролям.
Наприклад, застосунок може мати такі ролі:
admin — керує користувачами та всіма матеріалами;
editor — створює й редагує матеріали;
user — переглядає опублікований контент.
Користувач отримує набір дозволів через свою роль:
Користувач → Роль → ДозволиЦе спрощує керування доступом. Якщо десять редакторів мають однакові права, не потрібно налаштовувати дозволи для кожного окремо.
У простому застосунку достатньо перевіряти роль безпосередньо:
admin → доступ до адміністративної панелі
editor → створення та редагування статей
user → перегляд статейПроте зручніше описати дозволи явно:
admin:
- articles.read
- articles.create
- articles.update
- articles.delete
- users.manage
editor:
- articles.read
- articles.create
- articles.update
user:
- articles.readТакий підхід дозволяє згодом додати нові ролі без переписування всіх перевірок.
У прикладі використовується Next.js App Router і TypeScript:
app/
admin/
page.tsx
articles/
actions.ts
page.tsx
forbidden/
page.tsx
page.tsx
lib/
auth.tsДля демонстрації сесія зберігатиметься у cookie session. Значення cookie буде ідентифікатором користувача:
session=admin-session
session=editor-session
session=user-sessionУ реальному застосунку cookie повинна містити захищену сесію або токен, який перевіряється сервером. Не можна довіряти довільному значенню cookie, яке користувач може змінити вручну.
Створимо файл lib/auth.ts:
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export type Role = "admin" | "editor" | "user";
export type Permission =
| "articles.read"
| "articles.create"
| "articles.update"
| "articles.delete"
| "users.manage";
export type User = {
id: string;
name: string;
role: Role;
};
const rolePermissions: Record<Role, Permission[]> = {
admin: [
"articles.read",
"articles.create",
"articles.update",
"articles.delete",
"users.manage",
],
editor: ["articles.read", "articles.create", "articles.update"],
user: ["articles.read"],
};
// Демонстраційні дані замість справжньої перевірки сесії в базі даних.
const sessions: Record<string, User> = {
"admin-session": {
id: "1",
name: "Анна",
role: "admin",
},
"editor-session": {
id: "2",
name: "Олег",
role: "editor",
},
"user-session": {
id: "3",
name: "Марія",
role: "user",
},
};
export async function getCurrentUser(): Promise<User | null> {
const cookieStore = await cookies();
const sessionId = cookieStore.get("session")?.value;
if (!sessionId) {
return null;
}
return sessions[sessionId] ?? null;
}
export function hasPermission(
role: Role,
permission: Permission,
): boolean {
return rolePermissions[role].includes(permission);
}
export async function requireUser(): Promise<User> {
const user = await getCurrentUser();
if (!user) {
redirect("/");
}
return user;
}
export async function requirePermission(
permission: Permission,
): Promise<User> {
const user = await requireUser();
if (!hasPermission(user.role, permission)) {
redirect("/forbidden");
}
return user;
}
export async function requireRole(
...allowedRoles: Role[]
): Promise<User> {
const user = await requireUser();
if (!allowedRoles.includes(user.role)) {
redirect("/forbidden");
}
return user;
}Без централізованих функцій перевірки можуть дублюватися у різних компонентах:
if (user.role !== "admin") {
// ...
}Це створює кілька проблем:
перевірки стають непослідовними;
легко забути перевірити доступ у новій функції;
зміна правил потребує пошуку по всьому проєкту;
роль починає використовуватися замість конкретного дозволу.
Функції requirePermission і requireRole задають єдину точку для серверної авторизації.
Створимо app/admin/page.tsx:
import { requireRole } from "@/lib/auth";
export default async function AdminPage() {
const user = await requireRole("admin");
return (
<main>
<h1>Адміністративна панель</h1>
<p>Вітаємо, {user.name}.</p>
<ul>
<li>Керування користувачами</li>
<li>Видалення статей</li>
<li>Перегляд системних налаштувань</li>
</ul>
</main>
);
}Якщо сторінку відкриє редактор або звичайний користувач, requireRole("admin") перенаправить його на /forbidden.
Сама сторінка є серверним компонентом, тому перевірка виконується на сервері ще до формування відповіді.
Створимо app/articles/page.tsx:
import Link from "next/link";
import { getCurrentUser, hasPermission } from "@/lib/auth";
export default async function ArticlesPage() {
const user = await getCurrentUser();
const canCreate = user
? hasPermission(user.role, "articles.create")
: false;
const canDelete = user
? hasPermission(user.role, "articles.delete")
: false;
return (
<main>
<h1>Статті</h1>
{user ? (
<p>
Ви увійшли як {user.name}. Роль: {user.role}.
</p>
) : (
<p>Ви не авторизовані.</p>
)}
<ul>
<li>Стаття про Next.js</li>
<li>Стаття про TypeScript</li>
</ul>
{canCreate && (
<Link href="/articles/new">
Створити статтю
</Link>
)}
{canDelete && <p>Вам доступне видалення статей.</p>}
</main>
);
}Тут перевірка використовується для відображення або приховування елементів інтерфейсу.
Однак приховування кнопки не є захистом. Користувач може вручну відправити HTTP-запит або викликати серверну дію. Тому кожна операція, яка змінює дані, також повинна перевіряти дозвіл на сервері.
Створимо app/articles/actions.ts:
"use server";
import { requirePermission } from "@/lib/auth";
export async function createArticle(formData: FormData) {
const user = await requirePermission("articles.create");
const title = formData.get("title");
if (typeof title !== "string" || title.trim().length < 3) {
throw new Error("Назва статті повинна містити щонайменше 3 символи.");
}
// У реальному застосунку тут буде запис до бази даних.
console.log({
title: title.trim(),
createdBy: user.id,
});
}Редактор має дозвіл articles.create, тому дія буде виконана. Звичайний користувач цього дозволу не має й буде перенаправлений на /forbidden.
Форма для виклику дії може виглядати так:
import { createArticle } from "../actions";
export default function NewArticlePage() {
return (
<main>
<h1>Нова стаття</h1>
<form action={createArticle}>
<label htmlFor="title">Назва</label>
<input id="title" name="title" required />
<button type="submit">Створити</button>
</form>
</main>
);
}Важливо захистити не тільки сторінку /articles/new, а й саму дію createArticle. Інакше користувач без доступу зможе викликати операцію напряму.
Створимо app/forbidden/page.tsx:
export default function ForbiddenPage() {
return (
<main>
<h1>Доступ заборонено</h1>
<p>
У вашої ролі немає дозволу на виконання цієї операції.
</p>
</main>
);
}Окрема сторінка для забороненого доступу зрозуміліша для користувача, ніж загальна помилка сервера.
Перевірка ролі підходить для повністю рольових сторінок:
await requireRole("admin");Її можна використовувати для адміністративної панелі.
Перевірка дозволу краще підходить для конкретних операцій:
await requirePermission("articles.update");Переваги дозволів:
операція не залежить від назви ролі;
кілька ролей можуть мати однаковий дозвіл;
легше додати нову роль;
правила доступу описані в одному місці.
Наприклад, якщо згодом з’явиться роль moderator, їй можна надати articles.update, не змінюючи код сторінки редагування.
Перевірки потрібно виконувати на сервері в усіх точках, де доступ може вплинути на дані:
у серверних компонентах сторінок;
у Server Actions;
у Route Handlers;
перед читанням або зміною захищених даних.
Перевірки на клієнті корисні лише для інтерфейсу:
{canCreate && <button>Створити</button>}Але вони не захищають дані, оскільки клієнтський код можна змінити або обійти.
Правильний порядок дій для захищеної операції:
отримати поточного користувача;
перевірити його сесію;
перевірити необхідний дозвіл;
перевірити вхідні дані;
виконати операцію з базою даних.
Ці поняття виконують різні функції:
автентифікація відповідає на питання: «Хто це?»;
авторизація відповідає на питання: «Що цій особі дозволено?».
У прикладі:
getCurrentUser()визначає поточного користувача, а:
hasPermission(user.role, "articles.create")перевіряє його дозвіл.
Не слід вважати, що наявність користувача автоматично означає доступ до всіх функцій.
Для локальної перевірки можна встановити cookie session у браузері.
Використовуйте такі значення:
admin-session
editor-session
user-sessionОчікувана поведінка:
admin-session відкриває адміністративну панель і може створювати, редагувати та видаляти статті;
editor-session може створювати й редагувати статті, але не відкриває адміністративну панель;
user-session може переглядати статті, але не може створювати їх;
без cookie користувач вважається неавторизованим.
Під час тестування перевіряйте не лише видимість кнопок, а й безпосередній виклик захищених серверних дій.
{user.role === "admin" && <button>Видалити</button>}Це змінює лише відображення кнопки. Серверна операція видалення все одно повинна перевіряти дозвіл:
await requirePermission("articles.delete");Не слід передавати роль у прихованому полі форми або зберігати її в ненадійному клієнтському стані:
<input type="hidden" name="role" value="admin" />Користувач може змінити це значення. Роль потрібно отримувати із серверної сесії.
Захист сторінки /articles/new не захищає Server Action, Route Handler або іншу функцію, яка створює статтю. Перевірка повинна бути безпосередньо поруч із критичною операцією.
Такий код працює, але погано масштабується:
if (user.role === "admin" || user.role === "editor") {
// створення статті
}Краще перевіряти дозвіл:
await requirePermission("articles.create");У прикладі cookie містить простий рядок, щоб зосередитися на RBAC. У production потрібно використовувати справжню систему сесій із серверною перевіркою, захищеною cookie та коректною валідацією користувача.
RBAC призначає дозволи ролям, а не окремим користувачам.
Ролі admin, editor і user можуть мати різні набори дозволів.
Перевірки доступу потрібно виконувати на сервері.
Приховування кнопок не замінює авторизацію.
Для сторінок зручно перевіряти роль через requireRole.
Для окремих операцій краще перевіряти дозвіл через requirePermission.
Server Actions, Route Handlers і доступ до даних повинні мати власні перевірки.
Дані про роль не можна приймати від клієнта — їх потрібно отримувати із перевіреної серверної сесії.