Как работает аутентификация на сайтах: аналогия с банковской системой

Как работает аутентификация на сайтах: аналогия с банковской системой

Разбираем суть веб-аутентификации — JWT, сессии, cookie, CORS и XSS — с помощью простой банковской аналогии. Всё, что нужно знать фронтенду и бэкенду.

image

Существует странный ритуал, который веб-разработчики по всему миру соблюдают с зарождения компьютеров и по сей день. Этот ритуал — реализация аутентификации. Вы, возможно, уже не раз проходили через этот ритуал. Но действительно ли вы понимаете, что происходит? Эта статья сделает этот ритуал менее загадочным. Вы узнаете о токенах, авторизации, CORS, учетных данных, HTTP-заголовках и прочем. Чтобы реализовать хорошую систему аутентификации, не обязательно быть волшебником. Достаточно быть хорошим банкиром!

Пароль совпал. Что дальше?

Аутентификация — это процесс сопоставления запроса с уникальным субъектом, обычно человеком. Далее идет авторизация — процесс предоставления (или отказа в) доступе к ресурсу, будь то веб-страница или API-эндпоинт.

Я сосредоточусь на той части, которую сам долго не мог понять: что происходит после того, как пользователь отправил форму входа и сервер подтвердил корректность его учетных данных?

app.post("/login", async (request, response) => {
  const { body } = request;
  const user = await getUserByCredentials(body.username, body.password);
  if (user) {
    // А ЧТО ДАЛЬШЕ???
  }
});

Типичный эндпоинт входа. В этой статье пойдет речь о том, что происходит после этого.

Мы поговорим о токенах, CORS, cookies и заголовках. Эти концепции могут казаться абстрактными, но с банковской аналогией они станут более понятными.

Аутентификация держится на токенах, как банки — на деньгах

Есть два популярных подхода:

  • JWT (JSON Web Token) — открытый стандарт, ниже вы найдете ссылки для изучения подробнее.
  • Аутентификация на основе сессий — скорее шаблон, чем стандарт, реализация может отличаться, но мы рассмотрим суть.

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

Как бэкенд- или фуллстек-разработчик, вы сами выбираете подход, и вам тем более важно понимать, что вы делаете. Это ведь ваша работа, правда?

JWT — это как банкнота

При использовании JWT сервер выдает клиенту токен с зашифрованной информацией — так называемым «claim». При последующих запросах клиент передает этот токен. Сервер проверяет, что токен подлинный (т.е. был выдан этим сервером), а затем считывает полезную нагрузку, чтобы определить, кто вы — по ID пользователя, имени, email и т.п. На основе этих данных сервер разрешает или отказывает в доступе.

Представьте, что токен — это банкнота. Банкир (сервер) может напечатать новую банкноту (выдать токен). Деньги дают вам доступ к товарам и услугам. JWT — то же самое, только для защищенных сервисов. Если вы хотите использовать деньги, в банке или в магазине, сотрудники проверят, настоящая ли банкнота. Если настоящая — можно использовать.

Сложно отозвать токен: сервер должен вести черный список недействительных токенов, как банк ведет список украденных банкнот.

Аутентификация на сессиях — это как банковская карта

Для фронтендера главное отличие — в том, что сервер возвращает при успешной авторизации.

Сессионная аутентификация возвращает не токен с информацией, а простой session id. С точки зрения бэкенда, это требует хранения сессий в базе. Сессия — это объект, аналогичный JWT, содержащий ID пользователя, срок действия и т.д. Сервер контролирует аутентификацию: он может «разлогинить» пользователя, просто удалив сессию. JWT удалить невозможно, так как он не хранится на сервере.

Сессионная аутентификация — это как банковская карта. Банкир проверяет личность и выдает карту с истекающим сроком действия. Карта — это просто идентификатор, ее легко отозвать. При попытке «заплатить» (получить доступ к ресурсу) магазин связывается с банком, чтобы проверить карту и баланс. Аналогично — сервер проверяет session id.

Токены так же ценны, как банкноты и карты. Их нельзя терять!

Банки хранят деньги в стальных сейфах с сигнализацией и, возможно, драконами. Серверы тоже могут безопасно общаться между собой. А вот веб-сайты — как частные лица. Им приходится придумывать, как хранить токены безопасно.

Где вы храните свои деньги?

Что бы вы ни выбрали — JWT или session id — это ценности. В статье я буду называть их «токенами аутентификации», то есть тем, что вы показываете серверу, чтобы подтвердить свою личность. Чтобы каждый запрос к серверу сопровождался токеном, его нужно где-то хранить на клиенте. Мы рассмотрим именно случай веб-сайтов, где запросы отправляет браузер. Часто документация не уточняет, относится ли она к взаимодействию сервер-сервер или браузер-сервер. Второй случай сложнее и требует специальных подходов.

Итак, банкир выдал вам карту или пачку банкнот. Куда вы их спрячете? В подошву ботинка, в почтовый ящик, под матрас или в модную поясную сумку?

Не все запросы равны: загрузка веб-страницы против вызова API

Аутентифицированный контент делится на два типа:

  • Веб-страницы — возвращают HTML, вызываются автоматически при переходе по URL.
  • API-эндпоинты — возвращают JSON и другие данные, вызываются из JavaScript.

Два сценария:

  1. Вы хотите защитить саму веб-страницу, чтобы пользователь не видел даже её структуру без входа.
  2. Вы хотите защитить вызовы к API, чтобы пользователь мог видеть HTML, но не получить данные.

Можно комбинировать оба: неавторизованные пользователи не видят ни страницы, ни данных.

Сценарий 1: Защита веб-страницы с помощью cookie

Коротко: чтобы защитить доступ к HTML-странице, нужно использовать cookie, желательно HTTP-only и secure. Технически cookie — это данные, которые отправляются с каждым запросом к определенному домену. Это часть протокола HTTP. Когда сайт делит cookie на «обязательные» и остальные — как раз обязательные часто отвечают за аутентификацию.

Установить cookie можно как с клиента (через JavaScript), так и с сервера (через заголовок Set-Cookie). Именно сервер может установить HTTP-only cookie, которую нельзя прочитать из JavaScript. А secure cookie передается только через HTTPS.

Важно: HTTP-only cookie защищает от XSS (JavaScript-атаки), но не от CSRF (подделка запросов). Нужно применять комбинацию защитных мер.

При входе в систему сервер должен ответить заголовком Set-Cookie, и браузер автоматически сохранит токен.

Такой cookie — это как кошелек. В нем можно хранить и карточки лояльности, и деньги. Он у вас в сумке, недоступен посторонним (если вы не достаете его в опасных местах). Вы всегда готовы «заплатить» (подтвердить личность), когда нужно.

Сценарий 2: Защита API-вызовов. Вариант с cookie

API питают ваш сайт данными. Самый простой способ защитить вызовы — использовать cookie, как и с HTML. Но, в отличие от страниц, JavaScript-запросы требуют дополнительной настройки для передачи cookie. Если cookie HTTP-only, вы не можете прочитать его в JS и вставить в Authorization-заголовок. Вы должны передавать cookie напрямую.

Используя fetch, вы добавляете:

fetch("http://localhost:8000/account/signup", {
  method: "POST",
  body: JSON.stringify({ username: "foo", password: "barbar" }),
  credentials: "include"
});

Аналогичная настройка withCredentials есть в XMLHttpRequest. Кроме того, сервер должен разрешить отправку учетных данных, установив Access-Control-Allow-Credentials: true в ответе. Если этого не сделать — браузер проигнорирует Set-Cookie, и вы не поймете, почему все ломается. Проверьте credentials и CORS.

Банковская аналогия: вы всегда носите карту и банкноты в кошельке. Но для некоторых операций вам нужна предоплаченная карта, которую вы забыли взять. Это и есть отсутствие credentials: "include".

Сценарий 2: хранение токена в web storage

Для API-вызовов можно использовать web storage: localStorage или sessionStorage.

Они проще в использовании, так как доступны из JavaScript. Вы можете:

  1. Получить токен из ответа сервера,
  2. Сохранить его в localStorage,
  3. Добавить в заголовок Authorization:
headers: {
  "Authorization": `Bearer ${window.localStorage.getItem("auth_token")}`
}

Плюс: такой подход не уязвим для CSRF-атак, ведь токен не передается автоматически. Но он уязвим для XSS, т.к. токен можно украсть через вредоносный скрипт.

Если вы выбираете web storage — обязательно защищайтесь от XSS.

Особенности JWT: что в них должно быть и зачем нужен refresh token

В JWT не стоит хранить критически важную информацию. Даже если токен украдут и расшифруют — вред будет минимальным. JWT должны быть короткоживущими — 5–10 минут. Ведь их сложно отозвать. Чтобы не заставлять пользователя логиниться каждые 5 минут, применяют refresh-токен — долгоживущий, однократный токен, обычно хранящийся в cookie на ограниченном пути (/refresh).

Аналогия: чтобы снять деньги в банке, вы используете ID (refresh-токен), а не каждый раз получаете банкноту (JWT).

CORS — это как диалог магазина с банком

CORS (Cross-Origin Resource Sharing) — механизм, с помощью которого сервер указывает, какие источники могут обращаться к его API. Если браузер считает, что источник запроса недопустим, он даже не отправит запрос. Например, сайт https://www.foobar.com может обращаться только к API на https://api.foobar.com, если сервер это разрешает.

Для этого сервер должен в ответах указывать заголовки:

Access-Control-Allow-Origin: https://www.foobar.com
Access-Control-Allow-Credentials: true

Браузер очень вежлив: он не будет говорить с «грубыми» серверами (без CORS-заголовков) и не будет слать запросы, если чувствует, что не получит ответа.

Вежливый диалог: preflight-запросы

Если запрос считается «несложным» (например, обычный GET), preflight не нужен. Но при fetch с POST, Authorization или credentials — браузер сначала отправляет OPTIONS-запрос (preflight) на тот же URL. Сервер должен ответить заголовками Access-Control-Allow-*, иначе браузер заблокирует основной запрос.

Сравните: магазин принимает только карты Visa. Если вы пришли с MasterCard — кассир вежливо откажет.

SameSite: будет ли cookie отправлен при кросс-сайт запросе?

Атрибут SameSite управляет тем, когда cookie будет отправляться. Обычно стоит значение Lax.

Примеры:

  • Strict: cookie передаются только при навигации внутри сайта.
  • Lax: cookie передаются и при переходе с другого сайта (через ссылку).
  • None: cookie всегда передаются (если Secure тоже установлен).

Если настроено неправильно — сервер не получит cookie, и вы будете считаться неавторизованным.

Sec-Fetch-заголовки: интересный, но нестабильный способ определения источника запроса

Браузеры могут добавлять заголовки Sec-Fetch-*, которые помогают серверу понять контекст запроса.

Например: Sec-Fetch-Site: same-origin — можно не проверять Origin.

Но не все браузеры поддерживают эти заголовки (Safari — нет), так что это вспомогательная мера.

CORS касается только браузеров

Сервер-сервер запросы не подчиняются CORS. Кто-то может создать API-прокси, оборачивающий ваш API. Но запускать сервер стоит денег и оставляет следы. Поэтому злоумышленники предпочитают использовать браузеры жертвы — и вот тут помогает CORS.

Не путайте доступ к веб-странице с API-вызовом

Это две разные ситуации:

  • Доступ к HTML через URL — должен использовать cookie.
  • Вызов API через JavaScript — можно использовать Authorization-заголовок.

Если вы храните токен в storage, вы не сможете защитить саму HTML-страницу, только API.

Web Storage = почтовый ящик

localStorage или sessionStorage — это как хранить деньги в почтовом ящике. Если кто-то вскроет ящик (XSS), он украдет токен.

JWT в заголовке — только для API

Если вы храните токен в storage, то вставляете его в заголовок:

Authorization: Bearer <token>

Но это не работает для HTML, так как браузер не вставит заголовок автоматически при переходе по ссылке. Для страниц лучше использовать cookie.

Basic Auth

Basic Auth — устаревший и небезопасный способ аутентификации. Имя и пароль передаются с каждым запросом.

Иногда подходит для временных демо-страниц. Браузер сам добавит Authorization-заголовок при навигации. Вот почему в fetch и XHR используется общий параметр credentials, а не cookies.

Заключение

Вы дочитали? Поздравляю — теперь вы почти банковский аудитор по безопасности!

Вот краткий итог:

  • JWT — как банкнота, sessionId — как кредитка. Любой токен нужно хранить надежно.
  • HTTP-only secure cookie — безопасное место в браузере. Но если вы выбираете localStorage, защищайтесь от XSS.
  • Cookies отправляются автоматически, если правильно задать path, SameSite, Secure.
  • Для JavaScript-запросов (fetch, XHR) укажите credentials: "include", и не забудьте заголовок Access-Control-Allow-Credentials.
  • CORS — это вежливый способ общения между сайтами и API. Без нужных заголовков запросы просто не работают.
  • Не путайте API и HTML-доступ. Cookie — для страниц, заголовки — для API.
  • Basic Auth — исключение, использовать осторожно.
  • BFF (backend-for-frontend) — способ обойти ограничения API, когда нельзя изменить его реализацию.

Твой код — безопасный?

Расскажи, что знаешь о DevSecOps.
Пройди опрос и получи свежий отчет State of DevOps Russia 2025.