- Аутентификация в веб-приложениях
- Api Keys
- JWT Token
- Сравнение API-ключей и JWT-токенов
- Когда использовать каждый метод
В обычных веб-приложениях, работающих в браузере, аутентификация обычно осуществляется с помощью cookies, в которых хранится сессионный ключ. Браузер автоматически отправляет эти cookies при загрузке страниц, поддерживая состояние аутентификации пользователя на разных страницах сайта. Однако в API, особенно RESTful API, такие механизмы отсутствуют, поэтому аутентификация там работает немного иначе. В этом уроке мы подробно рассмотрим, как реализовать аутентификацию с помощью API-ключей и JWT-токенов — двух распространенных методов для обеспечения безопасности API.
Аутентификация в веб-приложениях
Прежде чем перейти к аутентификации в API, кратко напомним, как работает аутентификация в традиционных веб-приложениях. Когда пользователь входит в систему, сервер создает сессию и отправляет идентификатор сессии обратно в браузер, обычно храня его в cookie. При последующих запросах браузер отправляет этот cookie обратно серверу, позволяя серверу распознать пользователя и получить данные его сессии.
Этот механизм опирается на автоматическую обработку cookies браузером и способность сервера поддерживать состояние сессии. Хотя это хорошо работает для веб-приложений, это не переносится напрямую на API, особенно когда клиентами являются не браузеры, а другие сервисы или мобильные приложения.
Api Keys
Этот метод аутентификации в API предполагает, что каждое приложение или клиент получает уникальный ключ, который используется для доступа к API. API-ключ обычно выдается разработчику при регистрации приложения в сервисе, предоставляющем API.
API-ключ — это уникальный идентификатор, назначаемый клиенту, который позволяет серверу распознать клиента, делающего запрос. Клиент включает API-ключ в каждый запрос к API, обычно через заголовок или как параметр запроса. Сервер затем валидирует API-ключ, сверяя его со своими записями, чтобы аутентифицировать клиента.
Процесс аутентификации с API-ключами
- Запрос доступа клиентом: Клиент запрашивает API-ключ у поставщика сервиса.
- Выдача API-ключа сервисом: Сервис генерирует уникальный API-ключ и предоставляет его клиенту.
- Запросы клиента к API: Клиент включает API-ключ в каждый запрос к API. Обычно в виде заголовка. У такого заголовка нет стандартного имени и каждый сервис придумывает что-то свое, например,
X-API-KEY
. - Валидация API-ключа сервером: Сервер проверяет API-ключ, сверяя его со своими записями для аутентификации клиента.
- Ответ сервера: Если API-ключ валиден, сервер обрабатывает запрос и отправляет ответ.
Пример того, как может выглядеть API-запрос с API-ключом в заголовке:
GET /resource HTTP/1.1
Host: api.hexlet.io
X-API-KEY: <api-key>
Многие сервисы позволяют генерировать несколько API-ключей для разных целей, повышая безопасность путем ограничения области действия каждого ключа. Например, вы можете иметь отдельные ключи для разработки, тестирования и продакшн-среды. Продвинутые системы позволяют связывать ключи с определенными правами доступа, контролируя доступ к разным частям API.
На практике API-ключ должен храниться в секрете, как пароль. Если кто-то получит доступ к вашему API-ключу, он сможет делать запросы к API от вашего имени.
Какие проблемы не решают API-ключи
- Проблемы масштабируемости: Серверу необходимо обращаться к базе данных для проверки API-ключа при каждом запросе, что может стать узким местом в системах с высокой нагрузкой или в микросервисной архитектуре.
- Отсутствие контекста пользователя: API-ключи обычно идентифицируют приложение, а не пользователя, что затрудняет выполнение действий от имени конкретных пользователей, если это подразумевается в системе.
- Ограниченная безопасность: API-ключи не предоставляют детального контроля доступа или аутентификации на уровне пользователя.
- Сложность управления: Ротация API-ключей и управление их истечением требует дополнительного администрирования.
Из-за этих ограничений, особенно в системах, требующих аутентификации конкретных пользователей или распределенных архитектур, JWT-токены часто являются лучшим выбором.
JWT Token
JWT (JSON Web Token) — это компактный и URL-безопасный способ передачи информации между сторонами в виде JSON-объекта. Они предназначены для передачи информации безопасно и целостно, поскольку могут быть подписаны или зашифрованы.
Структура JWT
JWT-токен состоит из трех частей, разделенных точками:
- Header (заголовок): содержит метаданные о типе токена и алгоритме подписи.
- Payload (полезная нагрузка): содержит утверждения (claims) или данные, которые вы хотите передать.
- Signature (подпись): используется для проверки целостности токена.
# Компоновка. Каждая часть кодируется с помощью Base64URL.
xxxxx.yyyyy.zzzzz
# Конкретный пример
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNzI2MjQ5NjIzfQ.pASCTRgva-EyeCwJYrVWUdxdG2PPb8tRH885A_PxaUg
Как работают JWT-токены
JWT-токены позволяют встраивать информацию о пользователе непосредственно в сам токен. Это означает, что серверу не нужно запрашивать базу данных для получения информации о пользователе при каждом запросе, так как он может извлечь данные из токена.
Для получения jwt-токена, клиент должен выполнить аутентификацию через специальный эндпоинт, который вернет токен. Затем, при выполнении запросов к защищенным маршрутам клиент должен включать JWT-токен в каждый запрос. Обычно это делается через заголовок Authorization с использованием схемы Bearer:
GET /users/1 HTTP/1.1
Host: api.example.com
Authorization: Bearer <your-jwt-token>
Когда токен истекает, клиент должен получить новый токен, обычно повторно аутентифицируясь или используя механизм обновления токена.
Преимущества JWT-токенов
JWT-токен это строка, внешне похожая на API-ключ, но в отличие от ключа, в нее зашифрована полезная информация, например, идентификатор клиента и набор допустимых действий в системе. Благодаря этому JWT позволяет избежать необходимости хранить сессии на сервере и обеспечивает гибкость в распределенных системах, где API могут быть вызваны из различных источников.
- Безсессионность: Нет необходимости хранить данные сессии на сервере.
- Масштабируемость: Сокращает обращения к базе данных, улучшая производительность в распределенных системах.
- Гибкость: Можно включать различные утверждения и устанавливать истечение срока действия токена.
Использование JWT-токенов в Fastify
Работа с JWT-токенами в Fastify автоматизирована благодаря наличию готовой интеграции. Поэтому нам не придется вникать во все тонкости связанные с созданием, расшифровкой и передачей токенов по HTTP. Вы всегда сможете это сделать при желании, а здесь мы сосредоточимся на том, как все подключить и настроить для работы. Начнем с установки пакета:
npm install @fastify/jwt
Далее зарегистрируем плагин и настроим его в вашем приложении. Создайте файл plugins/jwt.js:
import fp from 'fastify-plugin'
import jwtPlugin from '@fastify/jwt'
export default fp(async (fastify) => {
fastify.register(jwtPlugin, {
// секрет используемый для шифрования
// правильно передавать через переменные окружения
// https://github.com/fastify/fastify-env
secret: 'supersecret',
})
fastify.decorate('authenticate', async function (request, reply) {
try {
await request.jwtVerify()
}
catch (err) {
reply.send(err)
}
})
})
Во время настройки создается метод-декоратор authenticate()
, который валидирует и расшифровывает jwt-токен. Этот метод автоматически выполняет аутентификацию и извлекает данные из токена помещая их в объект запроса.
Если нам нужна только аутентификация, то достаточно передать этот метод в хук onRequest
fastify.get(
'/users/:id',
{
onRequest: [fastify.authenticate],
},
async (request) => {
const user = await db.query.users.findFirst({
where: eq(schemas.users.id, request.params.id),
})
fastify.assert(user, 404)
return user
},
)
Если нужны данные, то после аутентификации они доступны в request.user
fastify.post(
'/courses',
{
onRequest: [fastify.authenticate],
},
async (request, reply) => {
const data = request.body
// Данные пользователя извлеченные из jwt-токена
body.creatorId = request.user.id
const [course] = await db.insert(schemas.courses)
.values(body)
.returning()
return reply.code(201)
.send(course)
},
И последнее, для работы с jwt-токенами понадобится эндпоинт выдачи токенов. Обычно это происходит после успешной авторизации. В примере ниже для этого создается адрес /tokens внутри которого проверяется пользователь, запросивший токен. Затем формируется сам токен с помощью метода fastify.jwt.sign
в который передается объект с данными для шифрования. Именно эти данные, потом оказываются внутри request.user
.
import { schema } from '../../schema.js'
export default async function (fastify) {
const db = fastify.db
fastify.post(
'/tokens',
async (request, reply) => {
const client = await db.query.users.findFirst({
// Добавить проверку пароля
where: eq(schemas.users.email, request.body.email),
})
fastify.assert.ok(client, 404)
const token = fastify.jwt.sign(
{ id: user.id, email: user.email },
{ expiresIn: '1h' }, // время протухания
)
return reply.code(201)
.send({ token })
},
)
}
Пример того, как это работает:
# Данные взяты из сидов
curl -XPOST localhost:3000/api/tokens \
-H "Content-Type: application/json" \
-d '{ "email": "support@hexlet.io", "password": "some secret password" }'
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiaWF0IjoxNzI2MjQ5NjIzfQ.pASCTRgva-EyeCwJYrVWUdxdG2PPb8tRH885A_PxaUg"}
Вопросы безопасности
- Управление секретом: Секретный ключ для подписания токенов должен храниться в безопасности. Не хардкодьте его; используйте переменные окружения.
- Хранение токена: Клиенты должны безопасно хранить токены, особенно в браузерных приложениях, где токены могут быть уязвимы для XSS-атак.
- HTTPS: Всегда используйте HTTPS для предотвращения перехвата токена через атаки типа "человек посередине".
Сравнение API-ключей и JWT-токенов
Характеристика | API-ключи | JWT-токены |
---|---|---|
Идентифицируют | Приложение/Клиента | Пользователя/Клиента |
Сессионность | Сессионные (требуют проверки в БД) | Безсессионные (не требуют проверки в БД) |
Масштабируемость | Менее масштабируемы | Более масштабируемы |
Безопасность | Менее безопасны | Более безопасны (подписанные токены) |
Контроль доступа | Ограниченный | Гибкий (на основе утверждений) |
Истечение срока действия | Ручное | Встроенное |
Когда использовать каждый метод
- API-ключи: Подходят для простых приложений, где нужно идентифицировать клиента, но не отдельных пользователей, или когда API потребляется серверными приложениями.
- JWT-токены: Идеальны, когда требуется аутентификация на уровне пользователя, безсессионность и масштабируемость, например, в микросервисной архитектуре или при разработке API для мобильных приложений.
Самостоятельная работа
- Добавьте аутентификацию с JWT в приложение. Добавьте во все роуты аутентификацию
- Добавьте маршрут POST /tokens для генерации токена, как описано в уроке
- Запушьте изменения в репозиторий
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.