Auth модели и фронтенд: RBAC, ABAC, PBAC и refresh-flow
Аутентификация отвечает на вопрос “кто ты”, авторизация на вопрос “что тебе можно”. Этот урок про второй вопрос (модели доступа) и про самую коварную часть первого: где на фронтенде хранить токены и как не отдать их атакующему. Опираемся на готовый AuthService из раздела про микросервисы, не изобретаем auth заново.
Идея: разделяй кто и что можно
Две вещи постоянно путают, а они независимы:
- Аутентификация устанавливает личность. Это вход: пароль, токен, 2FA.
- Авторизация решает, что этой личности можно. Это и есть про модели доступа.
Главное правило, которое нарушают чаще всего: авторизация всегда на
сервере. Фронтенд прячет кнопку “удалить” не ради безопасности, а ради
удобства. Кнопки нет, а эндпоинт есть, и curl до него дойдёт. Если право
проверяется только на клиенте, его нет вообще. Третью A, accounting,
разберём в следующем уроке, здесь сосредоточимся на authz.
RBAC, ABAC, PBAC
Модели доступа это разные способы записать правило “субъект, действие, ресурс, условие”. Они идут по нарастающей гибкости.
-
RBAC: доступ через роли. У пользователя роль
editor, у роли набор разрешений. Просто, читаемо, покрывает большинство приложений. Ломается, когда правило зависит от конкретного ресурса: “редактор может править только свои статьи” уже не выражается одной ролью без размножения ролей.// RBAC: проверка чистой роли if (!user.roles.includes('editor')) return forbidden(); -
ABAC: доступ по атрибутам субъекта, ресурса и контекста. То самое “свои статьи” выражается естественно: сравни
user.idсresource.ownerId. Гибко, но правила легко расползаются по кодовой базе.// ABAC: правило зависит от атрибутов ресурса if (user.id !== article.ownerId && !user.roles.includes('admin')) { return forbidden(); } -
PBAC: правила вынесены в декларативные политики и отдельный policy engine (OPA/Rego, Cedar). Код только спрашивает движок “можно?”, а сами правила версионируются и тестируются отдельно. Нужно, когда правил много, они меняются часто и их пишут не только разработчики.
На практике почти всегда гибрид: RBAC как грубый каркас (роль определяет доступ к разделу), а поверх ABAC-проверки на владение ресурсом. PBAC подключают, когда этой связки перестаёт хватать. Не начинай с PBAC: это оправдано сложностью, которой у маленького проекта ещё нет.
Frontend auth: где хранить токены
Теперь коварная часть. AuthService выдал токены, вопрос куда их положить в браузере. Два кандидата, и оба с проблемами:
localStorage: доступен из JS, поэтому любой XSS читает токен одной строкой. Удобно, но небезопасно для долгоживущих секретов.- HttpOnly cookie: недоступна из JS, поэтому XSS её не украдёт. Но она едет автоматически, значит появляется вектор CSRF (см. первый урок).
Идеального варианта нет, поэтому архитектуру выстраивают так, чтобы минимизировать ущерб от любого исхода. Рабочая схема это короткий access плюс долгий refresh:
- Access token живёт минуты и прикладывается к запросам. Утёк, скоро протухнет.
- Refresh token живёт долго, лежит в HttpOnly cookie и используется только чтобы получить новый access.
Refresh-token flow работает так: запрос упал с 401, клиент молча идёт за новым access по refresh-cookie, повторяет исходный запрос. Перелогина не видно.
// тихое обновление: один общий промис, чтобы не рефрешить параллельно
let refreshing: Promise<void> | null = null;
async function withAuth(request: () => Promise<Response>) {
let res = await request();
if (res.status === 401) {
refreshing ??= refreshAccessToken().finally(() => (refreshing = null));
await refreshing;
res = await request(); // повтор с новым access
}
return res;
}
Самый чистый вариант это вообще не отдавать токены в браузер. Их держит BFF: браузер получает обычную сессионную cookie, а BFF подставляет настоящие токены к запросам на бэкенд уже на сервере. Тогда даже XSS не доберётся до токенов, потому что их в браузере просто нет. Это рекомендуемый дефолт для SPA, и про browser storage в целом был урок AJAX, storage и формы.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.