AUTH¶
Идентификация, аутентификация, авторизация¶
Идентификация - заявление, кто ты, без проверки: ввод логина, email или другого уникального идентификатора.
Аутентификация - проверка, что ты действительно тот, за кого себя выдаёшь: пароль, биометрия, двухфакторная проверка.
Авторизация - проверка прав после успешной аутентификации. Отвечает на вопрос «что этому пользователю можно делать в системе?»: какие страницы, функции и данные ему доступны.
Note
Не прошёл аутентификацию - 401 Unauthorized. Вошёл, но прав нет - 403 Forbidden.
Способы аутентификации¶
- Логин и пароль
- Email и код - пользователь переходит по ссылке из письма или вводит код из него, почта служит идентификатором
- SMS-код, OTP (One-Time Password) - одноразовый код
- Биометрия - отпечаток пальца, лицо
- Push-уведомление - вход подтверждают на другом устройстве
- QR-код - его сканируют телефоном, на котором уже выполнен вход
- USB-ключи и физические токены - YubiKey и другие ключи FIDO2
Двухфакторная аутентификация (2FA)¶
2FA - вход по двум факторам: к паролю добавляется одноразовый код на телефон или из приложения-аутентификатора.
Кроме входа, 2FA просят на чувствительных операциях: смена пароля, доступ к личным данным.
Хранение паролей¶
В БД лежит не пароль, а его хэш. При входе сервер хэширует введённый пароль и сравнивает с сохранённым.
- Не годятся: MD5, SHA-1, SHA-256. Это быстрые хэши: GPU перебирает миллиарды вариантов в секунду, и соль от перебора не спасает.
- Безопасные: bcrypt, Argon2. Они медленные специально, их стоимость (число раундов, память) настраивается.
Соль - уникальная случайная строка, которую добавляют к каждому паролю перед хэшированием.
- одинаковые пароли дают разные хэши, заранее посчитанные таблицы (rainbow tables) не помогают
- хранится рядом с хэшем: bcrypt и Argon2 кладут её прямо в строку хэша
- формат bcrypt:
$2b$12$+ 22 символа соли + 31 символ хэша, где12- cost, 2^12 раундов
Перец - общий секрет для всех паролей. Добавляется отдельно и хранится вне БД: в конфиге или хранилище секретов. Утекла одна БД - без перца хэши не перебрать.
Basic Auth¶
Cookie¶
Cookie - пара имя=значение, которую сервер ставит заголовком Set-Cookie. Браузер хранит её и сам отправляет
в заголовке Cookie с каждым запросом на тот же домен и путь.
Атрибуты:
| Атрибут | Что делает |
|---|---|
Domain |
Для какого домена отправлять. Без него - только хосту, который поставил cookie, с ним - ещё и поддоменам |
Path |
Для каких путей. Path=/ - для всего сайта |
Expires / Max-Age |
Время жизни. Без них это session cookie: живёт до закрытия браузера |
HttpOnly |
Cookie недоступна из JavaScript, XSS её не прочитает |
Secure |
Отправляется только по HTTPS |
SameSite |
Слать ли cookie с запросов, начатых на другом сайте: Strict - никогда, Lax - только при переходе по ссылке, None - всегда, нужен Secure |
Удаление - та же cookie с пустым значением и датой в прошлом или Max-Age=0. Path и Domain должны совпадать
с исходными:
Cookie с HttpOnly из JavaScript не удалить, её стирает только сервер.
Диагностика: DevTools -> Application -> Cookies, или curl -i <url> покажет Set-Cookie в ответе.
Сессии (Session Authentication)¶
После аутентификации сервер должен узнавать пользователя на каждом следующем запросе. Способов два: сессия и токен.
Сессия - состояние входа, которое хранит сервер. Клиенту уходит только session ID в cookie, это stateful-подход.
Жизненный цикл сессии¶
- Создание. При логине сервер генерирует session ID и заводит запись в хранилище сессий. Гостевую сессию, например для корзины, создают уже на первом запросе.
- ID уходит клиенту в cookie:
Set-Cookie: JSESSIONID=abc123xyz456; Path=/; HttpOnly; Secure; SameSite=Lax. - Каждый запрос. Браузер хранит cookie и сам прикладывает её ко всем запросам на этот сайт.
- Проверка. Сервер ищет сессию по ID в хранилище:
- не нашёл -
401или редирект на страницу входа - нашёл - проверяет, что сессия не истекла, достаёт userId и права и решает, пускать ли к ресурсу
- не нашёл -
- Завершение.
- По таймауту. Сессию закрывает таймаут на сервере, а не срок cookie: истёкшая cookie только перестаёт отправляться.
- При выходе. Сервер удаляет сессию и стирает cookie.
- Программно. Сервер сам закрывает сессию при угрозе безопасности или смене прав.
Session ID¶
Session ID - уникальный идентификатор сессии, его генерирует сервер. Сам по себе ничего не значит: это ключ к записи в хранилище, где лежат данные пользователя.
- случайная строка из букв и цифр, например
abc123xyz456или UUIDd9f9b76d-315b-487d-a456-8308dfb87f2e - берётся из криптостойкого генератора, чтобы ID нельзя было подобрать
- имя cookie зависит от платформы:
JSESSIONIDв Java,PHPSESSIDв PHP, или своё, напримерsessionid
Украли ID - получили чужую сессию (session hijacking), см. Защита сессии.
Что хранить в сессии¶
- userId - если пользователь вошёл
- Данные сеанса - время последнего запроса, текущее состояние: корзина, фильтры
- Права - роли и разрешения, чтобы решать о доступе без похода в БД
- Служебные данные входа - одноразовые коды, токены других механизмов аутентификации
Где хранить сессии¶
| Где | Скорость | После перезапуска | Несколько серверов |
|---|---|---|---|
| Память процесса | Самая быстрая | Сессии теряются | У каждого сервера свои, балансировщик держит пользователя на одном сервере (sticky sessions) |
| Redis, Memcached | Быстро, данные в RAM | Redis восстановит из RDB-снапшота или AOF, если они включены. Memcached теряет всё | Общее хранилище |
| БД, таблица сессий | Медленнее | Сохраняются | Общее хранилище |
| Файлы на диске, по умолчанию в PHP | Медленнее | Сохраняются | У каждого сервера свои |
В БД часто держат только саму сессию: ID, пользователя, время последнего доступа. Данные пользователя лежат в своих таблицах.
Бывает и сессия на клиенте: Flask по умолчанию кладёт данные сессии в подписанную cookie. По сути это тот же подход, что у JWT.
Защита сессии¶
Украденный session ID - вход в чужой аккаунт. Исключить кражу полностью нельзя, но риск сильно снижают:
- HTTPS везде и cookie с
SecureиHttpOnly. ID не перехватят по сети (MITM) и не прочитают скриптом (см. Cookie, TLS). - Таймауты и инвалидация. Сессия истекает после простоя, например через 15 минут. При выходе и по истечении ID удаляется на сервере, чтобы его нельзя было использовать повторно.
- Новый ID после входа. Сервер выдаёт новый session ID при логине и смене прав. Иначе работает session fixation: атакующий заранее подсовывает жертве свой ID и ждёт, пока она войдёт. Менять ID можно и периодически, но не на каждом запросе: параллельные запросы со старым ID начнут падать.
- Привязка к IP и User-Agent. Если они сменились, сессию, возможно, украли или открыли на другом устройстве. IP у мобильных клиентов меняется сам, поэтому такую проверку делают мягкой.
- CSRF-токен на изменяющих запросах (см. CSRF).
- 2FA на чувствительных операциях (см. 2FA).
- Мониторинг. Логи и алерты на подозрительную активность: входы с разных IP за короткое время, серии неудачных попыток входа.
- Шифрование данных сессии на сервере. Утечка хранилища не раскроет данные пользователя и его действия.
- Обновления. Сервер, фреймворки и библиотеки сессий обновляют, чтобы не ловить известные уязвимости.
Итого: главное - HTTPS, защищённые cookie, таймауты и инвалидация сессии на сервере.
Токены (Token Based Authentication)¶
Токен - строка, которая подтверждает, что пользователь прошёл аутентификацию. Клиент передаёт её с каждым запросом к защищённым ресурсам.
- Stateless - все данные внутри токена: ID пользователя, время жизни, подпись от подделки. Сервер ничего не хранит, поэтому такие токены удобны при масштабировании. Так устроен JWT.
- Stateful (opaque) - токен лишь ссылка на данные, которые сервер хранит в БД или Redis. По сути это тот же session ID, зато доступ легко отозвать: удалил запись.
См. Сессии или JWT и Stateful и Stateless в Spring Security.
JWT (JSON Web Token)¶
JWT - stateless-токен: подписанный JSON с данными пользователя. Сервер проверяет подпись и верит содержимому, не обращаясь к хранилищу.
Структура¶
Три части через точку, каждая закодирована в base64url: HEADER.PAYLOAD.SIGNATURE.
- Header - тип токена и алгоритм подписи:
HS256,HS512,RS256 - Payload - claims, данные о пользователе и сроке:
sub,exp, роли - Signature - подпись header и payload, без ключа её не подделать
header: payload: signature:
{ { HMACSHA512(
"alg": "HS512", "sub": "1", base64url(header) + "." + base64url(payload),
"typ": "JWT" "username": "username", secret
} "role": ["admin", "manager"], )
"exp": 1767225600
}
Секрета в токене нет. Подпись считается из первых двух частей и секрета, который знает только сервер. Поменял хоть символ в payload - подпись не сойдётся.
Header и payload - это кодирование, а не шифрование: прочитать их может любой, у кого есть токен, а подделать без ключа нельзя. Если содержимое надо скрыть, токен шифруют (JWE).
Стандартные claims: sub - кто, exp - до какого момента действует (Unix time), iat - когда выдан, iss - кто
выдал, aud - для кого.
Диагностика - прочитать payload без ключа:
Подпись: HMAC и RSA¶
- HMAC (
HS256,HS512) - один общий секрет: им подписывают и им же проверяют. Кто может проверить токен, тот может и выпустить свой. Проверка самая дешёвая. - RSA (
RS256) - подписывают приватным ключом, он есть только у сервера аутентификации. Проверяют публичным, его можно раздать всем сервисам.
Что класть в токен¶
- Положить можно любые claims, но кладут минимум для аутентификации и авторизации: userId, username, роли.
- Никаких паролей и персональных данных: payload прочитает любой, у кого есть токен.
- Роли в токене экономят поход в БД, но устаревают: роль сменили, а токен со старой ролью живёт до
exp. Если в токене только userId, права приходится читать из БД на каждом запросе, а сессия держит их под рукой. - Каждый claim увеличивает токен, а он едет в каждом запросе.
Проверка JWT на сервере¶
Сервер разбирает и проверяет токен на каждом запросе:
- Достаёт токен из заголовка
Authorization: Bearer <token>или из cookie. - Проверяет подпись: секретом для HMAC или публичным ключом для RSA.
- Проверяет
exp: не истёк ли токен. - Берёт из payload userId, роли, scope и решает, можно ли выполнить запрос.
- Всё в порядке - обрабатывает запрос. Подпись не сошлась или токен истёк -
401 Unauthorizedс описанием ошибки.
Разбор и проверка подписи на каждом запросе - лишняя работа для сервера, но с HMAC она минимальна.
Access и refresh token¶
Пара токенов: утёкший access живёт недолго, а вход можно отозвать через refresh.
- Access token - короткоживущий, 15-30 минут. Ходит в каждом запросе к API.
- Refresh token - долгоживущий, 7-60 дней. Нужен только для получения нового access. Сервер хранит его в БД, поэтому может отозвать.
Где хранить на клиенте¶
| Токен | Где | Почему |
|---|---|---|
| Access | В памяти приложения: state, переменная | localStorage и sessionStorage читает любой JS на странице, XSS украдёт токен |
| Refresh | Cookie с HttpOnly, Secure, SameSite |
JS её не прочитает, браузер сам приложит её к запросу на обновление |
Часто access кладут в localStorage: так проще, и токен переживает перезагрузку страницы. Цена - токен доступен XSS. Access из памяти теряется при перезагрузке, и клиент просто получает новый через refresh.
Полный цикл¶
- Логин. Клиент отправляет
POST /api/loginс{email, password}, сервер сверяет их с БД. Если всё верно, генерирует access и refresh, refresh сохраняет в БД. Access уходит в теле ответа, refresh - в cookie:Set-Cookie: refreshToken=...; HttpOnly; Secure. - Запросы к API. Клиент кладёт access в заголовок
Authorization: Bearer <access>. Сервер проверяет токен (см. Проверка JWT на сервере): валиден -200и тело ответа, нет -401. - Обновление. Обработчик на
401вызываетPOST /api/refresh, браузер сам прикладывает cookie с refresh. Сервер проверяет refresh: подпись иexp, если это JWT, и что он есть в БД, ведь отозванного там уже нет. Выдаёт новый access и новый refresh: на каждом обновлении (rotation, старый удаляется из БД) или когда старый скоро истечёт. Клиент заменяет access в памяти, cookie с refresh браузер обновит сам. - Выход. Клиент вызывает
POST /api/logout, refresh приходит в cookie. Сервер удаляет его из БД и стирает cookie. Access отозвать нельзя, он доживает доexp. Клиент удаляет access из памяти.
Итого: короткий access ограничивает ущерб от утечки, refresh в БД даёт отзыв, и пользователь при этом не вводит пароль каждые 15 минут.
Отзыв и утечка токена¶
JWT нельзя отозвать до exp: пользователь вышел или у него сменились права, а токен всё ещё валиден. Refresh этого
не решает. Старый access он не отменяет, а только позволяет держать access коротким.
Способы отзыва:
- Короткий access и refresh в БД. Удалил refresh - через 15-30 минут доступ пропадёт сам.
- Blacklist. Список отозванных токенов (по
jti) в Redis, сервер сверяется с ним на каждом запросе. Это возвращает на сервер состояние. - Версия токенов. У пользователя в БД счётчик или хэш, в токене - его копия. Сменил значение в БД - все старые токены пользователя недействительны.
- Opaque-токен с проверкой через introspection (см. Безопасность микросервисов).
Утечка:
- Украденный access работает до
exp, украденный refresh - пока его не отзовут. - Refresh скомпрометирован: отозвать все токены пользователя (сменить версию или хэш в БД) и потребовать вход заново.
- Токены крадут через XSS из localStorage и перехватом без HTTPS (см. Атаки).
Сессии или JWT¶
Защищают их одинаково: HTTPS, флаги cookie, короткий срок жизни. Разница - где живёт состояние входа: у сессии на сервере, у JWT в самом токене.
| Сессия, stateful | JWT, stateless | |
|---|---|---|
| Где состояние | На сервере: память, Redis, БД | В токене у клиента |
| Что у клиента | Только session ID в cookie | Весь токен: в памяти или в cookie |
| Как сервер проверяет | Ищет сессию по ID в хранилище | Проверяет подпись и exp, хранилище не нужно |
| Несколько серверов | Нужно общее хранилище сессий, это сложнее | Каждый сервер проверяет токен сам |
| Данные пользователя | Не уходят клиенту | Payload читает любой, если токен не зашифрован |
| Сменились данные или роль | Сервер обновляет сессию сразу | Токен устарел, нужен новый через refresh |
| Выход и отзыв | Удалил сессию - доступа нет | Токен живёт до exp, нужен механизм отзыва |
| Истечение | Таймаут на сервере | Поле exp в токене |
| Размер | Короткий ID | Больше, растёт с каждым claim |
| Работа на запрос | Поиск сессии, данные и права уже в ней | Проверка подписи, права из токена или из БД |
| Память сервера | Растёт с числом пользователей | Не тратится |
Когда что:
- Сессии - приложение с централизованным хранением и сильным контролем на сервере, где важны мгновенный выход и отзыв доступа
- JWT - распределённые системы: много серверов и сервисов, каждый проверяет токен сам, без общего хранилища
OAuth¶
OAuth 2.0 - протокол делегированного доступа: приложение получает от провайдера (Google, GitHub) access token и действует от имени пользователя, не зная его пароля. «Войти через Google» - это OpenID Connect поверх OAuth 2.0 (см. Безопасность микросервисов).
Как подключить вход через провайдера:
- Завести проект в консоли разработчика провайдера, у Google это проект в Google Cloud Console. Service account для этого не нужен: он для доступа сервера к API без пользователя.
- Создать OAuth-клиента и указать redirect URI: адрес, куда провайдер вернёт пользователя после входа.
- Получить Client ID (публичный) и Client Secret (приватный, его показывают один раз при создании).
- Добавить тестовых пользователей или опубликовать приложение. Пока приложение в тестовом режиме, войти могут только добавленные пользователи.
Note
Шаги - из видео на YouTube.
SSO (Single Sign-On)¶
SSO - пользователь один раз проходит аутентификацию и потом получает доступ сразу к нескольким связанным системам без повторного ввода логина и пароля.
Вход выполняет один Identity Provider (Keycloak, Okta, Google), остальные системы доверяют его токенам. Протоколы - OpenID Connect поверх OAuth или SAML.
Модели авторизации¶
Права доступа после входа описывают одной из моделей:
- RBAC (Role-Based Access Control) - права привязаны к ролям, роли к пользователю
- ABAC (Attribute-Based Access Control) - решение по атрибутам пользователя, ресурса и среды
- ACL (Access Control List) - у каждого ресурса свой список: кто и что с ним может делать
См. Безопасность микросервисов.
Атаки¶
SQL-инъекции¶
XSS¶
XSS (Cross-Site Scripting) - атакующий внедряет свой скрипт в страницу сайта. Скрипт работает с правами
страницы и читает всё, что доступно JavaScript: localStorage, sessionStorage, cookie без HttpOnly. Так крадут
токены и session ID.
Защита от кражи токенов:
- session ID и refresh - в cookie с
HttpOnly - access - в памяти приложения, а не в localStorage
Память - не полная защита: пока страница открыта, скрипт может слать запросы от её имени, но унести токен с собой ему
сложнее. Саму XSS закрывают экранированием вывода и заголовком Content-Security-Policy.
CSRF¶
CSRF (Cross-Site Request Forgery) - чужой сайт заставляет браузер жертвы отправить запрос на твой сайт. Браузер сам прикладывает cookie, и запрос выполняется от имени вошедшего пользователя.
Защита:
- CSRF-токен - случайное значение в форме или заголовке запроса. Сервер сверяет его перед действием, чужой сайт его не знает.
SameSite=LaxилиStrictу cookie. Браузер не шлёт её с запросов чужих сайтов, сLax- кроме перехода по ссылке.- Запрос на refresh, если refresh лежит в cookie, тоже защищают CSRF-токеном в заголовке.
Токен в заголовке Authorization от CSRF не страдает: браузер не подставляет его сам.