JWT и проверка прав пользователя
Проверка пароля подтверждает один запрос входа. Для последующих операций DeskQueue нужен способ передать доказательство личности без повторной отправки пароля. Используем короткий JWT access-токен и библиотеку jose семейства 6. После проверки подписи сервер загрузит актуального пользователя из базы и решит, разрешено ли конкретное действие. Подлинность токена и право на заявку — разные вопросы.
В stages/17 добавлены tokens.js, middleware requireUser и ограничения адресов заявок. Публичными остаются регистрация, вход и проверка процесса. Просмотр заявок требует входа. Пользователь requester видит свои записи, сотрудник agent видит очередь и меняет состояния. Старые учебные строки без автора доступны сотруднику; мы не присваиваем их случайному зарегистрированному пользователю.
Подписанный набор утверждений
JWT содержит заголовок, набор утверждений и подпись. Обычный подписанный JWT не скрывает содержимое: клиент способен прочитать поля. Поэтому не помещаем туда пароль, производную, почтовую переписку или внутреннюю конфигурацию. Наш набор содержит идентификатор пользователя в sub, издателя iss, получателя aud и срок exp.
Полный модуль tokens.js использует HS256. Секрет получает приложение через JWT_SECRET; читатель создаёт случайные 32 байта и сохраняет их в конфигурации как base64url. Декодированный ключ должен иметь именно 32 байта. Строка «очень длинный пароль администратора» не является равноценной заменой случайного ключа.
import { SignJWT, jwtVerify } from 'jose';
const key = Buffer.from(process.env.JWT_SECRET ?? '', 'base64url');
if (key.length !== 32) throw new Error('JWT_SECRET must encode 32 random bytes');
export async function accessToken(user) {
return new SignJWT({})
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setSubject(String(user.id))
.setIssuer('deskqueue')
.setAudience('deskqueue-api')
.setIssuedAt()
.setExpirationTime('10m')
.sign(key);
}
export async function readAccess(token) {
const { payload } = await jwtVerify(token, key, {
algorithms: ['HS256'], issuer: 'deskqueue', audience: 'deskqueue-api'
});
return payload;
}
Проверка разрешает только выбранный алгоритм и ожидаемые стороны. Нельзя ограничиться декодированием base64: оно показывает поля, но не подтверждает подпись. Нельзя также принимать произвольный алгоритм из заголовка как команду серверу. Возможности подписи и проверки описаны в первичной документации jose.
Короткий срок уменьшает время использования украденного токена, но не отзывает его мгновенно. На этой ступени окончание срока является основной границей access-токена. В следующей главе появится семья refresh-токенов и дополнительная серверная проверка её активности. Такой переход явно меняет договор, а не делает вид, что JWT всегда обладает одинаковыми свойствами.
Личность в контексте запроса
Клиент передаёт Authorization: Bearer <token>. Middleware требует ровно этот формат, проверяет токен и числовой sub, затем читает пользователя из users. Роль берём из актуальной строки базы, а не доверяем присланному JSON или устаревшему утверждению. Таким образом, изменение роли не требует ждать нового входа для отражения в разрешении следующего запроса.
Если токен отсутствует, повреждён или просрочен, ожидается 401. Если личность подтверждена, но роль не разрешает изменение состояния, ожидается 403. Для requester, который пытается прочитать чужой идентификатор, выбираем 404: API не подтверждает существование этой заявки. Это осознанная политика ресурса; она должна применяться одинаково к чтению и будущим вложениям.
Ошибку базы во время загрузки пользователя нельзя превращать в неверный токен. Проверка криптографии и SQL выполняются с разными границами try/catch. Если хранилище недоступно, общий обработчик вернёт внутренний сбой. Клиент не должен многократно вводить пароль, пытаясь исправить инфраструктурную ошибку. Такая деталь сохраняет смысл кодов ответа.
Разрешение операции
Создание заявки получает author_id из req.user.id. Поле author_id тела запроса игнорируется. Иначе пользователь мог бы создать обращение от имени другого. Для списка requester получает SQL-условие своего идентификатора, а agent — общий список. Ограничение применяется до пагинации: нельзя сначала получить чужие строки, затем отфильтровать страницу в JavaScript и сохранить неправильный курсор.
Изменение состояния доступно только роли agent и вызывает уже знакомую транзакцию с actorId. Таблица истории теперь связывает переход с подтверждённым пользователем. Middleware роли сокращает повторение, но окончательный вызов всё равно получает действующего пользователя из серверного контекста. Номер сотрудника из тела запроса не участвует в аудите.
Внешний вид клиента не является границей прав. Скрытая кнопка изменения помогает пользователю, однако произвольный HTTP-клиент способен отправить PATCH напрямую. Поэтому разрешение находится на сервере перед операцией. Для новых маршрутов нужно проверить, что они подключены после requireUser и имеют правильное условие роли либо владения.
Хранение токена клиентом
Наш пример предназначен для учебного HTTP-клиента и возвращает access-токен в JSON. Он не описывает полный браузерный интерфейс. Если добавить cookies, потребуется отдельный договор атрибутов, источников запроса и защиты от CSRF. Если добавить хранение в браузерном JavaScript, потребуется оценить риск кражи при XSS. Нельзя обещать одинаковую безопасность всех способов хранения одной и той же строки.
Токен не записывается в журнал. Идентификатор запроса по-прежнему полезен для диагностики, но содержимое Authorization остаётся закрытым. При настоящем сетевом размещении нужны HTTPS и политика ключей. Утечка общего HS256-секрета позволяет подписывать произвольные токены, поэтому смена ключа является значимым изменением конфигурации, а не косметическим обновлением.
Результат ступени — защищённые адреса с явной разницей личности, роли и владения. Ожидаемое наблюдение: requester создаёт и читает свою заявку, чужой ресурс не раскрывается, изменение состояния разрешено сотруднику. Следующий шаг позволит обновлять короткий токен и отзывать семью при повторном использовании старого refresh-токена.
Секрет HS256 даёт возможность и проверять, и подписывать токены. Если несколько независимых сервисов получили такой ключ ради проверки, каждый из них также способен выпускать утверждения. Для другой архитектуры выбирают асимметричную подпись: проверяющим достаточно публичного ключа, а закрытый остаётся у издателя. Наш небольшой сервис имеет одного владельца ключа, поэтому выбранный пример сохраняет ясную границу; он не является универсальной рекомендацией для всех распределённых систем.
Поле sub содержит идентичность, а не отображаемое имя. Изменение email пользователя не должно менять ключ его заявок и истории. Поэтому токен ссылается на стабильный номер, а актуальные профиль и роль читаются из базы. Если пользователь удалён или недоступен согласно политике приложения, прежняя подпись не обязует сервер продолжать разрешать операции.
Ограничение списка выполняется запросом, но одиночное чтение тоже требует проверки. Защита только первой страницы создала бы возможность угадать чужой номер и обратиться напрямую. Когда добавляем вложение, проверка распространяется на его получение таким же способом. Права — это свойство каждой операции над ресурсом, а не одна удачная проверка входа в начале интерфейса.
Срок токена зависит от согласованных часов участников. Сервер проверяет время при верификации, а клиент может заранее начинать обновление, чтобы не ждать отказа операции. Небольшие допустимые расхождения требуют осмысленной политики; большой произвольный запас превращает короткий срок в другой договор. В учебном состоянии дополнительный запас не задан. Если часы стенда неверны, исправляйте окружение, а не отключайте проверку exp для всех запросов.
Полные файлы учебного проекта. Примеры при подготовке текста не запускались; ответы и вывод в уроке описывают ожидаемый результат читателя.