12k
All articles

Выбор стратегии аутентификации для нового веб-приложения

Выберите аутентификацию для веб‑приложения в 2026: сессии или JWT, OAuth 2.0/OIDC, passkeys и managed providers, с учетом revocation, CSRF и UX.

OpenReplay Team
OpenReplay Team
Выбор стратегии аутентификации для нового веб-приложения

Для нового веб-приложения в 2026 году стратегия определяется архитектурой приложения: серверный монолит с рендерингом на стороне сервера следует начинать с серверных сессий в подписанном HttpOnly-куки; SPA, мобильный клиент или API-бэкенд должны использовать краткосрочные токены доступа с ротацией refresh-токенов, хранящихся в HttpOnly-куки; всё, что требует социального входа или корпоративного SSO, должно строиться поверх OAuth 2.0/OIDC; а если аутентификация не является вашим продуктом, наиболее обоснованным выбором по умолчанию будет управляемый провайдер. Добавьте passkeys в качестве фишинго-устойчивого фактора независимо от выбранной базы. Остальная часть статьи обосновывает каждое из этих решений по конкретным критериям — отзыв доступа, масштабирование, уязвимость к CSRF/XSS, сложность и сбои UX, которые проявляются только в продакшене.

Типичная ловушка, в которую попадают новые приложения, — это использование JWT потому, что в каком-то туториале применялись именно они, без реальной причины для stateless-масштабирования, которая оправдывала бы издержки на отзыв токенов. Это сравнительное решение, а не выбор по умолчанию — поэтому ниже приведены критерии, сравнительная таблица и матрица, сопоставляющая архетип приложения с рекомендацией.

Ключевые выводы

  • Сессии против токенов — это вопрос о том, где хранится состояние аутентификации: на сервере или в клиентском токене, тогда как куки против заголовка Authorization определяют лишь как передаётся учётная запись; смешение этих понятий — наиболее распространённая причина, по которой новые приложения слепо копируют JWT, которые им не нужны.
  • Хранение JWT в localStorage является антипаттерном: любая XSS-уязвимость на странице позволяет прочитать и похитить токен, поэтому храните учётные данные в HttpOnly-куки, недоступных для JavaScript.
  • Главная проблема с JWT — не их выдача, а их отзыв; мгновенный выход из системы требует либо коротких сроков жизни токенов с ротацией refresh-токенов, либо серверного списка запрещённых токенов, что незаметно возвращает то самое состояние, от которого JWT призваны были избавить.
  • OAuth 2.1 по-прежнему является интернет-черновиком IETF (draft-15, март 2026 года), а не ратифицированным RFC, однако его направление определено: обязательный PKCE, точное сопоставление redirect URI и исключение потоков Implicit и Resource Owner Password Credentials.
  • Если аутентификация не является вашим продуктом, управляемый провайдер (Auth0, Clerk, Supabase Auth, WorkOS, Stytch) — это прагматичный выбор по умолчанию для 2026 года: вы получаете сессии, социальный вход, MFA и passkeys, не неся ответственности за поверхность атаки.

Ключевой фрейм: где живёт состояние и как оно передаётся

Сессии против токенов — это вопрос о том, где хранится ваше состояние аутентификации: на сервере или внутри клиентского токена, тогда как куки против заголовка Authorization определяют лишь как эти данные передаются по HTTP. Это ортогональные оси, и трактовка их как единого решения — причина, по которой многие команды внедряют JWT, которые им не нужны. Это разграничение является ключевым для всего изложенного ниже, и аутентификационный вендор Authgear формулирует это так же.

Сессия stateful: сервер хранит авторитетную запись и выдаёт клиенту непрозрачный идентификатор, указывающий на неё — модель гостиничной карточки-ключа, где стойка регистрации ведёт главный список. JWT stateless: утверждения самодостаточны и подписаны, поэтому любой сервер может проверить их без обращения к хранилищу — модель цифрового паспорта, где любой офицер доверяет подписи, не связываясь с выдавшим органом. Вы можете передавать любой из этих вариантов как через куки, так и через заголовок Authorization. Куки и заголовки — это транспорт; сессии и токены — это архитектура.

КритерийСерверные сессииJWT / самодостаточные токены
Где хранится состояниеСервер (хранилище)Клиент (подписанный токен)
ОтзывМгновенный — удалить записьСложный — короткий TTL + ротация или список запрещённых
МасштабированиеТребует общего хранилища (например, Redis)Stateless; без обращения к хранилищу на каждый запрос
Размер учётных данных~32 байта непрозрачного IDЧасто 800+ байт утверждений на каждый запрос
Основная поверхность атакиCSRF (куки отправляются автоматически)XSS (кража токена из доступного JS хранилища)
Лучшее применениеСерверный монолитAPI-first, SPA, мобильные, микросервисы

Разница в размере — это суть компромисса stateless против stateful в одной строке: сессионный куки несёт примерно 32 байта непрозрачного ID и требует обращения к серверу, тогда как JWT несёт утверждения inline — часто 800+ байт при каждом запросе — и обращение к хранилищу не требуется. Вы обмениваете пропускную способность и задержку при отзыве на устранение необходимости в хранилище.

Сессии: выбор по умолчанию для серверных приложений

Для серверного монолита начните с серверных сессий в подписанном, HttpOnly, Secure, SameSite-куки — это простейший правильный вариант, обеспечивающий мгновенный отзыв доступа бесплатно. Браузер отправляет куки автоматически, вы не храните состояние аутентификации в JavaScript, а выход пользователя из системы — это одно удаление на сервере.

Механика состоит из четырёх шагов: пользователь отправляет учётные данные, сервер создаёт запись сессии и возвращает её ID в куки, браузер воспроизводит этот куки при каждом запросе, а выход из системы уничтожает обе стороны. Хешируйте пароли с помощью Argon2 до их сохранения — OWASP Password Storage Cheat Sheet рассматривает его как текущий алгоритм первого выбора. Генерируйте идентификаторы сессий с высокой энтропией и никогда не встраивайте в них конфиденциальные данные; OWASP Session Management Cheat Sheet является каноническим справочником по длине ID, ротации и таймаутам.

# FastAPI: issue an opaque session, store it server-side, set a hardened cookie
import secrets
from fastapi import FastAPI, Response
# redis client + Argon2 password verification omitted for brevity

app = FastAPI()
SESSION_TTL = 60 * 60 * 24  # 24 hours

@app.post("/login")
async def login(response: Response):  # verify Argon2 hash first
    session_id = secrets.token_urlsafe(32)          # ~256 bits of entropy
    await redis.setex(f"sess:{session_id}", SESSION_TTL, user_id)
    response.set_cookie(
        "session", session_id,
        httponly=True,    # JS cannot read it -> blunts XSS exfiltration
        secure=True,      # HTTPS only
        samesite="lax",   # mitigates CSRF on top-level navigations
        max_age=SESSION_TTL,
    )
    return {"ok": True}

Этот фрагмент демонстрирует суть сессий: секрет никогда не живёт в JavaScript, а сервер является источником истины. Из этого следуют два компромисса. Первый — масштабирование: поскольку запись хранится на сервере, развёртывание с несколькими инстансами требует sticky-сессий или, что предпочтительнее, общего хранилища вроде Redis, чтобы любой инстанс мог обработать любую сессию. Второй — CSRF: куки отправляются автоматически при кросс-сайтовых запросах, поэтому сочетайте SameSite с CSRF-токеном, используя сравнение за постоянное время, согласно OWASP CSRF Cheat Sheet. Флаг HttpOnly закрывает путь чтения через XSS; SameSite в сочетании с токеном закрывает путь CSRF.

JWT и токены: stateless-масштабирование с издержками на отзыв

Используйте сырые JWT только при наличии конкретной причины для stateless-масштабирования — API, обслуживающий несколько типов клиентов, микросервисы, передающие идентификационные данные между собой, или кросс-доменный доступ, где куки неудобны, — а не как выбор по умолчанию. Выгода реальна: подписанный токен проверяется локально по открытому ключу, что позволяет пропустить обращение к хранилищу при каждом запросе, и любой узел может аутентифицировать любой запрос.

Главная проблема с JWT — не их выдача, а их отзыв. Поскольку токен самодостаточен, он остаётся действительным до истечения срока действия независимо от того, что думает ваш сервер. Мгновенный выход из системы поэтому требует либо коротких сроков жизни токенов с ротацией refresh-токенов, либо серверного списка запрещённых токенов, который вы проверяете при каждом запросе — и этот список незаметно возвращает то самое состояние, от которого JWT призваны были избавить. Стандартный паттерн — краткосрочный токен доступа в паре с долгосрочным ротируемым refresh-токеном; OAuth 2.0 Security Best Current Practice (RFC 9700, январь 2025) является ратифицированным источником для ротации refresh-токенов и коротких сроков жизни токенов доступа — более надёжным, чем круглые значения TTL из блог-постов.

Место хранения токена определяет масштаб потенциального ущерба. Хранение JWT в localStorage является антипаттерном: любая XSS-уязвимость на странице позволяет прочитать и похитить токен, поэтому храните токены в HttpOnly-куки, недоступных для JavaScript. Для браузерных приложений черновик IETF OAuth for Browser-Based Apps рекомендует поток Authorization Code с PKCE и предупреждает, что вредоносный JavaScript способен на значительно большее, чем просто чтение токена.

# FastAPI: verify an access token with PyJWT (not python-jose)
import jwt  # PyJWT
from datetime import datetime, timezone
from fastapi import HTTPException

def verify_access_token(token: str, public_key: str) -> dict:
    try:
        return jwt.decode(
            token, public_key,
            algorithms=["RS256"],          # never accept "none"; pin the alg
            options={"require": ["exp", "iat", "sub"]},
        )
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "token expired")  # client should refresh
    except jwt.InvalidTokenError:
        raise HTTPException(401, "invalid token")

# When you mint tokens, use timezone-aware UTC:
# exp = datetime.now(timezone.utc) + timedelta(minutes=15)

Два замечания по версиям обеспечивают актуальность кода. Используйте PyJWTофициальный туториал FastAPI по безопасности перешёл на него (import jwt) вместе с pwdlib, тогда как python-jose теперь поддерживается лишь минимально. Задавайте время истечения через datetime.now(timezone.utc): функция datetime.utcnow() была объявлена устаревшей в Python 3.12 и, согласно документации Python по ожидаемым удалениям, по-прежнему помечена как deprecated без указания версии удаления — она работает, но вызов с учётом часового пояса является рекомендуемой заменой.

OAuth 2.0/2.1 и OIDC: делегированная идентификация когда это необходимо

Используйте OAuth 2.0 с OpenID Connect, когда вы хотите, чтобы пользователи входили через существующего провайдера идентификации — «Войти через Google», GitHub, Microsoft, Okta — или когда вам нужен корпоративный SSO. Это модель VIP-списка гостей: вы не проверяете пользователя самостоятельно, а доверяете эмитенту, который уже его знает. OAuth 2.0 обеспечивает делегированную авторизацию (токены доступа для API), а OpenID Connect добавляет поверх него токен идентификации, позволяя узнать, кто является пользователем.

Сложность оправдана только тогда, когда делегирование или SSO является реальным требованием, поскольку поток включает реальные движущиеся части — коды авторизации, PKCE, области видимости, redirect URI и ротацию ключей JWKS. Для MVP с собственным входом и без требований к социальному входу OAuth избыточен: вы будете нести накладные расходы на механизм перенаправления, не используя делегирование, ради которого он существует.

Если вы всё же его принимаете, ориентируйтесь на современный профиль. OAuth 2.1 по-прежнему является интернет-черновиком IETF (draft-15, опубликован 2 марта 2026 года), а не ратифицированным RFC, однако его направление определено. Основные отличия от OAuth 2.0 состоят в том, что PKCE обязателен для всех клиентов, использующих поток кода авторизации, redirect URI должны сравниваться с точным строковым совпадением, а потоки Implicit и Resource Owner Password Credentials исключены. IETF сам отмечает, что интернет-черновики действительны не более шести месяцев и что на них неуместно ссылаться иначе как на «работу в процессе», поэтому воспринимайте 2.1 как целевую консолидацию — уже применяемую на практике крупными провайдерами идентификации, — а не как выпущенный стандарт. Суть важнее: используйте Authorization Code + PKCE для каждого клиента, включая конфиденциальные серверные, и откажитесь от wildcard-redirect URI.

Passkeys и WebAuthn: фишинго-устойчивое направление развития

Добавляйте passkeys в качестве фишинго-устойчивого фактора, а не как самоцель. Построенный на WebAuthn/FIDO2, passkey представляет собой пару открытого/закрытого ключей, где закрытый ключ остаётся в защищённом аппаратном обеспечении устройства и подписывает серверный вызов после локальной биометрической проверки или ввода PIN-кода. Поскольку учётные данные привязаны к origin и никогда не передаются, они устойчивы к фишингу, повторным атакам и подбору учётных данных так, как пароли и SMS-коды не могут быть.

Политическая почва сместилась решительно. В апреле 2026 года NCSC объявил, что passkeys должны стать первым выбором пользователей для входа во все цифровые сервисы, и что организация начнёт рекомендовать passkeys везде, где сервис их поддерживает, и двухэтапную верификацию там, где нет — изменение, отражённое в продолжающемся обновлении руководства, а не в единовременном резком переходе. Поддержка фактически универсальна: passkeys поддерживаются основными операционными системами, включая Windows, ChromeOS, macOS, Android, iOS и многие дистрибутивы Linux, а также браузерами Chrome, Firefox, Edge и Safari.

Поддержка passkeys решена; регистрация passkeys — нет, и именно это является реальным ограничением при выборе. По данным Corbado’s Passkey Benchmark 2026 (вендорный бенчмарк, а не нейтральная статистика по населению), первая попытка веб-регистрации успешна примерно в 49–83% случаев на iOS, но лишь в 25–39% на Windows. Рассматривайте UX регистрации и восстановление аккаунта как задачи первого класса: предлагайте passkeys как опциональное улучшение с понятным запасным вариантом и проектируйте восстановление до запуска, поскольку потерянное устройство без пути восстановления — это заблокированный пользователь.

Auth-as-a-Service: прагматичный выбор по умолчанию, когда аутентификация не ваш продукт

Если аутентификация не является вашим продуктом, наиболее обоснованным выбором по умолчанию в 2026 году является управляемый провайдер — Auth0, Clerk, Supabase Auth, WorkOS или Stytch — поскольку вы получаете сессии, социальный вход, MFA и passkeys, не неся ответственности за поверхность атаки или техническое обслуживание. Эти вендоры поставляют готовые SDK, обрабатывающие полный жизненный цикл, включая механизм перенаправления OAuth и WebAuthn-церемонии, которые вам иначе пришлось бы строить и отлаживать самостоятельно.

Решение очевидно экономически. Собственная реализация даёт контроль и нулевую стоимость за MAU; управляемый провайдер обеспечивает защищённую реализацию, более быстрый выход на рынок и чужую дежурную ротацию для критически важного с точки зрения безопасности пути. Для большинства новых приложений, где вход является базовым требованием, а не конкурентным преимуществом, управляемый путь — это рациональная отправная точка, и вы сможете мигрировать с него позже, если этого потребует масштаб или стоимость.

Матрица решений для методов аутентификации

Стратегия следует за архетипом приложения. Найдите свой случай в строке, начните с него и добавьте passkeys во всех случаях как фишинго-устойчивый фактор.

Архетип приложенияНачните сДобавьте при необходимости
Серверный монолитСерверные сессии (HttpOnly-куки)OIDC для социального входа; passkeys опционально
SPA / мобильное / API-бэкендКраткосрочные токены доступа + ротация refresh, в HttpOnly-кукиOIDC для сторонней идентификации
Нужен социальный вход или корпоративный SSOOAuth 2.0 + OIDC (целевой профиль 2.1)Сессии или токены для собственных поверхностей
Аутентификация не ваш продуктУправляемый провайдер (Auth0/Clerk/Supabase/WorkOS/Stytch)— уже включает всё остальное
Высокая безопасность / потребительское приложениеЛюбая из вышеперечисленных баз + passkeysПоток восстановления спроектирован заранее

Типичная продакшен-архитектура является гибридной: сессии для серверного веб-приложения, JWT для API, потребляемого SPA и мобильными приложениями, OIDC для социального входа и passkeys как опциональное улучшение. Отраслевая тенденция, лежащая в основе всего этого, — отправлять пароль реже, в меньшем количестве мест, с большей областью видимости и подотчётностью, что именно и объясняет, почему токены с ограниченной областью видимости заменили аутентификацию Git на основе пароля и почему passkeys сейчас вытесняют пароли полностью.

Что воспроизведение сессий раскрывает о сбоях UX аутентификации

Воспроизведение сессий — это техника, воссоздающая реальный клиентский поток, и воспроизведение процессов аутентификации выявляет узнаваемый каталог сбоев, которые упускают серверные логи, — потому что выбор правильной стратегии на бумаге не гарантирует, что пользователи действительно смогут войти в систему (это возможности техники, а не измеренные показатели инцидентов):

  • Циклы выхода из-за незаметного истечения токена. Когда refresh-токен истекает в середине сессии, SPA может многократно перенаправлять пользователя на страницу входа. Воспроизведение показывает цикл перенаправления на временной шкале, который упускают логи ошибок, поскольку каждый отдельный запрос возвращает технически корректный 401.
  • Незавершённая регистрация passkey. Воспроизведение показывает, где пользователи прерывают WebAuthn-церемонию — появляется системный запрос ОС, и пользователь отменяет его — что является именно тем сбоем, который предсказывают низкие показатели регистрации на Windows.
  • Тупики перенаправления OAuth. Перенаправление после подтверждения, ведущее на пустую страницу или страницу ошибки (несовпадающий redirect URI, потерянный параметр state), невидимо в серверных логах, поскольку цикл происходит вне вашего сервера; воспроизведение фиксирует тупик так, как его видел пользователь.
  • Сбои куки в конкретных браузерах. Проблемы с SameSite и Secure-куки, которые не воспроизводятся локально, проявляются, когда воспроизведение соотносит неудачную сессию с конкретным браузером и версией.

Суть в том, что аутентификация — это поверхность UX, а не только выбор протокола, и выбранная стратегия определяет, какие из этих классов сбоев вам придётся отлаживать.

Выберите базу, соответствующую архитектуре вашего приложения, защитите хранилище учётных данных (HttpOnly-куки, никогда localStorage) и добавьте passkeys как опциональный фишинго-устойчивый фактор. Для большинства новых приложений честным первым шагом является управляемый провайдер или монолит с приоритетом сессий; используйте сырые JWT только тогда, когда реальное требование stateless-масштабирования оправдывает издержки на отзыв. После запуска входа наблюдайте за реальным потоком — разрыв между обоснованной стратегией и работающей заполнен именно теми деталями UX, которые проявляются только в продакшене.

Часто задаваемые вопросы

Следует ли использовать JWT по умолчанию для нового веб-приложения?

Нет. Используйте сырые JWT только при наличии конкретной причины для stateless-масштабирования, например API, обслуживающего несколько типов клиентов, или микросервисов, передающих идентификационные данные между собой. Для серверного монолита серверные сессии проще и обеспечивают мгновенный отзыв доступа. JWT самодостаточны, поэтому их отзыв до истечения срока действия требует либо коротких сроков жизни токенов с ротацией refresh-токенов, либо серверного списка запрещённых токенов, что возвращает то самое серверное состояние, от которого JWT призваны были избавить.

Является ли сессионный токен тем же, что и JWT?

Нет. Сессионный токен — это непрозрачный идентификатор примерно в 32 байта, указывающий на состояние аутентификации, хранящееся на сервере, поэтому каждый запрос требует обращения к хранилищу. JWT несёт свои утверждения inline и проверяется локально по подписи без обращения к хранилищу, часто добавляя 800 и более байт на каждый запрос. Эти два варианта представляют компромисс stateless против stateful: сессия хранит состояние на сервере, тогда как JWT перемещает его в подписанный токен клиента.

Могу ли я хранить JWT в localStorage, если я санирую входные данные?

Нет, хранение JWT в localStorage является антипаттерном независимо от санирования входных данных, поскольку любая уязвимость межсайтового скриптинга в любом месте страницы позволяет прочитать localStorage и похитить токен. Храните учётные данные в HttpOnly-куки, недоступных для JavaScript, и сочетайте их с SameSite и CSRF-токеном. Для браузерных приложений черновик IETF OAuth for Browser-Based Apps рекомендует поток Authorization Code с PKCE вместо хранилища токенов, доступного для клиентского кода.

Является ли OAuth избыточным для MVP с собственным входом?

Да, если ваш MVP требует только собственного входа без социального входа или корпоративного SSO, OAuth 2.0 добавляет механизм, который вы не будете использовать. Поток включает коды авторизации, PKCE, области видимости, redirect URI и ротацию ключей JWKS — всё это существует для поддержки делегированной идентификации. Принимайте OAuth с OpenID Connect только тогда, когда «Войти через Google», GitHub или корпоративный SSO является реальным требованием; в противном случае начните с сессий или управляемого провайдера.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.