Роли и доступы в личном кабинете: как не отдать лишнего
Роли и права доступа в личном кабинете живут на сервере, а не в меню. Разбираю три уровня проверки, типовые дыры, токены с отзывом и журнал действий.
Роли и права доступа в личном кабинете проверяются на сервере, а не в интерфейсе. Скрытая кнопка в меню не защищает ничего: запрос к API отправляется руками из консоли браузера за десять секунд, и если сервер не проверил, кто пришёл и на что он имеет право, данные уедут. Всё, что происходит на стороне клиента, - это удобство. Защита начинается там, куда пользователь не дотягивается.
Дальше - как это устроено по слоям, где чаще всего протекает и что из этого стоит сделать даже в маленьком сервисе.
Разработка личного кабинетаот 150 000 до 500 000 ₽сроки: 4-10 недель
Три уровня проверки
Маршрут. Гард роутера не пускает пользователя на страницу, которая ему не полагается. Попытка открыть адрес админки перебрасывает на главную. Это нужно, чтобы человек не попадал на пустой сломанный экран, и только для этого.
Элемент интерфейса. Кнопка «удалить» не рисуется у роли, которая не имеет права удалять. Поле «скидка» скрыто от менеджера без полномочий. Тоже удобство: пользователь не тычется в то, что ему всё равно откажут.
Эндпоинт. Сервер получает запрос, достаёт из токена, кто это, и решает, можно ли выполнить операцию. Здесь и только здесь происходит защита.
Первые два уровня без третьего - декорация. Разработчик, который спрятал кнопку и на этом остановился, сделал сервис, где любой пользователь с открытым DevTools выполняет операции администратора. Проверка на клиенте существует ровно для того, чтобы пользователю было понятнее, а не для того, чтобы ему было нельзя.
Аутентификация и авторизация - разные вещи
Их путают постоянно, а ошибки они дают разные.
Аутентификация отвечает на вопрос «кто ты». Логин с паролем, код из SMS, вход через Apple или Google, биометрия на телефоне. Результат - сервер знает идентификатор пользователя.
Авторизация отвечает на вопрос «что тебе можно». Результат - решение по конкретному запросу: выполнить или отказать.
Успешная аутентификация не даёт никаких прав. Пользователь вошёл - и всё, что он получил, это возможность задавать вопросы серверу. На каждый вопрос сервер отвечает отдельно.
Как устроены роли и права доступа в личном кабинете на практике
Начинается всегда с ролей. Их немного, они понятны бизнесу, они хорошо ложатся в интерфейс настроек: клиент, менеджер, бухгалтер, администратор.
Дальше происходит одно и то же. Приходит требование: бухгалтеру нужно видеть акты, но не нужно видеть себестоимость. Появляется роль «бухгалтер без себестоимости». Потом менеджеру филиала надо видеть только свои заказы - появляется «менеджер филиала». Через год ролей четырнадцать, семь из них отличаются одним флагом, и никто не помнит, чем именно.
Переход выглядит так: роль перестаёт быть носителем прав и становится их набором. Право описывается парой «объект - действие»: orders:read, orders:cancel, invoices:export, users:invite. Роль - это именованный список таких прав. Проверка в коде идёт не по роли, а по праву: не «если админ», а «если есть право orders:cancel».
Выигрыш практический. Новое требование закрывается добавлением права в роль через настройки, а не правкой кода в двадцати местах. И на вопрос «что может бухгалтер» есть один ответ вместо чтения исходников.
На платформе событий, которую я делал целиком, ролей было четыре: фотограф, контролёр билетов, SMM и администратор. Разграничение шло на двух уровнях сразу - какие страницы открываются и какие данные отдаются. Фотограф загружает снимки в альбом своего события и не видит списка покупателей. Контролёр сканирует QR на входе и не видит фотоальбомы и рассылки. Четырёх ролей хватило, потому что права описали до того, как начали писать интерфейс, а не после.
Схема связей
Где на самом деле проверяются права
Слева направо: всё, что до третьей колонки, подконтрольно пользователю и защитой не является.
Пользователь
- Браузер или приложение Код, который пользователь может прочитать и изменить
- Токен доступа JWT, живёт 15 минут
- Refresh-токен Хранится на сервере, может быть отозван
- Запрос руками curl, DevTools, Postman - в обход интерфейса
Проверка на клиенте
- Гард маршрута Не пускает на страницу чужой роли
- Скрытие элементов Кнопка отмены не рисуется без права
- Валидация формы Обходится за десять секунд
- Ценность: удобство Защиты не даёт вообще
Проверка на сервере
- Кто пришёл Подпись токена, срок жизни, отзыв
- Право на действие orders:cancel, invoices:export
- Право на объект Этот документ принадлежит этому аккаунту?
- Лимиты и запись в журнал Кто, что, когда, с какого адреса
Данные
- Заказы и статусы Выборка всегда ограничена владельцем
- Документы Ссылки одноразовые, с истечением срока
- Персональные данные Только то, без чего процесс не работает
- Журнал действий Пишется всегда, чистится по расписанию
Самая частая дыра: право на объект
Проверять «что можно делать» научились почти все. Проверять «с какими данными» забывают регулярно, и это дыра номер один в кабинетах, которые я разбирал.
Выглядит она так. Пользователь открывает свой счёт по адресу /invoices/8412. Право invoices:read у него есть, сервер это проверил и документ отдал. Пользователь меняет цифру в адресной строке на 8413 и получает счёт другой компании: с суммой, реквизитами и составом заказа. Прав хватило, потому что сервер спросил «можно ли ему читать счета» и не спросил «его ли это счёт».
Чинится это одним правилом: выборка из базы всегда ограничена владельцем, а не фильтруется после. Не «найти документ 8413, потом проверить владельца», а «найти документ 8413 среди документов этого аккаунта». Разница в одну строку запроса, но первый вариант рано или поздно забудут проверить, а второй не может вернуть чужое физически.
Отдельно про ссылки на файлы. Прямая ссылка на PDF в хранилище живёт вечно и пересылается в мессенджере вместе со всем содержимым. Правильно - выдавать подписанную ссылку с истечением через несколько минут, которую генерирует сервер после проверки прав.
Токены: короткий доступ и отзываемое обновление
Схема, которая работает и не раздражает пользователей.
Токен доступа - JWT с временем жизни 15 минут. Он подписан, сервер проверяет подпись без обращения к базе, поэтому проверка дешёвая. Внутри лежит идентификатор пользователя и набор прав. Отозвать его нельзя, и это нормально: даже если токен утёк, он протухнет через четверть часа.
Refresh-токен - длинный, живёт неделями, хранится в базе в виде хеша и обменивается на новый токен доступа. Вот его отозвать можно и нужно. Удалили запись - и клиент больше не получит новый доступ.
Что это даёт на практике:
- Выход со всех устройств. Кнопка удаляет все refresh-токены пользователя. Через 15 минут все сессии мертвы. Без такой кнопки увольнение сотрудника превращается в смену пароля и надежду.
- Немедленное отключение при смене роли. Понизили права - отозвали refresh, следующий обмен выдаст токен с новым набором.
- Ротация. При каждом обмене старый refresh-токен становится недействительным. Если он приходит второй раз, значит его кто-то скопировал, и вся цепочка сессий обрывается.
На платформе событий это было устроено именно так: JWT с отзывом refresh-токена. Контролёр билетов, у которого забрали доступ, переставал сканировать не «когда-нибудь», а в течение четверти часа.
Двухфакторка: где она обязательна
Не везде. Клиенту, который смотрит статус заказа, второй фактор только мешает и снижает долю тех, кто вообще заходит.
Обязательна она для учётных записей с правом менять деньги, права и данные других людей: администраторы, бухгалтеры с доступом к платежам, все, кто может выгружать базу клиентов. Второй фактор на этих ролях стоит одного дня работы и закрывает целый класс проблем с подобранными и утёкшими паролями.
Разумный компромисс для остальных - второй фактор не на каждый вход, а на чувствительное действие: смену реквизитов, вывод средств, добавление нового сотрудника.
Журнал действий
Нужен даже сервису на сто пользователей, и стоит он один день работы.
Пишется минимум: кто, что сделал, с каким объектом, когда, с какого адреса и устройства. Отдельно - неудачные попытки: отказы по правам, ошибки входа, обращения к чужим объектам.
Зачем это нужно на практике. Клиент утверждает, что не менял реквизиты, а платёж ушёл не туда - в журнале видно, из-под какой учётной записи и когда правку внесли. Сотрудник выгрузил базу перед увольнением - видно, что и когда. Кто-то перебирает идентификаторы документов в адресной строке - серия отказов по правам от одного пользователя видна сразу, и это повод посмотреть внимательнее.
Без журнала любой такой разговор упирается в «этого не может быть».
Не собирайте того, что не придётся защищать
Каждое поле с персональными данными - это обязательство. Его надо хранить, ограничивать доступ, уметь удалить по запросу и объяснить, зачем оно вам.
Практический фильтр: если поле не используется ни в одном рабочем процессе, его не должно быть в форме. Паспортные данные нужны, только если вы оформляете договор. Дата рождения нужна, только если есть возрастное ограничение или поздравления, которые реально отправляются. Адрес нужен для доставки, а не «на всякий случай».
Отдельно про то, что не хранят никогда: полные номера карт и коды с обратной стороны. Этим занимается платёжный агрегатор, у которого есть сертификация, а у вас на руках остаётся только токен карты.
Что входит в проект кабинета, как выглядят этапы и почему матрица прав рисуется до первого экрана, я расписал на странице про личный кабинет. Разбор прав в существующем сервисе занимает один-два дня и обычно находит как минимум одну проверку, которую забыли перенести на сервер.


