Перейти к содержанию

Guards и серверное право изменения

API принимает корректный JSON, но это ещё не разрешение изменять базу. Viewer должен читать список, а dispatcher также выполнять PATCH. Добавим два guard: DemoAuthGuard определяет учебного actor по Bearer token, DispatcherGuard проверяет роль операции записи. Результат главы — серверная граница, которую нельзя заменить скрытой кнопкой Angular. Одновременно мы назовём ограничения публичных токенов, чтобы вы не приняли удобную лабораторию за безопасную систему входа.

Authentication отвечает, кого сервер признаёт стороной запроса; authorization — разрешено ли этой стороне конкретное действие. В лаборатории эти вопросы упрощены до двух token-значений из config. Они известны читателю и доступны в Angular, поэтому не являются секретами. Nest слушает loopback, а данные вымышленные. Для реального продукта потребуются настоящая сессия или IdP, срок действия, отзыв и права на предметные объекты. Текущие guards учат направлению проверки, а не полной модели безопасности.

Определение actor

Ниже полный auth.ts. ActorRequest расширяет Express Request полями actor и requestId, которые устанавливаются серверной цепочкой. DemoAuthGuard извлекает Authorization с префиксом Bearer и сравнивает token с проверенной конфигурацией. Результат — фиксированный demo-id и role. DispatcherGuard затем читает этот actor. Тип Request импортируется только как TypeScript-тип, потому что он не нужен для runtime injection и не представляет DTO-класс внешнего JSON.

import { CanActivate, ExecutionContext, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type { Request } from 'express';
import type { Actor } from './model.js';
export type ActorRequest = Request & {actor?: Actor; requestId?: string};
@Injectable()
export class DemoAuthGuard implements CanActivate {
  constructor(private readonly config: ConfigService) {}
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest<ActorRequest>();
    const header = req.headers.authorization;
    const token = header?.startsWith('Bearer ') ? header.slice(7) : '';
    if (token === this.config.getOrThrow<string>('DEMO_DISPATCHER_TOKEN')) req.actor = {id:'demo-dispatcher',role:'dispatcher'};
    else if (token === this.config.getOrThrow<string>('DEMO_VIEWER_TOKEN')) req.actor = {id:'demo-viewer',role:'viewer'};
    else throw new UnauthorizedException('Нужен учебный токен');
    return true;
  }
}
@Injectable()
export class DispatcherGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest<ActorRequest>();
    if (req.actor?.role !== 'dispatcher') throw new ForbiddenException('Изменение доступно диспетчеру');
    return true;
  }
}

Ожидаемый viewer token разрешает GET, но PATCH получает 403. Неподходящий token даёт 401 раньше controller. Dispatcher token разрешает проход к DTO и предметной операции, но не гарантирует 200: неверные поля, отсутствующий id и устаревшая version всё ещё могут дать 400, 404 или 409. Такой порядок делает отказ объяснимым. Право на действие является одной необходимой границей, а не обещанием любого результата после успешного guard.

Actor.id берётся из установленного token-сценария. JSON body не содержит actorId, а whitelist отклонит попытку его добавить. Поэтому ticket_events связывается с серверным actor, а не с произвольной строкой, присланной редактором. В настоящей системе id пришёл бы из проверенной сессии пользователя. Даже тогда роль и доступ к конкретной заявке потребуют предметного правила: удостоверенная личность не обязана иметь право читать или изменять любую запись только благодаря существованию аккаунта.

Регистрация и порядок

TicketsController имеет class-level DemoAuthGuard для всех методов. Patch имеет дополнительный DispatcherGuard. Ожидается, что удостоверение устанавливается до проверки роли. Если поставить только DispatcherGuard на незащищённый маршрут, actor будет отсутствовать и запрос получит 403, не различая неизвестную сторону и наблюдателя. Это ещё одно основание явно читать граф guards, а не считать необязательное поле actor автоматически заполненным в любом месте server-кода.

Guards объявлены providers TicketsModule. DemoAuthGuard нуждается в ConfigService, поэтому контейнер создаёт его с этой зависимостью. Не передавайте в UseGuards произвольно созданный объект без понимания DI. Для текущей формы используем классы, а module registration удерживает общий профиль создания. Во время выполнения guard получает ExecutionContext и переключается на HTTP request. Курс не добавляет GraphQL или WebSocket, для которых тот же доступ к контексту пришлось бы рассматривать отдельно.

CanActivate возвращает boolean после установки actor либо выбрасывает подходящее исключение. Возвращать просто false тоже возможно в отдельных схемах, но явные UnauthorizedException и ForbiddenException дают определённые статусы и понятное назначение. ApiErrors превращает их в общий envelope. Не следует возвращать объект {error:...} из guard как будто это нормальный JSON-успех: такой объект не является результатом controller и не описывает правильно отказ допуска к операции.

Предметная область доступа

В лаборатории один общий набор заявок. Role dispatcher разрешает изменение любого существующего rq-id. Здесь нет владельца, tenant или назначения сотруднику. Если следующий проект разделит данные по организациям, потребуется проверять tenant в запросах repository и правилах service. Клиентский фильтр списка не защищает прямой GET чужого id. Сервер должен применять ограничение к каждой операции, иначе скрытые строки останутся доступны внешнему HTTP-клиенту.

Нельзя сохранять actor в общем mutable-поле singleton service перед await. Guards устанавливают его в конкретном request, controller передаёт параметр service, а repository использует id в данной транзакции. Такое движение не смешивает одновременно обрабатываемые запросы. Модель понятна без request-scoped providers: временный контекст находится в аргументах. Request scope мог бы понадобиться для другой архитектуры, но его наличие само не определяет, какие предметные права нужно проверять.

Конфигурация требует разных tokens длиной минимум 12. Если значения совпадают, одна строка могла бы одновременно обозначать две роли, а порядок if решил бы доступ случайно. ValidateConfig останавливает такой профиль заранее. Это проверка целостности учебной настройки, а не криптографическая оценка надёжности token. В README значения прямо названы публичными, а production использование исключено. Не делайте вывод о безопасном secret только из длины и размещения в .env.

Для самостоятельного воспроизведения составьте таблицу token/action. Неверный token плюс GET ожидает 401; viewer плюс GET — 200 при готовой базе; viewer плюс PATCH — 403; dispatcher плюс корректный PATCH текущей версии — 200. Последняя строка зависит также от SQL и состояния, поэтому её нельзя проверить одним guard. Никакие запросы в сессии не отправлялись; таблица задаёт план будущего наблюдения и помогает локализовать отказ на нужной границе.

Следующий шаг добавит requestId и trace через interceptor. Он не станет ещё одним механизмом выдачи роли: идентификатор операции служит диагностике, а actor — доступу и авторству записи. Разделение этих контекстов сохраняет точный смысл журнала. Удобно понимать, кто выполнил изменение и какой запрос отказал, но совпадение полей requestId и actorId не заменяет проверку ни одного из них.

Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.

Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.