Kurs NestJS · Moduł 4: Uwierzytelnianie
JWT Tokens - rzymskie przepustki
W tej lekcji6
Legionista podał hasło przy bramie i został wpuszczony. Za godzinę wraca po zaopatrzenie - i co, znowu ma podawać hasło? A przy trzeciej bramie? Sprawdzanie hasła przy każdym kroku oznacza, że serwer musi przy każdym żądaniu sięgać do bazy po użytkownika. Setki razy na minutę, po to samo.
Rzymianie wydawali w takich razach przepustkę: dokument z wypisanymi uprawnieniami i pieczęcią, którą każdy strażnik potrafi rozpoznać. Wartownik nie biegnie do archiwum - patrzy na pieczęć i wie, że dokument jest prawdziwy. W sieci ta przepustka nazywa się JWT, czyli JSON Web Token.
Trzy części przepustki
Token JWT wygląda jak ciąg znaków przedzielony dwiema kropkami. Te kropki nie są przypadkowe - dzielą go na trzy części, zawsze w tej samej kolejności:
1eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjQyLCJyb2xlIjoiY2VudHVyaW9uIn0.dBjftJeZ4CVP
2 <-- Header --> <----- Payload -----> <- Signature ->Header mówi, jakim algorytmem podpisano dokument. Payload to treść przepustki - dane o legioniście, zwane claims: jego identyfikator, rola, czas wygaśnięcia. Signature to pieczęć: wynik podpisania dwóch pierwszych części tajnym kluczem serwera.
Teraz najważniejsze i najczęściej mylone: payload jest zakodowany, ale nie zaszyfrowany. Każdy, kto przechwyci token, odczyta jego treść - to zwykły Base64, nie tajemnica. Pieczęć nie ukrywa danych, tylko dowodzi, że nikt ich nie podmienił, bo bez tajnego klucza nie da się jej podrobić. Wniosek praktyczny jest jeden: do payloadu nigdy nie wkładaj hasła ani danych wrażliwych.
Konfiguracja - wydawanie przepustek
Zdolność wydawania przepustek dodajemy przez moduł:
1JwtModule.registerAsync({
2 imports: [ConfigModule],
3 inject: [ConfigService],
4 useFactory: (configService: ConfigService) => ({
5 secret: configService.get('JWT_SECRET'),
6 signOptions: {
7 expiresIn: '24h',
8 issuer: 'LegionaryFleet',
9 },
10 }),
11});secret to tajny klucz, którym serwer pieczętuje tokeny i którym je potem weryfikuje. Czytamy go przez ConfigService ze zmiennej środowiskowej, w pliku .env zapisanej jako JWT_SECRET=super-secret-key - nazwa, znak równości, wartość, bez cudzysłowów i spacji. Klucz wpisany w kod trafiłby do repozytorium, a stamtąd w cudze ręce; kto ma klucz, może wystawić sobie przepustkę centuriona.
Używamy registerAsync zamiast zwykłego register, bo wartość klucza nie jest znana w chwili pisania kodu - trzeba ją najpierw odczytać z konfiguracji. Stąd useFactory: funkcja, którą NestJS wywoła, gdy ConfigService będzie już dostępny.
W signOptions opisujemy samą przepustkę. expiresIn: '24h' to jej czas życia - po dobie dokument traci ważność i legionista musi zalogować się ponownie. To celowe ograniczenie: gdyby token wyciekł, złodziej ma tylko tyle czasu. issuer to wystawca, zapisywany w payloadzie.
Kształt przepustki
Warto opisać typem, co dokładnie wkładamy do payloadu:
1export interface JwtPayload {
2 sub: number;
3 username: string;
4 email: string;
5 role: string;
6 iat: number;
7 exp: number;
8}Trzy z tych nazw są skrótami ustalonymi w standardzie JWT i dlatego wyglądają dziwnie. sub to subject - identyfikator tego, kogo dotyczy przepustka, czyli po prostu id użytkownika. iat to issued at, moment wystawienia. exp to expiration, moment wygaśnięcia - to jego pilnuje weryfikacja czasu. Pola iat i exp biblioteka dopisuje sama; username, email i role to już Twoje własne claims.
Wystawienie przepustki przy logowaniu
Gdy hasło się zgadza, serwis podpisuje token i oddaje go klientowi:
1async login(user: User) {
2 const payload = {
3 sub: user.id,
4 username: user.username,
5 role: user.role,
6 };
7
8 return {
9 access_token: this.jwtService.sign(payload),
10 };
11}jwtService.sign() bierze Twój obiekt, dokłada iat i exp zgodnie z signOptions, a całość pieczętuje tajnym kluczem. Zwrócony ciąg znaków to gotowa przepustka. Zauważ, czego tu nie ma: serwer niczego nie zapamiętuje. Nie powstaje żaden wpis w bazie sesji - cała wiedza o legioniście podróżuje w samym tokenie. Na tym polega różnica między przepustką a listą gości przy bramie.
Cały obieg wygląda więc tak: klient wysyła login i hasło, serwer weryfikuje je w bazie, serwer generuje token, klient dołącza go do każdego kolejnego żądania w nagłówku Authorization, a serwer odczytuje token i autoryzuje żądanie - bez sięgania do bazy po hasło.
Wartownik od przepustek
Kto właściwie sprawdza pieczęć przy każdym żądaniu? Robi to strategia - klasa, w której opisujesz, skąd wziąć token i jak go zweryfikować. Pełny system strategii poznasz w kolejnej lekcji; na razie wystarczy ta jedna, od przepustek:
1@Injectable()
2export class JwtStrategy extends PassportStrategy(Strategy) {
3 constructor(private configService: ConfigService) {
4 super({
5 jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
6 ignoreExpiration: false,
7 secretOrKey: configService.get('JWT_SECRET'),
8 });
9 }
10
11 async validate(payload: JwtPayload) {
12 return { userId: payload.sub, username: payload.username, role: payload.role };
13 }
14}Trzy opcje w super() opisują, jak wartownik czyta dokument. jwtFromRequest mówi, skąd wziąć token - ExtractJwt.fromAuthHeaderAsBearerToken() wyciąga go z nagłówka Authorization: Bearer <token>, czyli najpopularniejszego miejsca. secretOrKey to ten sam klucz, którym token podpisano; bez niego nie da się sprawdzić pieczęci.
ignoreExpiration: false bywa mylące przez tę podwójną przeczącą formę, więc przeczytajmy ją wprost: nie ignoruj wygaśnięcia, czyli sprawdzaj exp i odrzucaj przeterminowane tokeny. To jest ustawienie, którego chcesz. Ustawienie true oznaczałoby, że przepustka sprzed roku wciąż otwiera bramy.
Metoda validate() dostaje już zweryfikowany payload - pieczęć sprawdzono, data ważności też, zanim Twój kod się uruchomił. Zwrócony obiekt trafia do request.user i staje się dostępny w kontrolerze. Zauważ, że nie ma tu zapytania do bazy: wszystkie potrzebne dane przyszły w tokenie, i to jest właśnie oszczędność, dla której wprowadziliśmy przepustki.
Podsumowanie
Legionista nosi przepustkę i nie podaje hasła przy każdej bramie:
- JWT składa się z trzech części w stałej kolejności: Header, Payload, Signature,
- payload jest kodowany, nie szyfrowany - każdy go odczyta, więc nie wkładaj tam haseł,
- podpis nie ukrywa danych, tylko dowodzi, że nikt ich nie podmienił,
JwtModule.registerAsynczuseFactorypozwala pobraćsecretzConfigService; klucz mieszka w.env, nigdy w kodzie,signOptions.expiresInustala czas życia tokenu,issuer- wystawcę,- standardowe claims:
sub(id użytkownika),iat(wystawiono),exp(wygasa), jwtService.sign(payload)wystawia przepustkę, a serwer niczego nie zapamiętuje,- w
JwtStrategy:ExtractJwt.fromAuthHeaderAsBearerToken()czyta token z nagłówka Bearer,ignoreExpiration: falseodrzuca przeterminowane, avalidate()dostaje payload już po weryfikacji.
W następnej lekcji poznasz Passport.js od strony całego systemu strategii - zobaczysz, że wartownik od przepustek to tylko jeden z wielu. A na razie zapamiętaj: JWT to przepustka z pieczęcią - dane w niej każdy przeczyta, ale podrobić ich nie zdoła nikt bez tajnego klucza.
Kod do tej lekcji: src/auth/jwt-config.ts
1// JWT Tokens - Rzymskie Przepustki
2// Kazda przepustka sklada sie z 3 czesci: Header, Payload, Signature
3import { Module } from '@nestjs/common';
4import { JwtModule, JwtService } from '@nestjs/jwt';
5import { ConfigModule, ConfigService } from '@nestjs/config';
6import { Injectable } from '@nestjs/common';
7
8// ===========================================
9// 1. Konfiguracja JWT Module
10// ===========================================
11
12@Module({
13 imports: [
14 JwtModule.registerAsync({
15 imports: [ConfigModule],
16 useFactory: async (configService: ConfigService) => ({
17 // Sekret do podpisywania tokenow
18 secret: configService.get<string>('JWT_SECRET') || 'spqr-secret-key',
19 signOptions: {
20 expiresIn: '24h', // Token wazny 24 godziny
21 issuer: 'imperium-romanum', // Wydawca tokenu
22 algorithm: 'HS256', // Algorytm podpisu
23 },
24 }),
25 inject: [ConfigService],
26 }),
27 ],
28 exports: [JwtModule],
29})
30export class JwtConfigModule {}
31
32// ===========================================
33// 2. Serwis obslugujacy tokeny
34// ===========================================
35
36@Injectable()
37export class TokenService {
38 constructor(private jwtService: JwtService) {}
39
40 // Tworzenie tokenu dostepu
41 createAccessToken(legionary: { id: string; username: string; rank: string }) {
42 const payload = {
43 sub: legionary.id,
44 username: legionary.username,
45 rank: legionary.rank,
46 type: 'access',
47 };
48 return this.jwtService.sign(payload, { expiresIn: '1h' });
49 }
50
51 // Tworzenie tokenu odswiezania
52 createRefreshToken(legionary: { id: string }) {
53 const payload = {
54 sub: legionary.id,
55 type: 'refresh',
56 };
57 return this.jwtService.sign(payload, { expiresIn: '7d' });
58 }
59
60 // Weryfikacja tokenu
61 verifyToken(token: string) {
62 try {
63 const decoded = this.jwtService.verify(token);
64 console.log('Token zdekodowany:', decoded);
65 return decoded;
66 } catch (error) {
67 console.log('Token niewazny lub wygasl!');
68 throw error;
69 }
70 }
71
72 // Dekodowanie bez weryfikacji (np. do odczytu payloadu)
73 decodeToken(token: string) {
74 return this.jwtService.decode(token);
75 }
76}
77
78// ===========================================
79// 3. Struktura tokenu JWT
80// ===========================================
81
82console.log('=== Struktura JWT ===');
83console.log('Header: { alg: "HS256", typ: "JWT" }');
84console.log('Payload: { sub: "1", username: "Caesar", rank: "consul" }');
85console.log('Signature: HMACSHA256(header + payload, secret)');
86console.log('');
87console.log('Token = base64(header).base64(payload).signature');
88console.log('Access Token: krotki czas zycia (1h)');
89console.log('Refresh Token: dlugi czas zycia (7d)');
90Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Z ilu części składa się token JWT?
2. Co zawiera sekcja Payload tokenu JWT?
To 2 z 5 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij konfigurację JwtModule, ustawiając secret z ConfigService, czas wygaśnięcia na '24h' i issuer na 'LegionaryFleet'
- Układanie w poziomie
Ułóż części tokenu JWT w kolejności od lewej do prawej (tak jak występują w stringu)
- Edytor kodu
Uzupełnij klasę JwtStrategy, która dziedziczy po PassportStrategy(Strategy) i implementuje metodę validate sprawdzającą czy użytkownik istnieje
- Klikanie w kolejności
Ułóż elementy deklaracji klasy JwtStrategy w poprawnej kolejności
- Układanie w pionie
Uporządkuj kroki procesu logowania z użyciem JWT
- Układanie w poziomie
Ułóż składnię definiowania zmiennej JWT_SECRET w pliku .env
- Edytor kodu
Stwórz interfejs JwtPayload w TypeScript zawierający pola: sub (number), username (string), email (string), role (string), iat (number) i exp (number)