Kurs NestJS · Moduł 2: Routing i cykl żądania
Guards - kto przechodzi przez bramę
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- nieNestMiddleware, nieNestInterceptor, niePipeTransform, canActivate()zwracatrue, żeby zezwolić na dostęp - nie napis'allowed', nie obiekt z rolami, nienull;falsedaje403i handler się nie wykonuje,- kolejność w metodzie:
canActivate(context: ExecutionContext) {→const request = context.switchToHttp().getRequest();→const token = request.headers.authorization;→return !!token;, ExecutionContextudostę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 przezuseGlobalGuardsalbo tokenAPP_GUARD, SetMetadata('roles', roles)zapisuje wymagania przy handlerze, aReflectorje 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");
96Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym middleware różni się od guarda w NestJS?
2. Jaki interfejs implementuje guard w NestJS?
3. Co zwraca metoda canActivate() guarda, aby zezwolić na dostęp do endpointu?
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