NestJS / 38 минут

Метаданные и свои декораторы

Свяжите метаданные маршрута с Reflector и проверкой доступа.

В процессе Теория: впереди Практика: впереди Тест: впереди
08 / 14
После этой главы
  • Свяжите метаданные маршрута с Reflector и проверкой доступа.
  • Применить тему в задаче «Метаданные и свои декораторы» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Описание поведения и его исполнение

Декораторы делают намерение маршрута заметным рядом с методом: путь, роль, способ извлечения параметра. В Nest многие из них записывают метаданные, которые framework прочитает позже. SetMetadata не выполняет проверку доступа. Если ни один guard не читает соответствующий ключ, правило остаётся лишь описанием.

Nest использует систему TypeScript-декораторов, поддерживаемую его инструментами и настройками проекта. Не смешивайте произвольно примеры стандартных новых декораторов JavaScript с legacy experimentalDecorators: сигнатуры и работа метаданных отличаются. Начинайте проект официальным CLI и сохраняйте совместимую конфигурацию компиляции и reflect-metadata.

TypeScript
export const ROLE = 'required-role';
export const RequireRole = (role: string) => SetMetadata(ROLE, role);

Один ключ для записи и чтения

Ключ метаданных должен быть общим для декоратора и его потребителя. Опечатка в повторённой строке может незаметно отключить правило. Вынесенная константа или типизированный декоратор уменьшает этот риск. Symbol тоже подходит как runtime-значение, если экспортируется один и тот же экземпляр, а не создаётся заново в каждом файле.

TypeScript
@Controller('admin')
class AdminController {
  @Get()
  @RequireRole('admin')
  @UseGuards(RoleGuard)
  index() { return { ok: true }; }
}

RoleGuard должен быть зарегистрирован и реально читать ROLE. Interface пригоден для описания формы метаданных, но не как самостоятельный runtime-токен: после компиляции его нет. Вместо универсального объекта any лучше задавать узкий тип допустимых ролей, чтобы редактор обнаруживал ошибочные значения до запуска.

Reflector связывает правило с запросом

Reflector предоставляет API чтения метаданных. ExecutionContext сообщает, какой метод и класс сейчас обрабатывают запрос. get читает конкретную цель. getAllAndOverride обходит цели в заданном порядке и выбирает первое определённое значение. Метод обычно имеет приоритет над классом, если именно такой контракт вы решили поддержать.

TypeScript
@Injectable()
class RoleGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(context: ExecutionContext) {
    const role = this.reflector.getAllAndOverride<string>(ROLE, [
      context.getHandler(), context.getClass(),
    ]);
    const req = context.switchToHttp().getRequest<{ user?: { role: string } }>();
    return role === undefined || req.user?.role === role;
  }
}

Пример считает маршрут без требования роли доступным на этой стадии. Это явный выбор политики, а не универсальное правило безопасности. Общая authentication-проверка может быть обязательной независимо от role. Для deny-by-default политики отсутствие метаданных обрабатывают иначе. Зафиксируйте оба случая тестами, чтобы добавленный маршрут не оказался публичным случайно.

Параметр из доверенного контекста

createParamDecorator позволяет вынести повторяющееся извлечение параметра. Например, CurrentUser читает req.user после успешного SessionGuard. Такой декоратор улучшает читаемость контроллера, но не подтверждает сессию сам. Тип аргумента и красивое имя не заменяют предыдущую проверку credential.

TypeScript
const CurrentUser = createParamDecorator(
  (_data: unknown, context: ExecutionContext) =>
    context.switchToHttp().getRequest<{ user: { id: string } }>().user,
);

Если пользователь отсутствует, заранее определите поведение: обязательный guard должен остановить запрос либо декоратор возвращает допускаемый nullable результат. Не подставляйте демонстрационного пользователя для удобства разработки. При переходе на WebSocket или другой транспорт HTTP-specific извлечение нужно пересмотреть, хотя метаданные роли могут остаться общими.

Композиция без скрытых зависимостей

applyDecorators помогает объединить несколько часто используемых декораторов. Например, команда проекта может объединить документирование и назначение guard. Делайте это только при устойчивом повторении: слишком абстрактное имя скрывает важные условия доступа. Ревьюер должен понимать, какой guard выполняется и что именно проверяет правило.

Метаданные не должны содержать secrets, изменяемые данные конкретного запроса или результат чтения пользователя. Обычно они принадлежат определению класса и разделяются всеми запросами. Параметры текущего запроса остаются в ExecutionContext и локальных переменных. Это сохраняет предсказуемость при параллельной обработке.

Практика роли маршрута

В упражнении назначьте GET /admin метаданные role со значением admin. Guard получает их через Reflector.get для context.getHandler и сравнивает с переданной ролью. solve создаёт настоящее Nest-приложение, отправляет HTTP-запрос и возвращает 200 или 403. Различие регистра существенно: Admin не равно admin по контракту задачи.

Сначала убедитесь, что метаданные присутствуют, затем что guard зарегистрирован и назначен маршруту. После решения задайте роль на контроллере, переопределите на одном методе и изучите getAllAndOverride. Объясните, почему перестановка целей меняет приоритет и почему декоратор без исполняющего механизма никогда не является самостоятельной авторизацией.

Загружаем редактор…
Следующая темаInterceptors и RxJS

Музыка: Reverie — Scott Buckley · CC BY 4.0