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.
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.
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.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.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.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.Legionista nosi przepustkę i nie podaje hasła przy każdej bramie:
JwtModule.registerAsync z useFactory pozwala pobrać secret z ConfigService; klucz mieszka w .env, nigdy w kodzie,signOptions.expiresIn ustala czas życia tokenu, issuer - wystawcę,sub (id użytkownika), iat (wystawiono), exp (wygasa),jwtService.sign(payload) wystawia przepustkę, a serwer niczego nie zapamiętuje,JwtStrategy: ExtractJwt.fromAuthHeaderAsBearerToken() czyta token z nagłówka Bearer, ignoreExpiration: false odrzuca przeterminowane, a validate() 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.