Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Guards - kto przechodzi przez bramę

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.

Rejestracja middleware - trzy elementy

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:

configure(consumer: MiddlewareConsumer) {
otwiera metodę,
consumer.apply(LoggerMiddleware)
wskazuje klasę middleware,
.forRoutes('tributes')
wskazuje trasy, na których ma działać, a klamra
}
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.

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, @name:

  • 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".

Przejdź do CodeWorlds