Umiesz już wystawić przepustkę JWT i sprawdzić hasło. Ale w prawdziwym Imperium do bram podchodzą różni goście: jeden ma hasło, drugi przepustkę z pieczęcią, trzeci list polecający od sojusznika z Galii, czwarty klucz do bramy handlowej. Pisząc obsługę każdego z osobna, powielisz tę samą logikę cztery razy - a każdy sposób wpuszczania to nowa okazja do pomyłki.
Rzymianie postawili przy bramie strażnicę z jednym regulaminem i wieloma wartownikami: każdy wartownik zna jeden rodzaj dokumentu, ale wszyscy meldują tak samo. W NestJS tą strażnicą jest Passport.js - biblioteka, w której każdy sposób logowania to osobna strategia, a ekosystem oferuje ich ponad 500 gotowych.
Zanim napiszemy linijkę kodu, ustalmy, z czego składa się ta strażnica - bo to jest szkielet całej lekcji i wraca przy każdej strategii:
@UseGuards() - postawienie strażnika przy wejściu do endpointu.Zapamiętaj tę kolejność od dołu do góry: strategia wie jak sprawdzić, guard wie kogo zawołać, dekorator wie gdzie go postawić.
Zacznijmy od najprostszego wartownika: sprawdza login i hasło.
1@Injectable()
2export class LocalStrategy extends PassportStrategy(Strategy) {
3 constructor(private authService: AuthService) {
4 super({
5 usernameField: 'username',
6 passwordField: 'password',
7 });
8 }
9
10 async validate(username: string, password: string): Promise<any> {
11 const user = await this.authService.validateUser(username, password);
12
13 if (!user) {
14 throw new UnauthorizedException();
15 }
16
17 return user;
18 }
19}Prześledźmy tę klasę, bo każdy jej element powtórzy się w kolejnych strategiach.
PassportStrategy(Strategy) to klasa bazowa budowana z importu Strategy - tu z pakietu passport-local. Zmieniając pakiet, zmieniasz rodzaj wartownika, a reszta szkieletu zostaje.Wywołanie
super() w konstruktorze konfiguruje strategię. usernameField i passwordField mówią, z których pól żądania wziąć dane - i to jest częsty punkt zaczepienia, bo jeśli Twój formularz wysyła email zamiast username, właśnie tu to zgłaszasz.Sercem jest
validate(). Passport wywołuje ją sam, podając wyciągnięte pola, a Twoim zadaniem jest odpowiedzieć: kto to jest. Zwróć uwagę na kontrakt tej metody - to najważniejsze zdanie tej lekcji. Zwrócony obiekt trafia do request.user i staje się dostępny w kontrolerze. Gdy dokument jest fałszywy, nie zwracasz null ani false - rzucasz UnauthorizedException, a NestJS zamieni ją na odpowiedź 401.Strategia sama z siebie nie zadziała. Trzeba ją aktywować, a robi to guard:
1@Injectable()
2export class LocalAuthGuard extends AuthGuard('local') {}Tak, to całe ciało klasy - puste.
AuthGuard('local') buduje gotowego guarda, który po nazwie odnajduje zarejestrowaną strategię i uruchamia jej validate(). Nazwa 'local' nie jest przypadkowa: to domyślny identyfikator strategii z pakietu passport-local, tak jak 'jwt' należy do passport-jwt, a 'google' do strategii Google.Analogicznie wygląda guard dla przepustek JWT, które poznałeś w poprzedniej lekcji:
1@Injectable()
2export class JwtAuthGuard extends AuthGuard('jwt') {}Po co w ogóle własna klasa, skoro można napisać
@UseGuards(AuthGuard('local')) wprost? Z dwóch powodów: nazwa LocalAuthGuard czyta się lepiej w kontrolerze, a gdy zechcesz dołożyć własne zachowanie, masz gdzie je wpisać. Właśnie do tego służy metoda handleRequest, którą możesz nadpisać:1@Injectable()
2export class JwtAuthGuard extends AuthGuard('jwt') {
3 handleRequest(err: any, user: any) {
4 if (err) {
5 throw err;
6 }
7
8 if (!user) {
9 throw new UnauthorizedException('Przepustka nieważna lub wygasła');
10 }
11
12 return user;
13 }
14}Kolejność jest tu logiczna i warto ją zapamiętać: najpierw sprawdzasz błąd, potem obecność użytkownika, dopiero na końcu zwracasz obiekt - a to, co zwrócisz, wyląduje w
request.user. Nadpisujemy tę metodę głównie po to, by dać czytelny komunikat zamiast gołego 401.Skoro szkielet się nie zmienia, dołożenie logowania przez Google sprowadza się do podmiany pakietu i danych konfiguracyjnych. Wartownik jest nowy, regulamin ten sam.
1@Injectable()
2export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
3 constructor(private configService: ConfigService) {
4 super({
5 clientID: configService.get('GOOGLE_CLIENT_ID'),
6 clientSecret: configService.get('GOOGLE_CLIENT_SECRET'),
7 callbackURL: '/auth/google/callback',
8 scope: ['email', 'profile'],
9 });
10 }
11}Tu
Strategy pochodzi z pakietu passport-google-oauth20. clientID i clientSecret to dane Twojej aplikacji wydane przez Google - i dlatego czytamy je przez ConfigService ze zmiennych środowiskowych, a nie wpisujemy w kod. Sekret w repozytorium to sekret cudzy; to polecam traktować jako regułę bez wyjątków, @name.callbackURL to adres, pod który Google odeśle użytkownika po zalogowaniu - Twoja aplikacja musi mieć tam endpoint. scope określa, o jakie dane prosisz.Zwróć uwagę na drugi argument
PassportStrategy(Strategy, 'google'): to jawnie nadana nazwa, po której guard AuthGuard('google') odnajdzie tę strategię. Przy strategii lokalnej mogliśmy go pominąć, bo nazwa domyślna wystarczała.Ostatni krok to wskazanie, których endpointów strażnik pilnuje:
1@Controller('auth')
2export class AuthController {
3 @UseGuards(LocalAuthGuard)
4 @Post('login')
5 async login(@Request() req) {
6 return this.authService.generateToken(req.user);
7 }
8
9 @UseGuards(JwtAuthGuard)
10 @Get('profile')
11 getProfile(@Request() req) {
12 return req.user;
13 }
14}Tu domyka się cały łańcuch. Guard uruchomił strategię, strategia wykonała
validate(), zwrócony obiekt wylądował w request.user - i dopiero teraz metoda kontrolera może po niego sięgnąć. Jeśli weryfikacja się nie powiodła, metoda nie wykona się wcale: guard zatrzymuje żądanie, zanim dojdzie ono do kontrolera.Strażnica stoi, a Ty umiesz zatrudnić w niej dowolnego wartownika:
@UseGuards() (gdzie postawić),PassportStrategy(Strategy), konfiguruje się przez super({...}) i implementuje validate(),validate() zwraca użytkownika, który trafia do request.user, a przy odrzuceniu rzuca UnauthorizedException,extends AuthGuard('local'), AuthGuard('jwt'), AuthGuard('google'),handleRequest nadpisujesz, gdy chcesz własną obsługę: sprawdź błąd, sprawdź użytkownika, zwróć obiekt,clientID i clientSecret czytaj z ConfigService, nigdy z kodu,PassportStrategy(Strategy, 'nazwa') nadaje strategii nazwę, po której znajdzie ją guard.W następnej lekcji zejdziemy poziom niżej - do ról i uprawnień, czyli pytania, co wolno gościowi, którego już wpuściliśmy. A na razie zapamiętaj: strategia wie, jak sprawdzić dokument, guard wie, którego wartownika zawołać, a
request.user to meldunek, który zostaje po udanej kontroli.