Kurs NestJS · Moduł 4: Uwierzytelnianie
Authentication - identyfikacja legionariuszy
W tej lekcji5
Do bramy obozu podchodzi człowiek w rzymskiej zbroi. Zbroja niczego nie dowodzi - można ją kupić albo zdjąć z poległego. Zanim wartownik go wpuści, musi odpowiedzieć na pytanie: kim ty właściwie jesteś?
To pytanie nazywamy uwierzytelnieniem (ang. authentication). Nie myl go z pytaniem, które padnie później, przy drzwiach do skarbca: co ci wolno? - to autoryzacja (authorization). Kolejność jest nieodwracalna: najpierw ustalamy tożsamość, dopiero potem uprawnienia. Nie da się sprawdzić rangi kogoś, kogo jeszcze nie rozpoznaliśmy.
W tym module zbudujemy całą bramę. Ta lekcja to jej fundament: rejestr legionu i bezpieczne przyjmowanie rekrutów.
Pakiety - wyposażenie strażnicy
Zacznijmy od narzędzi, które będą nam potrzebne w całym module:
1npm install @nestjs/passport passport passport-jwt @nestjs/jwtpassport to sama biblioteka uwierzytelniania, @nestjs/passport spina ją z NestJS. @nestjs/jwt wystawia przepustki, a passport-jwt uczy strażnika je czytać. Na razie tylko je instalujemy - poznasz je po kolei w kolejnych lekcjach.
Rejestr legionu - encja User
Każdy legionista musi mieć wpis w rejestrze:
1@Entity('users')
2export class User {
3 @PrimaryGeneratedColumn()
4 id: number;
5
6 @Column({ unique: true })
7 username: string;
8
9 @Column({ unique: true })
10 email: string;
11
12 @Column()
13 @Exclude()
14 password: string;
15}Encję czytasz już swobodnie - to wiedza z modułu o TypeORM. unique: true przy username i email gwarantuje, że dwóch legionistów nie zamelduje się pod tym samym imieniem.
Zatrzymajmy się przy @Exclude(), bo to jedyny nowy element i zarazem najczęstsza dziura w bezpieczeństwie początkujących aplikacji. Ten dekorator nie usuwa pola z bazy ani niczego nie szyfruje - hasło nadal siedzi w kolumnie. On działa dopiero przy wyjściu: gdy NestJS zamienia encję na odpowiedź JSON, pole oznaczone @Exclude() zostaje pominięte.
Bez niego zwykłe GET /users/1 odesłałoby klientowi hash hasła razem z resztą danych. Nikt tego nie zauważy w testach, bo aplikacja działa poprawnie - a hasła wyciekają przy każdym żądaniu. Zapamiętaj tę zasadę: pole, które nigdy nie ma opuścić serwera, oznaczasz @Exclude() w chwili, gdy je tworzysz, a nie kiedyś później.
Sprawdzenie dokumentów przy wejściu - DTO
Zanim dane trafią do rejestru, trzeba je obejrzeć. Służy do tego DTO z regułami walidacji:
1export class RegisterDto {
2 @IsNotEmpty()
3 username: string;
4
5 @IsNotEmpty()
6 @IsEmail()
7 email: string;
8
9 @IsNotEmpty()
10 @MinLength(8)
11 password: string;
12}Zwróć uwagę na porządek sprawdzeń - idzie od najbardziej podstawowego do najbardziej szczegółowego. Najpierw @IsNotEmpty() pyta, czy pole w ogóle wypełniono. Potem @IsEmail() bada, czy to, co wpisano, ma kształt adresu. Na końcu @MinLength(8) stawia wymóg co do treści.
Ta kolejność ma sens praktyczny: nie ma po co badać formatu pustego pola. I jeszcze jedno rozróżnienie, bo bywa mylące. Wszystkie te dekoratory sprawdzają samo żądanie - patrzą tylko na to, co przyszło, i nie wiedzą nic o bazie. Pytanie "czy ten username jest już zajęty?" wymaga zajrzenia do rejestru, więc nie da się go zadać dekoratorem. To sprawdzenie należy do serwisu i zaraz je zobaczysz.
Przyjęcie rekruta
Rejestracja to cztery kroki w ustalonej kolejności:
1async register(dto: RegisterDto): Promise<User> {
2 const existing = await this.userRepository.findOne({
3 where: { username: dto.username },
4 });
5
6 if (existing) {
7 throw new ConflictException('Legionista o tym imieniu już służy');
8 }
9
10 const hashedPassword = await bcrypt.hash(dto.password, 10);
11
12 return this.userRepository.save({
13 ...dto,
14 password: hashedPassword,
15 });
16}Prześledźmy je. Walidacja wykonała się wcześniej, automatycznie - dane z DTO doszły tu już sprawdzone. Sprawdzenie unikalności to to zapytanie do bazy, którego dekorator nie mógł wykonać; przy kolizji rzucamy ConflictException, czyli odpowiedź 409. Zahaszowanie hasła - i dopiero teraz zapis.
Kolejność dwóch ostatnich kroków jest nienegocjowalna: hasło zamieniamy na hash przed zapisem, nigdy po. Do bazy nie trafia nic, co dałoby się odczytać.
Czym właściwie jest to bcrypt.hash(dto.password, 10)? Hashowanie to przemiana jednokierunkowa - z hasła powstaje ciąg znaków, z którego nie da się wrócić do oryginału. To nie jest szyfrowanie, bo szyfrowanie z definicji można odwrócić kluczem. Przy logowaniu nie odszyfrowujemy więc niczego; hashujemy podane hasło jeszcze raz i porównujemy wyniki metodą bcrypt.compare(). Nawet Ty, mając pełen dostęp do bazy, nie odczytasz hasła swojego użytkownika - i o to właśnie chodzi.
Liczba 10 to salt rounds, sterujące kosztem obliczenia. Wrócimy do niej i do reszty mechaniki bcrypta w osobnej lekcji o hashowaniu.
Podsumowanie
Brama stoi, rejestr działa, rekruci zapisują się bezpiecznie:
- uwierzytelnienie odpowiada, kim jest przybysz; autoryzacja - co mu wolno; ta pierwsza zawsze poprzedza drugą,
- moduł opiera się na czterech pakietach:
passport,@nestjs/passport,@nestjs/jwt,passport-jwt, @Exclude()nie rusza bazy - zapobiega tylko wysłaniu pola w odpowiedzi API; bez niego hasła wyciekają przy każdymGET,- walidacja DTO idzie od ogółu do szczegółu:
@IsNotEmpty, potem@IsEmail, potem@MinLength, - dekoratory widzą wyłącznie treść żądania - sprawdzenie unikalności wymaga zapytania do bazy i należy do serwisu,
- rejestracja ma cztery kroki: walidacja, sprawdzenie istnienia, hashowanie, zapis,
- hash jest jednokierunkowy: hasła nie odszyfrowujemy, tylko hashujemy ponownie i porównujemy przez
bcrypt.compare().
W następnej lekcji rozbierzemy hashowanie na czynniki pierwsze - dowiesz się, czym jest salt i dlaczego wolne hashowanie bywa zaletą. A na razie zapamiętaj: uwierzytelnienie pyta "kim jesteś", a hasło w rejestrze legionu nie istnieje - jest tylko jego jednokierunkowy odcisk.
Kod do tej lekcji: src/auth/auth-basics.ts
1// Authentication - Identyfikacja Legionariuszy
2// Imperium Rzymskie - system weryfikacji tozsamosci
3import { Injectable, UnauthorizedException } from '@nestjs/common';
4import { JwtService } from '@nestjs/jwt';
5import * as bcrypt from 'bcrypt';
6
7// Interfejs uzytkownika w systemie Imperium
8interface Legionary {
9 id: string;
10 username: string;
11 passwordHash: string;
12 rank: 'miles' | 'centurion' | 'legatus' | 'consul';
13}
14
15@Injectable()
16export class AuthService {
17 // Symulacja bazy legionariuszy
18 private legionaries: Legionary[] = [];
19
20 constructor(private jwtService: JwtService) {}
21
22 // Rejestracja nowego legionariusza
23 async register(username: string, password: string) {
24 // Hashowanie hasla - jak pieczec woskowa na dokumencie
25 const saltRounds = 10;
26 const passwordHash = await bcrypt.hash(password, saltRounds);
27
28 const newLegionary: Legionary = {
29 id: Date.now().toString(),
30 username,
31 passwordHash,
32 rank: 'miles', // Nowy rekrut
33 };
34
35 this.legionaries.push(newLegionary);
36 console.log('Nowy legionariusz zarejestrowany:', username);
37
38 return { message: 'Rejestracja zakonczona pomyslnie' };
39 }
40
41 // Walidacja legionariusza - sprawdzenie tozsamosci
42 async validateUser(username: string, password: string) {
43 const legionary = this.legionaries.find(l => l.username === username);
44
45 if (!legionary) {
46 throw new UnauthorizedException('Nieznany legionariusz!');
47 }
48
49 // Porownanie hasla z hashem
50 const isPasswordValid = await bcrypt.compare(
51 password,
52 legionary.passwordHash
53 );
54
55 if (!isPasswordValid) {
56 throw new UnauthorizedException('Bledne haslo!');
57 }
58
59 return legionary;
60 }
61
62 // Logowanie - wydanie przepustki (tokenu)
63 async login(username: string, password: string) {
64 const legionary = await this.validateUser(username, password);
65
66 // Payload tokenu JWT
67 const payload = {
68 sub: legionary.id,
69 username: legionary.username,
70 rank: legionary.rank,
71 };
72
73 return {
74 access_token: this.jwtService.sign(payload),
75 legionary: {
76 id: legionary.id,
77 username: legionary.username,
78 rank: legionary.rank,
79 },
80 };
81 }
82}
83
84console.log('Authentication: register -> validate -> login');
85console.log('Hasla sa hashowane bcryptem - nigdy nie przechowuj tekstem!');
86Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym jest Authentication w kontekście aplikacji webowej?
2. Jaka jest różnica między Authentication a Authorization?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij kod instalacji @nestjs/passport, passport, passport-jwt i @nestjs/jwt do projektu NestJS
- Klikanie w kolejności
Ułóż elementy definicji encji User w poprawnej kolejności
- Układanie w pionie
Uporządkuj kroki walidacji danych rejestracji od najbardziej podstawowego do szczegółowego
- Edytor kodu
Uzupełnij serwis AuthService o metodę hashPassword, która przyjmuje hasło i zwraca zahashowaną wersję za pomocą bcrypt
- Układanie w pionie
Uporządkuj etapy procesu rejestracji użytkownika w NestJS