Bearer-токен — стандартный способ авторизации в современных API. По принципу это тот же «браслет» из главы 7.1, только с двумя важными отличиями от обычного API key: токен выдаёт сервер на каждый сеанс, и у него есть срок жизни.

Как выглядит заголовок

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJBbm5hIiwiZXhwIjoxNzM5ODcwMDAwfQ.X1Ny...

Слово Bearer — это схема авторизации. По смыслу — «кто принёс этот токен (bearer), тот и владелец». Сам токен — всё, что после слова Bearer. Сервер берёт этот кусок и проверяет.

В fetch выглядит так:

const response = await fetch('/api/posts', {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
  },
});

JWT — самый частый формат Bearer-токена

Сам по себе токен может быть чем угодно — случайной строкой, идентификатором сессии, шифрованной нагрузкой. На практике стандартом стал JWTJSON Web Token.

JWT — это три куска, разделённых точками:

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIxMjMiLCJleHAiOjE3M30 . X1Ny...
   ↑                       ↑                                  ↑
header                  payload                            signature

Каждый из трёх — base64url-кодированный текст (это просто переупакованная строка, не шифрование).

Header — маленький JSON с указанием алгоритма подписи:

{"alg": "HS256", "typ": "JWT"}

Payload — JSON с данными о пользователе и сроке жизни. Самые частые поля (claims):

{
  "sub":   "12345",                  // subject — id пользователя
  "name":  "Anna",
  "email": "anna@example.com",
  "role":  "admin",
  "iat":   1739864400,               // issued at — UNIX-время выдачи
  "exp":   1739868000                // expiration — UNIX-время истечения
}

Signature — криптографическая подпись первых двух частей секретным ключом, который знает только сервер. Если кто-то подменит header или payload — подпись не сойдётся, сервер отвергнет токен.

Главный сюрприз для новичка: JWT не зашифрован

Payload — обычный base64. Любой, у кого в руках токен, может его декодировать и прочитать всё, что внутри. Откройте jwt.io и вставьте любой свой JWT — сразу увидите содержимое.

Защищена только целостность: подменить payload незаметно нельзя (подпись отвалится). А прочитать — можно. Поэтому в JWT не кладут пароли, номера карт и прочее, что не должно быть видно.

Прочитать payload на фронте бывает полезно — например, чтобы достать имя пользователя или его роль без лишнего запроса:

function decodeJwt(token) {
  const [, payload] = token.split('.');
  return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
}

const claims = decodeJwt(accessToken);
console.log(claims.name);   // "Anna"
console.log(claims.exp);    // 1739868000

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

Срок жизни и refresh

JWT обычно живёт недолго — от 5 минут до 1 часа. Зачем так мало? Если токен утечёт, окно атаки маленькое — через час он сам перестанет работать.

Но просить пользователя логиниться каждый час неудобно. Поэтому при логине обычно выдают два токена:

  • access token — короткоживущий JWT, ходит в каждом запросе;
  • refresh token — долгоживущий (дни, недели), хранится отдельно, используется только для одного: обменять на новый access.

Когда access протух (сервер вернул 401), клиент идёт по refresh:

POST /api/auth/refresh
Cookie: refreshToken=fF8a2d...

→ {"access_token": "новый-jwt..."}

Получили новый access — повторили исходный запрос. Пользователь ничего не заметил.

Типичный паттерн перехвата 401

async function api(url, options = {}) {
  let response = await fetch(url, {
    ...options,
    headers: { ...options.headers, 'Authorization': `Bearer ${accessToken}` },
  });

  if (response.status === 401) {
    // Access протух — пробуем refresh:
    accessToken = await refreshAccessToken();
    // Повторяем исходный запрос с новым токеном:
    response = await fetch(url, {
      ...options,
      headers: { ...options.headers, 'Authorization': `Bearer ${accessToken}` },
    });
  }

  if (!response.ok) throw new ApiError(response.status);
  return response.json();
}

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

Краткая шпаргалка

  • Bearer-токен в заголовке Authorization: Bearer ....
  • JWT — три части через точки, base64, не зашифрованы (только подписаны).
  • Payload можно прочитать — нельзя подменить незаметно.
  • Access живёт минуты-час; refresh — дни-недели.
  • На 401 пробуем refresh и повторяем запрос; если refresh упал — на логин.

Куда лучше класть токены — в куки или в localStorage — разбирается в последней главе модуля.