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.
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.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ę, @name: pole, które nigdy nie ma opuścić serwera, oznaczasz @Exclude() w chwili, gdy je tworzysz, a nie kiedyś później.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.
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.Brama stoi, rejestr działa, rekruci zapisują się bezpiecznie:
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żdym GET,@IsNotEmpty, potem @IsEmail, potem @MinLength,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.