Проверки прав доступа на фронтенде, которые совпадают с правилами сервера
Узнайте, как проверки прав доступа на фронтенде помогают интерфейсу, пока сервер остается главным, с простыми схемами, примерами и коротким списком для проверки.

Почему это так часто ломается
Баги с правами обычно начинаются с одного неверного предположения: если UI скрывает действие, значит приложение в безопасности. Это не так. Скрытая кнопка меняет только то, что видит человек. Она не мешает вызвать тот же API из другого экрана, из сохраненного запроса или через консоль браузера.
Одно и то же действие часто встречается в нескольких местах. Возврат денег может быть на странице заказа, в боковой панели с деталями платежа, в меню массовых действий и на панели поддержки клиентов. Управление пользователями может расползтись еще сильнее: кнопка "Изменить роль" на странице команды, поток приглашения, быстрое действие в результатах поиска и экран настроек профиля. Если каждый экран проверяет права по-своему, расхождения появляются очень быстро.
Эти расхождения становятся хуже, когда фронтенд-проверки прав растут из маленьких упрощений в скопированную логику. Одна страница проверяет admin. Другая — canRefund. Третья смотрит, состоит ли пользователь в команде поддержки и моложе ли заказ 30 дней. Все три варианта могут выглядеть разумно, но только один может совпадать с правилами сервера. Браузер должен помогать с UX, а не придумывать политику.
Еще один частый сбой дают смены ролей. Сотрудник поддержки открывает приложение утром. В полдень админ убирает у него доступ к возвратам. Если страница не обновит данные о правах, кнопка возврата может оставаться видимой часами. Нажатие все равно не пройдет, если сервер делает свою работу, но пользователь увидит сломанный сценарий и начнет меньше доверять продукту. Обратный баг не лучше: доступ уже выдали, а UI все еще выглядит заблокированным.
Простой пример с возвратом денег показывает проблему очень наглядно. Правило на сервере может звучать так: финансовые менеджеры могут делать возврат по любому оплаченному заказу, а руководители поддержки — только до $100 в течение 14 дней. UI часто превращает это в неаккуратное "показывать возврат, если роль — manager или support." На первый взгляд похоже на правду, пока сотрудник поддержки не увидит действие возврата на заказе возрастом 6 месяцев и на $900. Он нажмет кнопку, сервер заблокирует запрос, и продукт начнет казаться непоследовательным.
Поэтому так важно тонкое зеркало прав доступа в UI. Браузеру нужно достаточно информации, чтобы показывать или скрывать действия, отключать формы и объяснять, почему что-то заблокировано. Он должен отражать сервер, а не спорить с ним.
Что браузер должен делать, а что нет
Браузер должен помогать человеку двигаться по экрану без путаницы. Он может скрыть кнопку, сделать пункт меню недоступным или пропустить действие, которое пользователь все равно не может выполнить. Так продукт становится понятнее и предсказуемее.
Фронтенд-проверки прав нужны для подсказки, а не для власти. Они формируют интерфейс так, чтобы человек не продолжал нажимать на то, что все равно завершится ошибкой.
Последнее решение всегда остается за сервером. Если кто-то откроет инструменты разработчика, изменит запрос или вызовет API вручную, браузер не сможет его остановить. Сервер должен проверить то же действие еще раз и вернуть четкое да или нет.
Рабочее правило простое: браузер решает, что показывать, а сервер решает, что разрешено. Как только команды размывают эту границу, ошибки появляются очень быстро.
Возьмем маленькую админ-панель. Сотрудник поддержки может смотреть записи клиентов, но не может их удалять. Браузер должен скрыть кнопку Delete или показать ее как недоступную. Если пользователь все же отправит запрос на удаление другим способом, сервер обязан его отклонить.
Когда такое отклонение происходит, сообщение должно быть простым. "У вас нет прав на удаление этой записи" — этого достаточно. Большинству пользователей не нужен код ошибки, название политики или внутренняя причина.
В браузере также не должно быть секретных правил. Не стоит отправлять приватную логику, которая раскрывает проверки на мошенничество, внутренние пороги согласования или скрытые состояния счета. Все, что попало в код браузера, можно прочитать, скопировать и протестировать.
Это не значит, что зеркалу на клиенте нельзя быть вообще. Оно должно содержать только небольшой объем данных о правах, нужный экрану: может ли пользователь редактировать, архивировать или приглашать других. Держите его узким. Если правило зависит от чувствительных данных или сложной бизнес-логики, пусть его считает сервер, а наружу выдает только результат.
Такое разделение делает UI быстрее и одновременно не врет про авторизацию. Пользователь получает более плавный экран, а система остается защищенной, даже если кто-то пытается обойти браузер.
Держите один источник истины на сервере
Сервер должен владеть каждым правилом доступа. Если пользователь может редактировать счет, одобрять возврат или приглашать коллегу, сервер определяет это один раз и применяет везде. Браузер может отражать это решение ради удобства, но никогда не должен придумывать собственную версию правил.
Команды попадают в неприятности, когда разбрасывают логику по разным местам. Один backend-маршрут проверяет одно имя роли, компонент React — другое, и через шесть недель никто уже не понимает, какое правило правильное. Исправление скучное, и именно поэтому оно работает: пишите правила доступа в одном серверном слое и считайте этот слой источником и для поведения API, и для подсказок в UI.
Используйте понятные имена действий и держите их одинаковыми по всему стеку. Если API говорит о project.edit, UI должен использовать project.edit тоже. Не называйте это canUpdateProject в одном месте и edit_project в другом. Совпадающие имена экономят время, уменьшают догадки и делают фронтенд-проверки прав гораздо надежнее.
Хороший паттерн — возвращать разрешенные действия вместе с теми данными, которые странице все равно нужны. Если страница загружает проект, ответ может включать поля проекта и короткий список действий, которые доступны текущему пользователю. Тогда UI сам решит, показывать ли кнопку Edit, отключать ли опасное действие или скрывать ли админские элементы без собственной выдуманной политики.
Обычно в ответе достаточно:
- данных ресурса
- текущей роли пользователя, если ее нужно показать на странице
- списка или карты разрешенных действий
- причины отказа, когда UI должен объяснить, почему что-то заблокировано
На сервере также полезно вести историю отказов. Когда пользователи продолжают пытаться сделать то, чего им нельзя, это многое говорит вашей команде. Возможно, UI показал не ту кнопку. Возможно, для роли не хватает одного действия, которое люди ожидают. Возможно, названия разъехались, и зеркало потеряло синхронизацию.
Если логировать отказы с названием действия, типом ресурса и ролью пользователя, закономерности проявятся быстро. Так правила легче исправить до того, как они превратятся в обращения в поддержку.
Показывайте в зеркале только то, что нужно UI
Хорошее зеркало прав доступа для UI — маленькое и скучное. Браузеру не нужна вся модель политик. Ему достаточно информации, чтобы понять, показывать ли кнопку "Edit", "Refund" или "Invite", и какое сообщение вывести, когда действие заблокировано.
Используйте фронтенд-проверки прав для реальных действий, а не для угадывания намерения по названию роли. Флаги, привязанные к действиям, проще читать и безопаснее подключать к кнопкам, меню и пустым состояниям. canEditProject точно говорит UI, что делать. role = admin оставляет слишком много места для неверных выводов.
Связывайте каждую проверку с тем, что человек действительно может сделать:
- изменить запись
- вернуть платеж
- пригласить коллегу
- одобрить запрос
- экспортировать данные
Иногда доступ зависит не только от человека, но и от самой записи. Сотрудник поддержки может вернуть платеж только если платеж в нужном состоянии. Менеджер может редактировать заказ до отправки, но не после. В таких случаях передавайте состояние записи, которое экран и так должен показать, или передавайте финальный флаг, который сервер уже вычислил из этого состояния.
Простой payload часто лучше, чем умный. Если странице платежа нужно знать, показывать ли кнопку возврата, ответ может содержать поля вроде status: captured и canRefund: true. Этого достаточно для UI. И это легко проверить в инструментах разработчика, если что-то выглядит не так.
Не отправляйте всю древовидную структуру правил, все унаследованные роли или список исключений из политики. Это делает ответ шумным и переносит логику политики в браузер. Как только так происходит, правила клиента и сервера начинают расходиться.
Делайте payload достаточно маленьким, чтобы разработчик мог прочитать его за несколько секунд. Если человек открывает ответ сети и не может понять, почему кнопка скрыта, зеркало слишком сложное. Маленькие объекты прав проще тестировать, проще отлаживать и сложнее солгать пользователю.
Хорошее правило простое: отправляйте только то, что нужно текущему экрану, чтобы показать следующее очевидное действие.
Стройте UI-проверки шаг за шагом
Начните с загрузки зеркала прав вместе с первыми данными страницы. Если страница показывает запись клиента, тот же ответ может содержать небольшой блок прав вроде canEdit, canDelete и canInvite. Это делает фронтенд-проверки прав быстрыми и убирает неприятную вспышку, когда страница на долю секунды показывает все действия, а потом прячет половину из них.
Держите задачу браузера маленькой. Сервер решает, кто что может. Браузер читает эти ответы и подстраивает страницу под них.
Небольшие вспомогательные проверки помогают поддерживать код в порядке. Заворачивайте кнопки, вкладки и пункты меню в маленькие проверки вместо того, чтобы разбрасывать if-ы по всей странице. Такой helper должен быть скучным и прямым: прочитать флаг права, а затем показать, отключить или удержать элемент, пока не придут данные.
Рабочий паттерн обычно такой:
- Загружайте права вместе с данными записи.
- Показывайте состояние загрузки, пока права неизвестны.
- Скрывайте элементы, которые текущему пользователю не нужны.
- Отключайте элементы, когда действие видно, но использовать его пока нельзя.
- Повторно проверяйте права после любого события, которое может их изменить.
Скрыть и отключить — не одно и то же, поэтому выбирайте осознанно. Скрывайте вкладку "Admin settings", если пользователю туда вообще не нужно заходить. Отключайте кнопку "Publish", если в целом публикация ему доступна, но черновик еще нельзя опубликовать, пока не заполнены обязательные поля. Эта разница помогает людям понимать продукт, а не гадать.
Неизвестное состояние тоже должно иметь свое правило. Не считайте действие разрешенным, пока страница загружается. Если права еще не пришли, покажите skeleton, заглушку или неактивный элемент без обработки клика. Это предотвращает баги интерфейса и не приучает людей нажимать на кнопки, которые через секунду все равно сломаются.
Обновляйте зеркало после всего, что может изменить доступ. Сохранение может поменять состояние записи. Изменение роли может открыть или убрать действия. Перезагрузка записи может принести новые результаты политики с сервера. Например, если менеджер одобрил расход, кнопка "Edit" может исчезнуть сразу после сохранения, потому что запись теперь заблокирована.
Когда такие проверки находятся рядом с UI и обновляются по свежим данным сервера, страница ощущается последовательной. Люди перестают видеть действия, которыми не могут воспользоваться, и вы избегаете ложного впечатления, что браузер сам управляет авторизацией.
Следите, чтобы зеркало не отставало
Зеркало прав в UI начинает ломаться в тот момент, когда оно перестает двигаться вместе с реальным состоянием. Люди входят в систему, переключают аккаунты, получают доступ, теряют его или меняют статус записи. Если браузер слишком долго держит старые данные о правах, экран начинает врать.
Обновляйте данные о правах каждый раз, когда меняется идентичность пользователя. После входа загрузите свежий снимок до того, как покажете защищенные кнопки и формы. То же самое делайте, когда человек переключает рабочее пространство, команду или аккаунт. После выхода из системы очищайте все кэшированные проверки, привязанные к этому пользователю. Если на общем устройстве пропустить этот шаг, следующий человек может несколько секунд видеть не тот интерфейс, и этого достаточно, чтобы запутаться.
Статус записи может менять права не меньше, чем роль пользователя. Человек может редактировать заказ, пока он в состоянии draft, и потерять это право, когда он станет approved. Когда статус меняется, сразу обновляйте локальное зеркало или запрашивайте новое состояние прав с сервера. Полная перезагрузка страницы — слишком поздно.
Несколько правил синхронизации закрывают большинство случаев:
- повторно запрашивайте данные после входа и смены аккаунта
- пересчитывайте или запрашивайте заново, когда меняется статус записи
- очищайте кэшированные проверки при выходе
- удаляйте старые данные о правах, когда сервер возвращает новую версию или метку времени
Проверка в двух вкладках ловит баги, которые обычное тестирование по счастливому сценарию пропускает. Откройте одну и ту же запись в двух вкладках. Вкладка A меняет статус записи или переключает аккаунт. Затем вернитесь на вкладку B и попробуйте старое действие. Хорошие фронтенд-проверки прав обновят экран или безопасно откажут и попросят свежие данные. Плохие будут по-прежнему показывать действия, которые уже не совпадают с серверной авторизацией.
Простой пример: вкладка A переводит обращение в поддержку в состояние closed. Вкладка B все еще показывает "Reply", потому что у нее в памяти лежит старое зеркало. Исправление — не в более сложном наборе клиентских правил. Исправление — в синхронизации. Делайте повторный запрос после изменения, слушайте события хранилища между вкладками или broadcast-сообщение и держите кэш короткоживущим.
Если зеркало в браузере тонкое и не вечно живущее, устаревшее состояние становится небольшой UX-ошибкой, а не риском для безопасности.
Простой пример
Представьте экран счета с двумя ролями: staff и managers. Staff могут читать детали счета, скачивать копию и смотреть историю платежей. Возвраты им недоступны.
Managers могут больше, но только в узком случае. Они могут делать возврат по счету только пока этот счет еще открыт.
Так получается четкое разделение между тем, что показывает браузер, и тем, что решает сервер. Браузер может использовать небольшое зеркало прав в UI примерно так: пользователи staff никогда не видят кнопку Refund, а managers видят ее только на открытых счетах.
Это делает фронтенд-проверки прав полезными, но не превращает их в фальшивую безопасность. Страница выглядит правильно для каждого человека, но правило по-прежнему принадлежит серверу.
Обычный сценарий выглядит так:
- Сотрудник staff открывает счет #1842 и видит детали счета.
- Страница скрывает кнопку Refund, потому что зеркало UI говорит
canRefund = false. - Менеджер открывает тот же счет и видит кнопку Refund только если статус
open. - Когда менеджер нажимает Refund, сервер еще раз проверяет роль и текущий статус счета.
Последняя проверка важнее всего. Любой может изменить код браузера, повторить запрос или вызвать API из другого клиента. Если пользователь staff отправит запрос на возврат вручную, сервер должен его отклонить. Если менеджер попытается сделать возврат по закрытому счету, сервер должен отклонить и это.
Изменения в середине сценария случаются часто. Менеджер может открыть счет в 10:00, увидеть, что он открыт, и нажать Refund в 10:03. За эти три минуты другая система или коллега могли поменять статус счета.
Когда это происходит, не делайте вид, что возврат удался, и не оставляйте человека с туманной ошибкой. Покажите простое сообщение: "Этот счет больше не открыт, поэтому возврат сделать нельзя." Затем обновите данные счета и состояние кнопки.
Вот и весь паттерн в миниатюре: скрывайте действия, которые человеку недоступны, показывайте действия, которые, скорее всего, доступны, и каждый раз позволяйте серверу принять окончательное решение.
Ошибки, из-за которых появляются расхождения
Расхождение начинается тогда, когда UI растет быстрее, чем модель правил. Команда добавляет одну проверку в кнопку, другую в меню и третью в поле формы. Вскоре у каждого компонента появляется своя версия истины. Два экрана, которые должны вести себя одинаково, начинают спорить, и пользователи это замечают.
Самый частый баг — копирование и вставка правил. Один компонент проверяет canEditInvoice, другой — user.role === "manager", а модальное окно добавляет еще одно исключение для owner. Каждая строчка казалась безобидной в момент написания. Вместе они создают фронтенд-проверки прав, которые уже никто не может уверенно объяснить.
Роли также подталкивают команды делать модель слишком простой. Реальный доступ часто зависит от самой записи: статуса, владельца, региона, состояния платежа или того, одобрял ли кто-то это уже. Сотрудник поддержки может редактировать черновик обращения, но не заблокированное обращение. Финансовый руководитель может вернуть заказ до списания, но не после. Если UI спрашивает только, является ли человек администратором, он очень быстро начнет расходиться с серверной авторизацией.
Особые админские случаи только ухудшают ситуацию. Команды часто жестко прописывают в браузере, что "admins can always do this", потому что это чинит демо или разблокирует одного внутреннего пользователя. Позже сервер остается со строгой логикой, или появляется второй тип администратора, и зеркало прав в UI начинает врать.
Кэширование дает более тихие ошибки. Если данные о правах живут дольше сессии, пользователь может продолжать видеть действия редактирования после выхода и входа, смены роли или обновления записи. Держите зеркало короткоживущим. Обновляйте его при смене личности, при изменении записи и тогда, когда сервер сообщает, что доступ изменился.
Последняя ошибка — путать представление с защитой. Неактивная кнопка меняет только страницу. Она не останавливает прямой запрос, повторно отправленный API-вызов или скрипт в консоли браузера. Скрывайте или отключайте действия, чтобы уменьшить путаницу, но окончательное решение всегда оставляйте серверу.
Быстрый тест на запах помогает заметить проблему:
- два компонента проверяют разные поля для одного и того же действия
- проверка роли игнорирует статус записи
- в UI есть обход для админа, о котором API ничего не знает
Тонкие UI-проверки легче доверять. Они читают небольшой payload с правами с сервера и используют его одинаково везде.
Быстрая проверка перед релизом
Непосредственно перед релизом протестируйте приложение как скептичный пользователь. Баги с правами обычно прячутся в мелочах: кнопка все еще видна, старая вкладка все еще работает или сообщение об ошибке почти ничего не объясняет.
Начните с простого списка. Каждый элемент управления, который может менять данные, показывать приватную информацию или влиять на биллинг, должен иметь соответствующее серверное правило. Если вы не можете указать, какое правило стоит за защищенной кнопкой, считайте это багом.
- Пройдитесь по каждому экрану и сопоставьте каждую защищенную кнопку, пункт меню и массовое действие с серверным правилом.
- Измените тестовому пользователю роль и проверьте, что экран обновляется так, как обещает продукт.
- Оставьте старую вкладку открытой, поменяйте роль пользователя в другой сессии, а потом попробуйте то же действие из устаревшей вкладки.
- Прочитайте вслух каждое сообщение об отказе и исправьте все, что звучит расплывчато, холодно или непонятно.
- Запишите, каким действиям нужен и UI-check, и жесткий отказ со стороны сервера.
Последний пункт важнее, чем многие думают. Скрыть кнопку — это только удобство. Сервер все равно должен отклонить запрос, если человек использует старую вкладку, скопированный запрос или состояние клиента, которое не обновилось.
Короткий список действий помогает вскрыть слабые места. Команды часто забывают одно из них:
- удалить запись
- экспортировать приватные данные
- пригласить или удалить пользователей
- изменить биллинг или настройки тарифа
- посмотреть админские логи или внутренние заметки
Сообщения об отказе тоже стоит быстро отредактировать. "Доступ запрещен" технически верно, но человек после этого застревает. "У вас больше нет прав на экспорт этого отчета. Обновите страницу или попросите администратора открыть доступ" — намного понятнее.
Если фронтенд-проверки прав и серверная авторизация где-то расходятся, пользователи это быстро найдут. Если поймать это до релиза, вы сэкономите время поддержки и избежите худшего вида бага: экран говорит "нет", а API все еще отвечает "да".
Что делать дальше
Выберите один рискованный поток и сначала приведите в порядок его. Биллинг, настройки команды, удаление пользователей и смена ролей — хорошие точки старта, потому что ошибки там быстро путают людей и могут создать реальные проблемы с доступом.
Небольшой первый шаг лучше полного переписывания. Если пытаться исправить все экраны сразу, команда обычно получает больше расхождений, а не меньше. Одного потока достаточно, чтобы доказать паттерн.
Сначала задайте понятные названия действий, еще до кода. Пишите их так, как сказал бы человек:
- просматривать счета
- менять тариф
- приглашать участника команды
- удалять участника команды
- редактировать настройки компании
Потом проверьте каждое действие в двух местах. Сервер решает, разрешено ли действие. UI только отражает этот ответ, чтобы страница могла скрыть, показать, отключить или объяснить действие так, как это имеет смысл.
Это и есть практический следующий шаг для фронтенд-проверок прав. Держите браузер тонким. Если серверное правило изменится, зеркало в UI должно измениться вместе с ним, а не придумывать свою логику.
Сделайте проверку расхождений частью каждого релиза. Для этого не нужен большой процесс. Во время QA или релизного ревью задайте несколько прямых вопросов: совпадает ли состояние кнопки с результатом API, все еще ли понятно сообщение об ошибке и не добавил ли кто-то новую роль или исключение без обновления зеркала?
Простое правило помогает очень хорошо: каждое новое защищенное действие должно выходить сразу с тремя вещами — серверным правилом, зеркалом в UI и одним тестом, который доказывает, что они совпадают. Такая привычка ловит очень много грязных крайних случаев.
Если у вашей команды уже накопились годы смешанных правил, внешний аудит может сэкономить время. Олег на oleg.is проводит ревью продуктовых потоков как fractional CTO и помогает командам выровнять UI-проверки с серверной авторизацией, особенно когда правила расползлись по нескольким экранам, ролям и старым API. Короткого разбора одного рискованного потока обычно хватает, чтобы увидеть, где начинается расхождение.