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. Zaczniemy od domknięcia sprawy z poprzedniej lekcji: jak middleware w ogóle podpina się do aplikacji.
Sama klasa middleware nic nie robi, dopóki nie wskażesz, gdzie ma działać. Robi to moduł, który implementuje interfejs
NestModule i wypełnia jego metodę configure:1import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
2
3@Module({
4 controllers: [TributesController],
5})
6export class AppModule implements NestModule {
7 configure(consumer: MiddlewareConsumer) {
8 consumer.apply(LoggerMiddleware).forRoutes('tributes');
9 }
10}Kolejność zapisu jest zawsze ta sama:
otwiera metodę, configure(consumer: MiddlewareConsumer) {
wskazuje klasę middleware, consumer.apply(LoggerMiddleware)
wskazuje trasy, na których ma działać, a klamra .forRoutes('tributes')
zamyka całość. Cały proces to cztery kroki: utwórz klasę middleware z }
@Injectable(), zaimplementuj NestModule w klasie modułu, wywołaj consumer.apply() w metodzie configure(), na końcu określ trasy przez .forRoutes().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.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".
Guard to klasa implementująca interfejs
. Nie CanActivate
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:
przyjmuje kontekst, canActivate(context: ExecutionContext) {
wydobywa z niego obiekt żądania, const request = context.switchToHttp().getRequest();
sięga po nagłówek, a const token = request.headers.authorization;
zamienia go na decyzję.return !!token;
Ta decyzja to wartość logiczna
, gdy dostęp ma być przyznany - nie napis true
'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.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?".
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:
otwiera dekorator, @UseGuards(
nazywa klasę strażnika - klasę, nie jej instancję, bo tworzenie zostawiamy wstrzykiwaniu zależności - a AuthGuard
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.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ęć.Rogatka liczy wchodzących, strażnik skarbca decyduje, @name:
NestModule: configure(consumer: MiddlewareConsumer) { → consumer.apply(LoggerMiddleware) → .forRoutes('tributes') → },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,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,@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".