Kurs NestJS · Moduł 2: Routing i cykl żądania

Guards - kto przechodzi przez bramę

5 min czytania
W tej lekcji6

Wartownik przy rogatce liczy wszystkich wchodzących i zapisuje godzinę. Robi to samo dla kupca, posłańca i legionisty - bo na rogatce jeszcze nie wiadomo, dokąd który z nich idzie. Dopiero przy drzwiach skarbca stoi ktoś, kto wie, do której komnaty puka przybysz, i dlatego może powiedzieć „ty nie".

To jest cała różnica między middleware a guardem, i w tej lekcji zamkniemy ją do końca.

W poprzedniej lekcji middleware podpięliśmy w module przez configure(consumer) i .forRoutes('tributes'). Zwróć uwagę na argument 'tributes': middleware dostaje wzorzec ścieżki, nie nazwę kontrolera ani metody - i to jest pierwszy trop do tego, czego mu brakuje.

Czego middleware nie wie

Middleware wykonuje się przed guardem i nie zna trasy docelowej. Nie jest to niedopatrzenie autorów frameworka, tylko konsekwencja momentu: middleware działa na poziomie Express, zanim NestJS rozstrzygnie, który kontroler i która metoda obsłużą żądanie. Dostaje req, res i next - trzy obiekty HTTP i nic ponadto.

Dlatego middleware świetnie nadaje się do rzeczy niezależnych od celu: logowania, nagłówków CORS, parsowania ciasteczek. Nie nadaje się do decyzji w rodzaju „ten endpoint wymaga roli administratora", bo w chwili jego działania nie istnieje jeszcze pojęcie „tego endpointu".

CanActivate - interfejs strażnika

Guard to klasa implementująca interfejs CanActivate. Nie NestMiddleware - ten należy do middleware z poprzedniej lekcji; nie NestInterceptor - to interceptory, które poznasz za chwilę; nie PipeTransform - to pipes, jeszcze dalej. Każdy z czterech mechanizmów cyklu żądania ma własny interfejs i własną jedną metodę:

1@Injectable()
2export class AuthGuard implements CanActivate {
3  canActivate(context: ExecutionContext): boolean {
4    const request = context.switchToHttp().getRequest();
5    const token = request.headers.authorization;
6
7    return !!token;
8  }
9}

Ciało metody czyta się w czterech krokach, zawsze w tej kolejności: canActivate(context: ExecutionContext) { przyjmuje kontekst, const request = context.switchToHttp().getRequest(); wydobywa z niego obiekt żądania, const token = request.headers.authorization; sięga po nagłówek, a return !!token; zamienia go na decyzję.

Ta decyzja to wartość logiczna true, gdy dostęp ma być przyznany - nie napis 'allowed', nie obiekt z rolami, nie null. Podwójny wykrzyknik w !!token robi dokładnie to: zamienia „nagłówek jest albo go nie ma" na true albo false. Gdy padnie false, NestJS odrzuca żądanie z kodem 403 Forbidden, a metoda kontrolera nie uruchamia się wcale.

ExecutionContext - to, czego brakowało

Argument context jest powodem, dla którego guard potrafi więcej niż middleware. ExecutionContext udostępnia informacje o docelowym kontrolerze i metodzie (handlerze) - nie dostęp do bazy danych, nie dostęp do systemu plików, nie konfigurację serwera. Te rzeczy zdobywa się inaczej: bazę przez wstrzyknięcie repozytorium, konfigurację przez ConfigService.

Dwie metody wystarczą na początek. context.switchToHttp().getRequest() schodzi do warstwy HTTP po znajomy obiekt żądania - „switch", bo NestJS obsługuje też WebSockety i mikroserwisy, a kontekst jest wspólny dla wszystkich. context.getHandler() zwraca samą metodę kontrolera, która ma obsłużyć żądanie, a context.getClass() - klasę kontrolera.

I właśnie tego middleware nie ma. Guard, mając w ręku handler, może zapytać: „czego ta konkretna metoda wymaga?".

Podpięcie guarda

Guard podpina się dekoratorem @UseGuards - na pojedynczej metodzie albo na całym kontrolerze:

1@Controller('treasury')
2@UseGuards(AuthGuard)
3export class TreasuryController {
4  @Get()
5  findAll() {
6    return this.treasuryService.findAll();
7  }
8}

Zapis składa się z trzech części: @UseGuards( otwiera dekorator, AuthGuard nazywa klasę strażnika - klasę, nie jej instancję, bo tworzenie zostawiamy wstrzykiwaniu zależności - a ) domyka. Guardów można podać kilka po przecinku; sprawdzane są po kolei i wystarczy jeden false, żeby żądanie przepadło.

Gdy strażnik ma pilnować całej aplikacji, rejestrujesz go globalnie: app.useGlobalGuards(new AuthGuard()) w main.ts albo jako provider z tokenem APP_GUARD. Druga droga jest lepsza, gdy guard sam czegoś potrzebuje - provider przechodzi przez wstrzykiwanie zależności, new nie.

Reflector - guard, który czyta wymagania

Skoro guard zna handler, może odczytać wymagania zapisane przy nim. Wymagania dopina się przez SetMetadata, a odczytuje klasą Reflector:

1export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
2
3@Injectable()
4export class RolesGuard implements CanActivate {
5  constructor(private reflector: Reflector) {}
6
7  canActivate(context: ExecutionContext): boolean {
8    const requiredRoles = this.reflector.get<string[]>(
9      'roles',
10      context.getHandler(),
11    );
12
13    if (!requiredRoles) {
14      return true;
15    }
16
17    const request = context.switchToHttp().getRequest();
18
19    return requiredRoles.includes(request.user?.role);
20  }
21}

SetMetadata('roles', roles) przykleja do metody etykietę pod kluczem 'roles'. this.reflector.get<string[]>('roles', context.getHandler()) czyta ją z powrotem - pierwszy argument to klucz, drugi to miejsce, przy którym szukać, czyli nasz handler. Brak etykiety oznacza endpoint bez wymagań, więc zwracamy true i przepuszczamy.

Jeden guard obsługuje w ten sposób całą aplikację: @Roles('senator') nad jedną metodą, @Roles('centurion', 'tribune') nad inną, a logika porównania zapisana raz. To ten sam SetMetadata, do którego wrócimy przy własnych dekoratorach - tam zobaczysz, jak zwinąć @Roles i @UseGuards w jedną pieczęć.

Podsumowanie

Rogatka liczy wchodzących, strażnik skarbca decyduje:

  • middleware rejestrujesz w module implementującym NestModule: configure(consumer: MiddlewareConsumer) { → consumer.apply(LoggerMiddleware) → .forRoutes('tributes') → },
  • middleware wykonuje się przed guardem i nie zna trasy docelowej - działa, zanim NestJS rozstrzygnie, który handler obsłuży żądanie,
  • guard implementuje interfejs CanActivate - nie NestMiddleware, nie NestInterceptor, nie PipeTransform,
  • canActivate() zwraca true, żeby zezwolić na dostęp - nie napis 'allowed', nie obiekt z rolami, nie null; false daje 403 i handler się nie wykonuje,
  • kolejność w metodzie: canActivate(context: ExecutionContext) { → const request = context.switchToHttp().getRequest(); → const token = request.headers.authorization; → return !!token;,
  • ExecutionContext udostępnia informacje o docelowym kontrolerze i metodzie (handlerze) - nie bazę danych, nie system plików, nie konfigurację serwera,
  • context.getHandler() zwraca metodę, context.getClass() - kontroler, switchToHttp().getRequest() - obiekt żądania,
  • podpięcie: @UseGuards( → AuthGuard → ), globalnie przez useGlobalGuards albo token APP_GUARD,
  • SetMetadata('roles', roles) zapisuje wymagania przy handlerze, a Reflector je odczytuje: this.reflector.get<string[]>('roles', context.getHandler()).

W następnej lekcji poznasz interceptory - trzeci mechanizm cyklu, który jako pierwszy dotyka odpowiedzi, a nie tylko żądania. A na razie zapamiętaj różnicę: middleware pyta „co przyszło", guard pyta „dokąd to idzie i czy wolno".

Kod do tej lekcji: src/guards.ts
1// Guards w NestJS - Elitarni Strażnicy Skarbca
2import {
3  Injectable, CanActivate, ExecutionContext,
4  SetMetadata, UnauthorizedException, ForbiddenException,
5} from '@nestjs/common';
6import { Reflector } from '@nestjs/core';
7
8console.log("Guards - elitarni strażnicy decydujący o dostępie!");
9
10// ===========================================
11// 1. Prosty AuthGuard
12// ===========================================
13
14@Injectable()
15export class RomanAuthGuard implements CanActivate {
16  canActivate(context: ExecutionContext): boolean {
17    const request = context.switchToHttp().getRequest();
18    const authHeader = request.headers['authorization'];
19
20    if (!authHeader || !authHeader.startsWith('Bearer ')) {
21      throw new UnauthorizedException('Brak tokenu autoryzacji!');
22    }
23
24    const token = authHeader.split(' ')[1];
25
26    // W prawdziwej aplikacji: weryfikacja JWT
27    if (token !== 'roman-secret-token') {
28      throw new UnauthorizedException('Nieprawidłowy token!');
29    }
30
31    // Dodaj dane użytkownika do requestu
32    request.user = { id: 1, name: 'Marcus', roles: ['Centurio'] };
33    return true;
34  }
35}
36
37// ===========================================
38// 2. RolesGuard z metadanymi
39// ===========================================
40
41export const ROLES_KEY = 'roles';
42export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
43
44@Injectable()
45export class RolesGuard implements CanActivate {
46  constructor(private reflector: Reflector) {}
47
48  canActivate(context: ExecutionContext): boolean {
49    const requiredRoles = this.reflector.get<string[]>(
50      ROLES_KEY,
51      context.getHandler(),
52    );
53
54    // Brak wymaganych ról - wpuść
55    if (!requiredRoles || requiredRoles.length === 0) {
56      return true;
57    }
58
59    const request = context.switchToHttp().getRequest();
60    const user = request.user;
61
62    if (!user) {
63      throw new ForbiddenException('Użytkownik niezalogowany!');
64    }
65
66    const hasRole = requiredRoles.some(role => user.roles?.includes(role));
67
68    if (!hasRole) {
69      throw new ForbiddenException('Brak wymaganej rangi: ' + requiredRoles.join(', '));
70    }
71
72    return true;
73  }
74}
75
76// ===========================================
77// 3. Użycie w kontrolerze
78// ===========================================
79
80// @Controller('senate')
81// @UseGuards(RomanAuthGuard, RolesGuard)
82// export class SenateController {
83//   @Roles('Senator', 'Consul')
84//   @Get('secret-decrees')
85//   getSecretDecrees() {
86//     return { decrees: ['Tajny dekret...'] };
87//   }
88// }
89
90console.log("\n=== PODSUMOWANIE GUARDS ===");
91console.log("CanActivate - interfejs guardów");
92console.log("canActivate() zwraca true/false");
93console.log("@SetMetadata - ustawianie metadanych (np. role)");
94console.log("Reflector - odczytywanie metadanych");
95console.log("@UseGuards() - stosowanie guarda na kontrolerze/metodzie");
96

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Czym middleware różni się od guarda w NestJS?

  2. 2. Jaki interfejs implementuje guard w NestJS?

  3. 3. Co zwraca metoda canActivate() guarda, aby zezwolić na dostęp do endpointu?

  4. 4. Co udostępnia ExecutionContext w guardzie, czego nie ma w middleware?

Zadania praktyczne w grze

  • Klikanie w kolejności

    Ułóż elementy konfiguracji middleware w prawidłowej kolejności:

  • Edytor kodu

    Zaimplementuj NestModule w AppModule z metodą configure(), która rejestruje LoggerMiddleware dla tras 'tributes'

  • Edytor kodu

    Napisz AuthGuard implementujący CanActivate, który sprawdza obecność tokenu w nagłówku authorization żądania

  • Układanie w poziomie

    Ułóż elementy użycia dekoratora @UseGuards na kontrolerze:

  • Klikanie w kolejności

    Ułóż elementy implementacji guarda autoryzacji w prawidłowej kolejności:

  • Edytor kodu

    Napisz RolesGuard, który używa Reflector do odczytania ról z SetMetadata i porównania z rolą użytkownika

Przydatne artykuły