Реализация role-based access control во Vue 3 с помощью Vue Router

В любом серьезном корпоративном приложении рано или поздно встает вопрос разграничения прав доступа. Кто-то должен видеть только панель управления заказами, кто-то — отчеты по аналитике, а супер-админ — вообще всё, включая настройки системы и управление пользователями.
Реализация Role-Based Access Control (RBAC) на фронтенде — это не столько про безопасность (так как любой клиентский код можно взломать или обойти через DevTools), сколько про пользовательский опыт (UX). Мы не должны показывать пользователю кнопки, которые выдадут 403 ошибку при нажатии, и не должны позволять ему заходить на страницы, где он ничего не может сделать.
В этой статье мы разберем, как выстроить гибкую систему прав в связке Vue 3 + Vue Router + Pinia.
Архитектурный подход: где хранить правду?
Прежде чем писать код, определимся с иерархией. Ошибка новичка — жестко прописывать роли прямо в компонентах (if (user.role === 'admin')). Это путь в ад, потому что при изменении названия роли или добавлении новой (например, «модератор») вам придется перерыть весь проект.
Правильный подход:
- Backend возвращает список ролей или конкретных прав (permissions) пользователя при авторизации.
- Store (Pinia) хранит эти данные в состоянии приложения.
- Vue Router выступает в роли «вышибалы», проверяя метаданные маршрута перед переходом.
- Custom Directive скрывает элементы интерфейса.
Шаг 1: Определение метаданных маршрутов
Vue Router позволяет добавлять любой объект в поле meta. Именно здесь мы опишем, какие роли имеют доступ к конкретной странице.
javascript
// router/index.js
import { createRouter, createWebHistory } from ‘vue-router’;
const routes = [
{
path: ‘/’,
component: () => import(‘./views/Home.vue’),
meta: { requiresAuth: true } // Доступно всем авторизованным
},
{
path: ‘/admin’,
component: () => import(‘./views/AdminPanel.vue’),
meta: {
requiresAuth: true,
roles: [‘admin’] // Только для админов
}
},
{
path: ‘/manager’,
component: () => import(‘./views/ManagerDashboard.vue’),
meta: {
requiresAuth: true,
roles: [‘admin’, ‘manager’] // Для админов и менеджеров
}
},
{
path: ‘/login’,
component: () => import(‘./views/Login.vue’),
meta: { guestOnly: true } // Только для неавторизованных
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
Шаг 2: Создание хранилища пользователя (Pinia)
Нам нужно место, где будет храниться информация о текущем пользователе. Важно, чтобы данные были доступны из любого места приложения, включая навигационные гарды роутера.
javascript
// stores/auth.js
import { defineStore } from ‘pinia’;
export const useAuthStore = defineStore(‘auth’, {
state: () => ({
user: JSON.parse(localStorage.getItem(‘user’)) || null,
token: localStorage.getItem(‘token’) || null,
}),
getters: {
isAuthenticated: (state) => !!state.token,
hasRole: (state) => {
return (role) => state.user?.roles?.includes(role);
},
hasAnyRole: (state) => {
return (roles) => roles.some(r => state.user?.roles?.includes(r));
}
},
actions: {
setUser(userData, token) {
this.user = userData;
this.token = token;
localStorage.setItem(‘user’, JSON.stringify(userData));
localStorage.setItem(‘token’, token);
},
logout() {
this.user = null;
this.token = null;
localStorage.clear();
}
}
});
Шаг 3: Глобальный навигационный гард (Navigation Guard)
Теперь самое интересное — реализация логики проверки доступа. Мы будем использовать router.beforeEach.
javascript
// router/index.js (продолжение)
import { useAuthStore } from ‘@/stores/auth’;
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
// 1. Проверка на «только для гостей» (например, страница логина)
if (to.meta.guestOnly && authStore.isAuthenticated) {
return next({ name: ‘Home’ });
}
// 2. Проверка на необходимость авторизации
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return next({ name: ‘Login’ });
}
// 3. Проверка ролей (RBAC)
if (to.meta.roles) {
const hasAccess = authStore.hasAnyRole(to.meta.roles);
if (!hasAccess) {
// Если доступа нет, отправляем на страницу 403 или на главную
return next({ name: ‘Forbidden’ });
}
}
next(); // Если всё ок, пропускаем пользователя
});
Шаг 4: Управление видимостью элементов (Custom Directive)
Скрывать целые страницы — это хорошо, но пользователь не должен видеть кнопку «Удалить пользователя», если у него нет прав администратора. Вместо того чтобы писать v-if в каждом компоненте, создадим кастомную директиву v-role.
javascript
// main.js
const app = createApp(App);
app.directive(‘role’, {
mounted(el, binding) {
const authStore = useAuthStore();
const requiredRoles = binding.value; // Массив ролей из v-role=»[‘admin’]»
if (!authStore.hasAnyRole(requiredRoles)) {
// Полностью удаляем элемент из DOM
el.parentNode?.removeChild(el);
}
}
});
Использование в шаблоне:
<button v-role=»[‘admin’]» @click=»deleteUser»>Удалить
Нюансы и «подводные камни» при реализации
Когда вы перенесете эту схему в реальный проект, вы столкнетесь с несколькими проблемами. Вот как их решать:
- Обновление страницы (F5):
При перезагрузке страницы состояние Pinia сбрасывается. Если вы храните пользователя вlocalStorage, убедитесь, что вы инициализируете Store до того, как сработаетrouter.beforeEach. Иначе пользователь будет выкинут на страницу логина, даже если он авторизован. - Динамические права с бэкенда:
Иногда роли слишком грубые. Вам могут понадобиться «пермишены» (например,users.edit,reports.view). В этом случае вmetaмаршрутов пишите не роли, а конкретные права, а в Store храните массивpermissions. Логика проверки останется той же. - Безопасность API:
Помните: фронтенд-защита — это иллюзия. Опытный пользователь может через Vue DevTools изменить состояние Store и «включить» себе роль админа, чтобы увидеть скрытые кнопки. Поэтому каждый запрос к API должен проходить проверку JWT-токена и прав доступа на стороне сервера. Если фронтенд пропустил пользователя на страницу/admin, но бэкенд вернул403 Forbiddenпри запросе данных — это нормально и правильно. - Сложные условия доступа:
Бывают случаи, когда доступ зависит не от роли, а от владения объектом (например, пользователь может редактировать только свой профиль). Такие проверки нельзя делать в роутере, их нужно реализовывать внутри компонентов или через специальные функции-хелперы в Store.
Итоговый чек-лист реализации
Чтобы ваша система RBAC была надежной и поддерживаемой, следуйте этому списку:
- [ ] Все маршруты с ограниченным доступом помечены в
meta. - [ ] Логика проверки прав вынесена в геттеры Pinia (чтобы не дублировать код).
- [ ] Глобальный гард роутера обрабатывает три состояния: гость, авторизован, имеет нужную роль.
- [ ] Интерфейс очищается от лишних элементов с помощью кастомной директивы.
- [ ] Бэкенд дублирует все проверки прав доступа.
Реализация RBAC таким образом делает код чистым, модульным и легко расширяемым. Добавление новой роли теперь занимает пару минут: вы просто добавляете её в массив разрешенных ролей в конфиге роутера и в нужных местах шаблона.

