NestJS / 37 минут
Guards и авторизация
Проверяйте права до выполнения обработчика.
- Проверяйте права до выполнения обработчика.
- Применить тему в задаче «Guards и авторизация» и объяснить граничные случаи.
Около 7 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Допуск до выполнения сценария
Guard решает, разрешено ли продолжать обработку запроса. Он выполняется раньше pipes и метода контроллера. Это важно: запрет должен остановить действие до изменения данных. Скрытая кнопка в интерфейсе и frontend middleware помогают навигации, но не ограничивают прямой HTTP-запрос к API. Авторизация требуется на сервере.
Authentication определяет пользователя по доверенному credential, например проверенной сессии. Authorization решает, что этот пользователь может сделать. Наличие поля user в JSON не подтверждает личность отправителя. Guard должен получать principal из проверенной серверной стадии, а не принимать роль admin из тела или query.
interface Principal { id: string; role: 'user' | 'admin' }
function canRead(user: Principal | null, ownerId: string) {
return user !== null && (user.role === 'admin' || user.id === ownerId);
}CanActivate и ExecutionContext
Guard реализует CanActivate. Возвращённое true пропускает запрос, false вызывает отказ, по умолчанию с HTTP 403. Можно вернуть Promise или Observable, когда решение асинхронно. ExecutionContext позволяет получить handler, класс контроллера и контекст транспорта; для HTTP используют switchToHttp().
@Injectable()
class OwnerGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest<{
user: Principal | null;
params: { owner: string };
}>();
return canRead(req.user, req.params.owner);
}
}req.user в примере должен установить предыдущий authentication-механизм. Типизация getRequest не создаёт это свойство и не проверяет cookie. На этапе guard параметры пути ещё не прошли parameter pipes, поэтому не предполагайте, что строковый id уже преобразован в число. Для безопасного поиска используйте понятный валидируемый контракт.
401 и 403 выражают разные ситуации
Unauthenticated запрос обычно получает 401. Подтверждённому пользователю без права выполняется отказ 403. Если authentication guard бросает UnauthorizedException, следующий authorization guard вообще не должен исполняться. Возврат false из одного учебного guard даёт 403 даже для null, если отдельной проверки личности нет.
@Get(':owner')
@UseGuards(SessionGuard, OwnerGuard)
read(@Param('owner') ownerId: string) {
return this.notes.findForOwner(ownerId);
}SessionGuard и сервис notes здесь принадлежат приложению. Порядок двух guards отражает зависимость: сначала получить principal, затем проверить право. Не превращайте любую внутреннюю ошибку репозитория в «не авторизован»: это скрывает эксплуатационные сбои и мешает диагностике. Клиенту при этом не нужны детали БД и stack trace.
Права на конкретный объект
Роль — только часть доступа. Пользователь может редактировать собственную заметку, но не чужую с известным id. Проверяйте ownership или более точное правило на каждой операции. Если запрос уже содержит ownerId, это ещё не доказывает, что выбранная запись принадлежит ему: сравнивать нужно с достоверными данными хранилища.
Для конкурентных обновлений полезно включать ownerId в условие самой операции с БД. Между отдельным чтением и записью состояние может измениться. Сервис остаётся ответственным за доменное правило, даже если guard выполнил общий допуск. Не следует переносить сложный сценарий целиком в guard и затем повторять его в контроллере.
Повторное использование и метаданные
Guard можно назначить маршруту, классу или всему приложению. Общую политику удобно описать декоратором с метаданными, а прочитать через Reflector. Сам декоратор не блокирует запрос. Если объявить @Roles без guard, надпись существует в коде, но проверка не выполняется. Следующая глава разбирает эту связь отдельно.
Избегайте сохранения текущего пользователя в поле guard с singleton scope. Два одновременных запроса обслуживаются одним объектом. Контекст и локальные переменные принадлежат конкретному вызову; долгоживущее поле подходит для зависимости, а не для principal. Это правило одинаково относится к guards, сервисам и interceptors.
Практика доступа к заметке
Создайте настоящий guard для GET /notes/:owner. Переданный user считается уже подтверждённым либо равен null. Разрешите admin или пользователя с id, совпадающим с ownerId. В упражнении authentication отдельно не реализуется, поэтому все отказы guard, включая null, ожидаются как 403. solve возвращает реальный HTTP-статус.
Проверьте владельца, чужого пользователя, администратора и отсутствие пользователя. Не сравнивайте только роль и не допускайте null из-за необязательной цепочки. После решения добавьте в локальном проекте независимую authentication-стадию и покажите разницу 401 и 403 в интеграционном тесте, сохранив неизменной саму политику ownership.