Знаете, что чаще всего приводит к утечкам данных в стартапах? Не сложные хакерские атаки с использованием нулевых дней, а банальная ошибка программиста. Кто-то забыл добавить проверку прав доступа на один эндпоинт API, и вдруг обычный пользователь получил возможность удалять чужие заказы или менять настройки администратора. Звучит страшно, правда? А ведь это происходит постоянно.
Многие разработчики путают понятия «аутентификация» и «авторизация». Аутентификация отвечает на вопрос «Кто ты?», а авторизация - «Что тебе разрешено делать?». Сегодня мы разберем, как построить надежную систему проверки прав, чтобы ваш код спал спокойно, а пользователи не наткнулись на дыры в безопасности. Мы поговорим о моделях RBAC и ABAC, типичных ловушках и конкретных примерах реализации.
Почему простая проверка «is_admin» не работает
На начальном этапе проекта часто кажется, что достаточно одной булевой переменной isAdmin в таблице пользователей. Если она равна true - пускай дальше, если false - стоп. Но бизнес-логика быстро усложняется. Появляется менеджер, который может редактировать товары, но не может их удалять. Появляется бухгалтер, которому нужен доступ только к отчетам. И вот уже ваша переменная превращается в спагетти из условий:
if (user.isAdmin || user.isManager && action == 'edit') { ... }
Такой подход ломается при первом же масштабировании. Вы теряете контроль над тем, кто именно имеет доступ к каким данным. Чтобы избежать хаоса, нужно внедрить четкую модель управления доступом. Самой популярной и понятной для большинства приложений является модель RBAC (Role-Based Access Control). Это система, где права назначаются не конкретному человеку, а его роли.
Разбираемся с RBAC: Роли, Разрешения и Пользователи
В основе RBAC лежит принцип минимальных привилегий. Каждый пользователь получает одну или несколько ролей (например, «Администратор», «Редактор», «Гость»). Каждая роль содержит набор разрешений (permissions). Например, роль «Редактор» может иметь разрешения post.create, post.edit, но не post.delete.
Как это выглядит на практике? Представьте себе интернет-магазин. У вас есть три ключевые сущности:
- Пользователь (User): Конкретный человек, например, Иван Петров.
- Роль (Role): Группа обязанностей, например, «Сотрудник склада».
- Разрешение (Permission): Конкретное действие, например,
inventory.update.
Связь между ними должна быть гибкой. Один пользователь может иметь несколько ролей (быть одновременно «Менеджером» и «Поддержкой»). Одна роль может включать сотни разрешений. Главная задача - хранить эту структуру так, чтобы ее можно было легко изменять без переписывания кода.
| Роль | Разрешения | Доступные ресурсы |
|---|---|---|
| Guest | view.public_posts | Статьи блога |
| User | view.public_posts, create.comment | Статьи, комментарии |
| Moderator | view.all_posts, edit.comment, delete.comment | Все посты, все комментарии |
| Admin | manage.users, view.logs, * (все действия) | Вся система |
Где проверять права: Бэкенд или Фронтенд?
Это классический вопрос, на который новички часто отвечают неправильно. Многие думают: «Если я скрыл кнопку «Удалить» во фронтенде, значит, пользователь не сможет удалить запись». Ошибка! Любой злоумышленник может отправить прямой HTTP-запрос через Postman или curl, минуя интерфейс полностью.
Единственное место, где действительно происходит проверка прав - это серверная часть (бэкенд). Фронтенд нужен только для UX: скрывать недоступные кнопки, чтобы не бесить пользователя лишними ошибками 403 Forbidden. Но финальное слово всегда за сервером.
Типичная ошибка: проверка прав выполняется только в контроллере одного метода, но забывается в другом. Или проверка есть в сервисном слое, но отсутствует в репозитории. Лучшая практика - использовать middleware (промежуточное ПО) или декораторы, которые автоматически проверяют наличие нужного разрешения перед выполнением логики.
Проблема масштабируемости: Когда RBAC становится тесным
RBAC отлично работает до определенного момента. Но представьте корпоративную CRM. Менеджер А должен видеть клиентов только своего региона. Менеджер Б видит всех клиентов своего отдела. При этом оба они имеют одинаковую роль «Менеджер по продажам». В классическом RBAC вам придется создавать отдельные роли для каждого региона и отдела, что приведет к взрывному росту количества ролей.
Здесь на помощь приходит ABAC (Attribute-Based Access Control). Вместо жестких ролей здесь используются атрибуты. Система проверяет условия на лету: если user.department == resource.department И user.role == 'manager' ТОГДА allow.
ABAC сложнее в реализации и отладке, но он дает невероятную гибкость. Для большинства веб-приложений гибридный подход работает лучше всего: базовые уровни доступа через RBAC (админ, юзер), а тонкая настройка видимости данных - через ABAC (фильтрация выборок из базы данных).
Технические нюансы реализации
Давайте посмотрим, как это можно реализовать в коде. Допустим, вы используете Node.js с Express или Python с Django/FastAPI. Вам понадобится механизм, который будет сопоставлять токен пользователя с требуемым разрешением.
Важный момент: никогда не храните список всех разрешений пользователя в токене JWT, если их много. Токен станет огромным, а изменение прав потребует перевыпуска токена всем пользователям. Лучше хранить в токене только ID пользователя и роль, а полный список разрешений кэшировать на сервере (например, в Redis).
Вот псевдокод того, как должен выглядеть middleware проверки:
// Псевдокод middleware
function checkPermission(requiredPerm) {
return async (req, res, next) => {
const userId = req.user.id;
// Получаем роли пользователя из кэша или БД
const roles = await getRoles(userId);
// Собираем все разрешения из этих ролей
const permissions = await getPermissionsFromRoles(roles);
if (!permissions.includes(requiredPerm)) {
return res.status(403).send('Access Denied');
}
next();
};
}
Обратите внимание на статус 403 Forbidden. Он отличается от 401 Unauthorized. Первый означает «ты не тот, кого я жду», второй - «ты не представился». Смешивать их нельзя, это сбивает с толку и клиентов, и системы мониторинга.
Частые ошибки и как их избежать
За годы работы с разными проектами я выделил топ-3 ошибки, которые убивают безопасность системы прав:
- Жесткое кодирование ID ролей. Никогда не пишите
if (roleId == 1). Используйте константы или имена ролей. Завтра база мигрирует, и ID 1 станет пустым местом. - Отсутствие аудита изменений. Кто дал пользователю роль «Админ»? Когда? Если вы не ведете лог действий (audit log), расследовать инциденты будет невозможно.
- Игнорирование кеширования. Запрос списка разрешений к базе данных на каждый API-вызов убьет производительность. Кешируйте пары «пользователь-разрешения» с коротким временем жизни (TTL).
Также следите за тем, чтобы проверка прав происходила до любых операций с данными. Нет смысла доставать объект из базы, чтобы потом понять, что у пользователя нет прав на его просмотр. Это лишний ввод-вывод.
Инструменты и библиотеки
Не обязательно писать свою систему с нуля. Экосистема предлагает готовые решения, которые прошли проверку временем.
| Инструмент | Язык/Фреймворк | Особенности |
|---|---|---|
| CanCanCan | Ruby on Rails | Простая декларативная синтаксис, интеграция с ActiveRecord. |
| Laravel Gates/Policies | PHP (Laravel) | Встроенная поддержка, отличная документация, гибкие политики. |
| Casbin | Multi-language (Go, Java, JS...) | Поддержка RBAC, ABAC, ACL. Мощный движок политик. |
| Spring Security | Java | Стандарт де-факто для enterprise Java приложений. |
Выбор зависит от вашего стека. Для небольших проектов часто хватает встроенных механизмов фреймворка. Для сложных систем с динамическими политиками стоит присмотреться к Casbin или внешним провайдерам вроде Keycloak, если нужна централизованная авторизация для микросервисов.
Практический чек-лист перед релизом
Перед тем как выкатить новую версию приложения в продакшен, пройдитесь по этому списку:
- [ ] Все новые эндпоинты защищены middleware проверки прав?
- [ ] Проверены ли случаи, когда пользователь меняет роль прямо во время сессии? (Нужен инвалидирование кеша)
- [ ] Есть ли защита от эскалации привилегий? (Нельзя самому себе дать роль Админа)
- [ ] Логируются ли попытки несанкционированного доступа?
- [ ] Протестированы ли граничные случаи? (Пользователь без ролей, удаленный пользователь)
Помните: безопасность - это не функция, которую можно включить галочкой. Это процесс. Регулярно пересматривайте матрицу доступов вместе с бизнес-заказчиками. То, что было актуально полгода назад, сегодня может стать дырой в безопасности.
Чем отличается RBAC от ACL?
ACL (Access Control List) связывает разрешение напрямую с объектом и пользователем (или группой). RBAC связывает пользователя с ролью, а роль с разрешением. RBAC проще администрировать в больших системах, так как изменение роли влияет на всех ее носителей сразу, тогда как в ACL нужно менять списки у каждого объекта отдельно.
Можно ли хранить права в JWT токене?
Можно, но осторожно. Если прав мало и они редко меняются - это ускоряет работу, так как не нужно ходить в базу. Но если прав много, токен станет тяжелым. Плюс, если вы отзовете права, старый токен все еще будет работать до истечения срока годности. Поэтому для критичных операций лучше делать сверку с сервером или использовать короткие сроки жизни токенов.
Что делать, если у пользователя несколько ролей с конфликтующими правами?
Обычно применяется принцип объединения (union). Если в одной роли есть право «читать», а в другой «удалять», итоговый набор прав включает оба действия. Исключение составляют явные запреты (deny rules), которые имеют приоритет над разрешениями, но такая логика встречается реже и требует более сложной реализации.
Как проверить права на уровне строк в базе данных?
Для этого используют Row-Level Security (RLS) в PostgreSQL или фильтры в ORM. Например, запрос всегда добавляет условие WHERE owner_id = current_user_id, если у пользователя нет глобальной роли администратора. Это защищает данные на уровне SQL, исключая человеческий фактор при написании запросов.
Нужна ли отдельная таблица для истории изменения ролей?
Да, это настоятельно рекомендуется. Таблица audit_log должна фиксировать: кто изменил роль, кому, какая роль была назначена/снята, дата и время. Это необходимо для соответствия стандартам безопасности (SOC2, GDPR) и для быстрого расследования инцидентов.