Паттерны аутентификации и авторизации
Паттерны аутентификации и авторизации простыми словами: чем «кто ты» отличается от «что тебе можно», JWT и сессии, OAuth2 с PKCE, RBAC и ABAC, вызовы между сервисами.
Каждое приложение рано или поздно задаётся двумя вопросами: кто этот пользователь и что ему разрешено делать. Это разные задачи с разными инструментами, и их легко перепутать. Разберём с нуля.
Разницу проще увидеть на одном запросе: «кто ты» проверяют один раз на границе, а «что тебе можно» — дальше по пути, и остановить запрос может только тот уровень, который видит сам объект.
Подпись токена и роль пускают запрос к эндпоинту, но не к конкретному объекту: заказ №77 остановил только доменный сервис, где видно userId=1005. Тот же токен на своём заказе №19 проходит все три уровня.
Аутентификация и авторизация — в чём разница
Аутентификация отвечает на вопрос «кто ты?». Пользователь доказывает свою личность: вводит логин и пароль, предъявляет токен, проходит Face ID. Результат — система знает, что перед ней конкретный пользователь, например Иван с userId=42.
Авторизация отвечает на вопрос «что тебе можно?». Уже зная, кто пользователь, система решает: может ли Иван удалить чужую статью, открыть страницу администратора, посмотреть чужой заказ.
На практике они идут одна за другой: сначала аутентификация, потом авторизация. Вернуть 403 Forbidden без проверки личности — ошибка.
Токены: JWT, Opaque и Session ID
Когда пользователь вошёл, сервер должен как-то «помнить» его в следующих запросах. Есть три подхода:
Session ID — сервер создаёт сессию, хранит её (обычно в Redis), а клиенту отдаёт только короткий идентификатор в cookie. При каждом запросе сервер загружает сессию из хранилища.
JWT (JSON Web Token) — токен, который содержит данные прямо внутри себя. Структура: header.payload.signature. В payload лежат userId, роли, срок действия. Подпись позволяет серверу проверить токен локально, без обращения к базе. Размер — от 300–400 байт у самого простого токена до 1–2 КБ у токена Keycloak, в котором лежит список realm- и client-ролей. За размером стоит следить: токен едет в заголовке каждого запроса, а прокси и серверы обычно не принимают заголовки больше 4–8 КБ. Разрастётся список ролей — и запросы начнут отваливаться с жалобой на слишком большой заголовок (431, а у части прокси просто 400), не дойдя до приложения.
Все данные о пользователе токен несёт с собой, поэтому сервер проверяет его подписью на месте, без похода в базу или к серверу авторизации. Плата — отозвать выданный токен нельзя: он остаётся действительным, пока не выйдет срок из exp.
Opaque Token — просто строка-идентификатор вроде a3f8b2c1-4d5e-.... Данных внутри нет. Для проверки нужно обратиться к серверу авторизации (этот запрос называется introspection).
| Тип | Проверка | Инвалидация | Размер |
|---|---|---|---|
| Session ID | Запрос к Redis | Мгновенная (удалить из Redis) | Маленький |
| JWT | Локально (подпись) | Нельзя до истечения | Средний |
| Opaque | Запрос к auth-серверу | Мгновенная | Маленький |
OAuth 2.0 и OpenID Connect
Раньше приложения просили у пользователя его логин и пароль, а потом от его имени ходили в другие сервисы. Это небезопасно: приложение видит пароль и может делать что угодно.
OAuth 2.0 решает эту проблему: пользователь входит напрямую в доверенный сервис (например, Keycloak или Google), а приложение получает только ограниченный токен доступа — без пароля.
OpenID Connect (OIDC) — надстройка над OAuth 2.0. Добавляет к токену доступа ещё id_token — JWT с информацией о том, кто вошёл: идентификатор, имя, почта. На роли из id_token не опираются: он отвечает на вопрос «кто это», а права лежат в токене доступа. Положить туда роли провайдер умеет — тот же Keycloak делает это настройкой, — но проверять по ним доступ не стоит: id_token адресован приложению, которое показывает пользователя на экране, а не API, которое решает, что можно. Именно OIDC превращает OAuth из протокола «что приложению разрешено» в протокол «кто этот пользователь».
| Токен | Назначение |
|---|---|
access_token | Что приложению разрешено делать |
refresh_token | Получить новый access_token без повторного входа |
id_token | Кто пользователь (только OIDC) |
Аутентификация для браузерного приложения (SPA)
Браузер — небезопасная среда. JavaScript-код виден всем через DevTools. Хранилища localStorage и sessionStorage доступны любому скрипту на странице. Один вредоносный скрипт — и все токены скомпрометированы.
Попасть на страницу такой скрипт может двумя разными путями, и путать их не стоит. XSS — когда чужой код подсовывают через непроверенный ввод: комментарий, имя в профиле, параметр в адресе. Атака на цепочку поставки — когда вредоносный код уже лежит внутри сторонней библиотеки, которую вы подключили сами. Пути разные, а результат один: чужой скрипт выполняется на вашей странице с вашими правами.
Браузер умеет кое-что важное: он поддерживает cookie с флагом HttpOnly. Такой cookie JavaScript вообще не видит — только браузер. Именно это свойство используют все безопасные подходы для SPA.
Сессия в cookie + Redis (классика)
Самый простой вариант. Пользователь вводит логин и пароль, сервер проверяет их, создаёт сессию в Redis и возвращает её идентификатор в HttpOnly cookie.
Браузер хранит такую cookie сам и не показывает её скриптам на странице — вытащить её чужим JavaScript не выйдет. Плата в том, что браузер подставляет её вообще в каждый запрос к домену, поэтому и нужны Secure и SameSite: иначе чужая страница сможет дёргать наш сервер от имени пользователя.
Плюсы: сессию легко удалить — пользователя выкидывает из системы мгновенно; простая реализация.
Минусы: сессии живут в Redis, и он становится обязательной частью каждого запроса — пока Redis недоступен, ни один пользователь не может ничего сделать, а если хранилище потеряло данные, все разом разлогинены. И cookie — браузерный механизм, мобильному приложению он не подходит.
OAuth2 Authorization Code Flow + PKCE (рекомендуется для SPA)
Современный стандарт. Пользователь входит через внешний Identity Provider (Keycloak, Okta, Entra ID) — приложение никогда не видит его пароль. Токены хранятся на сервере, а в браузер идёт только сессионная cookie.
У этого потока есть слабое место: промежуточный код авторизации едет через браузер в редиректе, и его можно перехватить — подменённое приложение или расширение ловит редирект и само обменивает код на токены. Эту дыру закрывает PKCE (Proof Key for Code Exchange): приложение генерирует случайный code_verifier, отправляет его хеш при запросе, а сам code_verifier — при обмене кода на токен. Перехватчик без code_verifier ничего не получит.
Упрощённо поток выглядит так:
- SPA обращается к backend — нет cookie →
401. Перенаправить в ответ на фоновый запрос нельзя: браузер такой редирект отработает молча внутри того же запроса. Поэтому приложение само переводит браузер на/loginбэкенда. - Бэкенд перенаправляет браузер на страницу входа IdP.
- Пользователь вводит пароль на сайте IdP (не приложения).
- IdP возвращает code в callback.
- Backend обменивает code +
code_verifierна токены у IdP. - Токены сохраняются на сервере (Redis), браузер получает
HttpOnlycookie. - Дальнейшие запросы идут с cookie — backend достаёт токены из Redis.
Почему это хорошо: токены никогда не попадают в JavaScript — со страницы их просто нечем достать; пароль видит только IdP, и он же может добавить второй фактор и единый вход (SSO).
Важная оговорка: HttpOnly закрывает кражу токена, но не отменяет XSS. Внедрённый на страницу скрипт токен не прочитает — зато сможет слать запросы к вашему же бэкенду, и cookie к ним браузер подставит сам. Пользователь при этом ничего не заметит. То есть украсть доступ «с собой» не выйдет, а сделать что-то от имени жертвы, пока она на странице, — вполне.
Конфигурация для Spring Boot:
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-web-app
client-secret: ${KEYCLOAK_CLIENT_SECRET}
scope: openid,profile,email,offline_access
authorization-grant-type: authorization_code
provider:
keycloak:
issuer-uri: https://keycloak.example.com/realms/my-realm
JWT в HttpOnly cookie (без IdP)
Альтернатива, когда внешний IdP не нужен. Сервер выдаёт JWT и кладёт его в HttpOnly cookie. Проверка — локально по подписи, Redis не нужен.
Главный недостаток: JWT нельзя «отозвать» до истечения срока. Если доступ нужно отобрать мгновенно — придётся вести список отозванных токенов (чёрный список), что фактически возвращает нас к серверному хранилищу.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtCookieFilter extends OncePerRequestFilter {
private final JwtDecoder jwtDecoder;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
String token = extractFromCookie(req, "TOKEN");
if (token != null) {
try {
Jwt jwt = jwtDecoder.decode(token);
var roles = jwt.getClaimAsStringList("roles");
var authorities = (roles == null ? List.<String>of() : roles).stream()
.map(r -> new SimpleGrantedAuthority("ROLE_" + r))
.toList();
SecurityContextHolder.getContext()
.setAuthentication(new JwtAuthenticationToken(jwt, authorities));
} catch (JwtException e) {
// не молча: битый или просроченный токен стоит увидеть в логах
log.debug("Невалидный токен в cookie: {}", e.getMessage());
}
}
chain.doFilter(req, res);
}
private static String extractFromCookie(HttpServletRequest req, String name) {
Cookie[] cookies = req.getCookies();
if (cookies == null) {
return null;
}
return Arrays.stream(cookies)
.filter(c -> name.equals(c.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
}
Две мелочи, на которых этот фильтр обычно и ломается. Первая: getClaimAsStringList возвращает null, если такого поля в токене вообще нет, — поэтому список ролей проверяют до .stream(). Иначе вместо честного «токен не подошёл» приложение получит падение на пустом месте, и клиенту уйдёт 500 вместо 401: catch (JwtException) такую поломку не ловит. Вторая: поле roles здесь — наше собственное, мы сами его и кладём в токен. У Keycloak роли лежат глубже, в realm_access.roles и resource_access.<client>.roles, и достают их отдельным конвертером.
Одно важное дополнение к этому примеру. Как только токен переехал в cookie, браузер начинает подставлять его сам — в том числе к запросам, которые инициировала чужая страница. Значит, вместе с таким фильтром обязательно нужна защита от подделки межсайтовых запросов: атрибут SameSite у cookie и парный токен, который Spring Security умеет выдавать из коробки. Для варианта с заголовком Authorization это не требовалось, для cookie — обязательно.
JWT в localStorage — почему это плохо
В туториалах часто хранят токен в localStorage. Это удобно, но опасно: любой JavaScript на странице может его прочитать. Одна уязвимость в сторонней аналитике или рекламном SDK — и токены пользователей скомпрометированы.
Если используете JWT для браузера — только в HttpOnly cookie.
Что выбрать для SPA
| Подход | Когда использовать |
|---|---|
| OAuth2 + PKCE через BFF | Лучший выбор при наличии IdP (Keycloak, Okta) |
| Сессия (cookie + Redis) | Простой вариант без внешнего IdP |
| JWT в HttpOnly cookie | Stateless, но сложнее инвалидация |
| JWT в localStorage | Не использовать в продакшене |
Аутентификация для мобильного приложения
Мобильное приложение — другая среда. У него есть защищённое хранилище (iOS Keychain, Android EncryptedSharedPreferences), куда можно безопасно положить токены. Нет браузерного cookie — токены передаются в заголовке Authorization: Bearer <token>.
OAuth2 + PKCE для мобильных (рекомендуется)
Тот же Authorization Code Flow + PKCE, но с двумя отличиями:
- Нет
client_secret— мобильное приложение не может хранить секрет безопасно. Это нормально: его роль берёт на себя PKCE. - Вход через системный браузер (Chrome Custom Tabs на Android, ASWebAuthenticationSession на iOS), а не встроенный WebView. WebView позволял бы приложению перехватить введённые данные — системный браузер этого не даёт.
// Android, AppAuth SDK
val authRequest = AuthorizationRequest.Builder(
serviceConfig,
"mobile-app",
ResponseTypeValues.CODE,
Uri.parse("myapp://callback")
).setCodeVerifier(CodeVerifierUtil.generateRandomCodeVerifier())
.setScopes("openid", "profile", "email", "offline_access")
.build()
После успешного входа access_token и refresh_token сохраняются в Keychain / EncryptedSharedPreferences.
JWT Bearer Token
Приложение получает JWT от сервера авторизации и добавляет его к каждому запросу. Backend проверяет подпись локально по открытым ключам IdP (JWKS).
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://keycloak.example.com/realms/my-realm
Когда access_token истекает (обычно через 5–15 минут), приложение автоматически обновляет его с помощью refresh_token, не беспокоя пользователя повторным входом.
Opaque Token + Introspection
Приложение получает непрозрачный токен. Backend при каждом запросе спрашивает сервер авторизации: «этот токен действителен?» — и получает данные пользователя.
Плюс: мгновенная инвалидация — auth-сервер отзывает токен, и следующий запрос вернёт 401.
Минус: каждый запрос = дополнительный сетевой вызов к auth-серверу. Если auth-сервер недоступен — всё приложение перестаёт работать.
Под нагрузкой с этим минусом живут одним способом — кэшируют ответ. Сервис спрашивает про токен один раз и держит ответ в памяти или в Redis короткое время, скажем минуту, а дальше эту минуту отвечает сам. Число вызовов к серверу авторизации падает в разы, и его короткая недоступность перестаёт мгновенно останавливать приложение.
Цена у кэша ровно одна, и назвать её нужно прямо: отзыв перестаёт быть мгновенным. Токен, отозванный секунду назад, будет приниматься столько, сколько живёт кэш, — то есть непрозрачный токен с минутным кэшем ведёт себя ровно как JWT со сроком жизни в минуту. Отсюда правило выбора: время кэша равно задержке отзыва, которую вы готовы терпеть, а не «сколько не жалко». Для большинства систем это секунды или десятки секунд, а там, где отзыв обязан действовать сразу (деньги, административные операции), кэш не ставят и платят вызовом на каждый запрос.
Обновление токенов
access_token живёт недолго — 5–15 минут. refresh_token — дни или недели. Когда access_token истекает, приложение использует refresh_token, чтобы получить новый, без повторного входа пользователя.
Refresh Token Rotation — хорошая практика безопасности: при каждом обновлении IdP выдаёт новый refresh_token и инвалидирует старый. Если злоумышленник украл токен и попытался использовать его повторно — IdP заметит это и инвалидирует всю цепочку. Следующий же запрос оригинального клиента вернёт 401, что сигнализирует о компрометации.
Смотрите на отметку 20 минут: повторно предъявленный R1 не продлевает доступ, а гасит всю цепочку, и настоящий клиент на своём R2 получает 401 - это и есть сигнал о краже.
Почему access живёт минуты, а не сутки и не минуту
Цифра «5–15 минут» выглядит взятой с потолка, и понять её проще всего через вопрос «что будет, когда доступ отозвали». Роль администратора сняли, сотрудника уволили, токен украли — а выданный JWT продолжает действовать, потому что проверяется подписью и ни у кого разрешения не спрашивает. Значит, срок жизни токена — это и есть окно, в течение которого отозванный доступ ещё работает.
Отсюда обе границы. Сутки не годятся: уволенный сотрудник работает ещё сутки, и украденный токен тоже. Минута технически возможна, но дорога — каждое обновление это запрос к серверу авторизации, при тысяче активных пользователей это постоянный поток, и любая заминка провайдера становится заметна сразу всем. Пять-пятнадцать минут — компромисс, при котором обновления редки, а окно отозванного доступа терпимо.
Что делать, когда терпеть нельзя. Сокращать срок точечно: для административных ролей и денежных операций берут минуту-две, а не общее значение для всех. Отзывать не токен, а сессию: refresh_token отзывается мгновенно, и через срок жизни access пользователь просто не получит новый. А для случаев «прямо сейчас» держат отдельную проверку — сервис спрашивает у сервера авторизации статус сессии или читает признак блокировки из своего кэша, который обновляется событием.
Из того же рассуждения следует правило, которое экономит много отладки: в токене живёт только то, что не меняется за его срок. Роли в токене опасны именно тем, что меняются: сняли роль в провайдере, а в токене она есть ещё десять минут. Поэтому длинный срок жизни и роли внутри вместе не сочетаются.
Выход: почему после «Выйти» пользователь остаётся внутри
Вход в статье разобран по шагам, а выход обычно сводят к «удалить токен» — и получают дыру, которую трудно заметить: пользователь нажал «Выйти», интерфейс показал страницу входа, а доступ никуда не делся. Полный выход состоит из трёх разных действий, и пропуск любого оставляет часть доступа открытой.
Убрать токен у клиента. В схеме с BFF это удаление серверной сессии и cookie (ответ с истёкшим Set-Cookie), без BFF — удаление токенов из памяти приложения. Этот шаг делают всегда, и сам по себе он не отзывает ничего: кто успел скопировать токен, продолжит им пользоваться до конца срока.
Отозвать refresh. Долгий доступ даёт refresh_token, и его отзывают явно — у сервера авторизации для этого есть отдельная ручка отзыва. После отзыва продлить сессию нельзя, и доступ гаснет через срок жизни access-токена. Это и есть настоящая граница выхода: не мгновенно, но не дольше тех самых 5–15 минут.
Закончить сессию у провайдера. Если этого не сделать, пользователь нажимает «Выйти», потом «Войти» — и оказывается внутри без ввода пароля: сессия в Keycloak или у Google жива, и вход проходит молча. Человек читает это как «выход не работает», и он прав. Отвечает за шаг переход на адрес завершения сессии у провайдера (end_session_endpoint) с указанием id_token и адреса возврата.
Есть и четвёртая часть, про которую вспоминают последней: остальные приложения. Один вход через провайдера открыл доступ в три приложения, и выход из одного не должен оставлять открытыми два других. За это отвечает выход по обратному каналу: провайдер сам вызывает заранее настроенный адрес каждого приложения и сообщает, что сессия закончилась, а приложение удаляет свою серверную сессию. Работает это только когда адрес настроен у каждого клиента — иначе провайдер просто никого не оповещает, молча.
Проверяют выход тремя попытками, и они ловят почти все ошибки: старый токен доступа после выхода перестаёт работать не позже своего срока, попытка обновить токен отвечает отказом сразу, а повторный вход спрашивает пароль, а не впускает молча.
Авторизация: что пользователю разрешено
После того как система знает, кто пользователь, нужно решить — что ему можно. Есть три основных подхода.
RBAC — роли
Пользователю назначают роль (ADMIN, EDITOR, VIEWER), роль определяет доступные операции. Проверяется до входа в бизнес-логику.
@RestController
public class ArticleController {
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/articles/{id}")
public void delete(@PathVariable Long id) {
articleService.delete(id);
}
@PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')")
@PutMapping("/articles/{id}")
public ArticleDto update(@PathVariable Long id, @RequestBody UpdateRequest body) {
return articleService.update(id, body);
}
}
Подходит, когда набор ролей фиксирован и небольшой. Не подходит, когда нужно проверять свойства конкретного объекта.
ABAC — атрибуты
Решение о доступе принимается на основе атрибутов: пользователя, ресурса, действия и контекста. Правила выносятся в отдельный компонент политики.
@Component("access")
public class AccessPolicy {
public boolean canEditArticle(Long articleId, UserPrincipal user) {
Article article = articleRepository.findById(articleId).orElse(null);
if (article == null) return false;
return user.hasRole("EDITOR")
&& article.getDepartment().equals(user.getDepartment())
&& article.getStatus() == ArticleStatus.DRAFT;
}
}
// В контроллере:
// @PreAuthorize("@access.canEditArticle(#id, authentication.principal)")
Здесь условие сложнее: пользователь может редактировать статью только если он редактор, статья из его отдела и ещё в статусе черновика. RBAC с этим не справится — нужно знать данные конкретного объекта.
Это вид схемы с высоты. Полный разбор — компонент доступа против проверки внутри обработчика команды, состояние объекта как атрибут, списки, которые фильтруют вместо проверки, и обязательный тест «чужой объект отвечает 404» — в статье ABAC: владение ресурсом.
Resource-Based — владелец
Частный случай ABAC: пользователь видит и изменяет только свои объекты.
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id, Authentication auth) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Order not found"));
Long currentUserId = ((UserPrincipal) auth.getPrincipal()).getId();
if (!order.getUserId().equals(currentUserId)) {
throw new AccessDeniedException("Not your order");
}
return orderMapper.toDto(order);
}
Один нюанс про код отказа. 403 честно говорит «заказ есть, но не ваш» — и этим же выдаёт, что заказ с таким номером существует. Если номера идут подряд, перебором так собирают список чужих заказов. Поэтому правило такое: если само существование объекта — не публичная информация (заказы, документы, переписка), на чужой ресурс отвечают 404, ровно как на несуществующий. А если существование и так видно всем — например, карточка товара в открытом каталоге, — прятать нечего, и 403 понятнее для клиента. Подробный разбор обоих случаев — в статье ABAC: владение ресурсом.
Когда что использовать
| Модель | Когда подходит |
|---|---|
| RBAC | Фиксированные роли, грубая проверка |
| ABAC | Доступ зависит от свойств объекта или контекста |
| Resource-Based | Пользователь работает только со своими данными |
| RBAC + ABAC | RBAC на уровне API Gateway/BFF, ABAC внутри сервиса |
Авторизация в микросервисах
В микросервисной архитектуре запрос проходит через несколько уровней, и на каждом проверяется своё:
API Gateway — технический контроль: токен есть, подпись верна, срок не истёк, лимиты не превышены. Пробрасывает идентификатор пользователя в downstream-сервисы.
BFF / Application Layer — грубая проверка по роли: этому пользователю вообще доступен этот эндпоинт?
Доменный сервис — тонкая проверка: пользователь — владелец этого конкретного объекта? Статус объекта позволяет операцию? Это нельзя вынести на Gateway — он не знает доменную модель.
Правило: Gateway решает «кто», сервис решает «что можно».
Это карта уровней, а не инструкция. Каждый уровень с примером одного запроса, прошедшего все три, с разными кодами отказа, с разбором того, почему владение нельзя поднять на шлюз, и с тем же набором уровней для запроса не от человека, а от сервиса, — в статье Где делать проверку: Gateway, BFF или домен.
Когда всё это избыточно
Схемы выше описывают то, что нужно публичному сервису с браузером, мобильным приложением и несколькими бэкендами. Для части задач это лишний вес, и честнее сказать, где именно.
Внутренняя система на несколько десятков человек. Одно приложение, одна база, пользователи из одного отдела. Серверная сессия и форма входа закрывают вопрос целиком: сессия отзывается мгновенно, выход работает по-настоящему, ролей четыре и лежат они в базе. Ставить отдельный провайдер идентичности, чтобы одно приложение получало токены само у себя, значит добавить сервис, базу, обновления и новую точку отказа ради схемы, которая ничего здесь не решает. Развилка появляется, когда приложений становится больше одного и людям надоедает входить в каждое; тогда и стоит читать статью про Keycloak.
Сессия честнее токенов, если клиент только браузер. Схема с BFF и так заканчивается серверной сессией в cookie, а токены в ней — внутренняя механика между вашим приложением и вашим провайдером. Если внешних клиентов нет и мобильного не планируется, сессия в хранилище даёт то, чего у токенов нет из коробки: мгновенный отзыв, список активных сессий пользователя и работающий выход без всяких обратных каналов.
Непрозрачный токен там, где важен отзыв. Когда сервисов немного и они рядом с сервером авторизации, непрозрачный токен с коротким кэшем проверок проще: отзыв действует почти сразу, и не нужно ничего придумывать про окно в 5–15 минут. JWT выигрывает при масштабе — много сервисов, много запросов и нежелание зависеть от провайдера на каждом вызове.
Общее правило: сложность схемы входа оправдана числом клиентов и числом сервисов, а не желанием «сделать правильно». Лишний провайдер идентичности в маленькой системе даёт не безопасность, а ещё один сервис, за обновлениями которого никто не следит.
Коротко
- Аутентификация — кто ты, авторизация — что тебе можно. Это разные задачи.
- Session ID инвалидируется мгновенно, JWT проверяется локально без обращения к серверу.
- Для браузера — токены только в
HttpOnlycookie, никогда вlocalStorage. - OAuth2 + PKCE — стандарт для SPA и мобильных приложений: пользователь входит у IdP, приложение пароля не видит.
- Для мобильных — вход через системный браузер (не WebView), токены в Keychain.
- Refresh Token Rotation защищает от повторного использования украденного токена.
- RBAC — роли, ABAC — атрибуты объекта и контекста, Resource-Based — владелец; в микросервисах шлюз проверяет токен, а доменный сервис — бизнес-правила.
- Срок жизни access — это окно, в котором отозванный доступ ещё работает: отсюда 5–15 минут, точечно короче для админов и денег, и в токене живёт только то, что за его срок не меняется.
- Выход — три действия: убрать токен у клиента, отозвать
refresh, закончить сессию у провайдера; остальным приложениям о выходе сообщает обратный канал, если у них настроен адрес. - Сложность схемы оправдана числом клиентов и сервисов: для внутренней системы на десятки человек серверная сессия честнее токенов, а непрозрачный токен с кэшем проверок проще там, где важен быстрый отзыв.
Что почитать дальше
- Где какая проверка: Gateway, BFF или домен - тот же запрос по трём уровням и три разных кода отказа.
- ABAC: владение ресурсом - как проверить, что объект принадлежит именно этому пользователю, и что отвечать на чужой.
- Service-to-service: mTLS и Client Credentials - как сервисы удостоверяют друг друга, когда человека за запросом нет.
- Аудит действий администратора - что писать в журнал, когда права позволяют обойти проверку владения.
- PII и секреты - куда утекают персональные данные и где держать ключи, если не в репозитории.
- PCI DSS для разработчика - что меняется в коде, когда через него проходят карточные данные.
- GDPR для разработчика - права субъекта, срок хранения и удаление, переведённые в требования к коду.
- Auth Patterns Style Guide — правила для ревью кода с кодами
AUTH-N. - REST API: заголовки и трассировка — заголовок
Authorizationи идемпотентность.