Пошук уроків, статей та іншого контенту
Створите модуль автентифікації в NestJS і розподілите відповідальність між контролерами, сервісами та стратегіями.
Модуль автентифікації відповідає за перевірку особи користувача та видачу облікових даних для подальших запитів.
У NestJS цю відповідальність зручно розподілити між кількома частинами:
контролер приймає HTTP-запити та повертає відповіді;
сервіс містить бізнес-логіку автентифікації;
стратегія описує, як перевіряти облікові дані, наприклад JWT-токен;
guard запускає стратегію перед виконанням захищеного обробника;
модуль об’єднує всі залежності та визначає, що доступно іншим модулям.
У цьому уроці створимо спрощений модуль автентифікації з JWT. Дані користувачів зберігатимуться в пам’яті, щоб зосередитися на структурі NestJS.
Зберігання користувачів у масиві та порівняння паролів у відкритому вигляді підходять лише для навчального прикладу. У реальному застосунку використовують базу даних і хешування паролів.
Для роботи JWT і Passport встановимо такі пакети:
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwtСтворимо такі файли:
src/
├── auth/
│ ├── auth.controller.ts
│ ├── auth.module.ts
│ ├── auth.service.ts
│ ├── jwt.strategy.ts
│ └── dto/
│ └── login.dto.ts
└── app.module.tsМодуль auth буде ізольованим: він міститиме контролер, сервіс і JWT-стратегію.
DTO описує дані, які клієнт надсилає під час входу.
src/auth/dto/login.dto.tsexport class LoginDto {
username: string;
password: string;
}Поки що DTO лише описує структуру запиту. Наприклад, клієнт має надіслати:
{
"username": "anna",
"password": "password123"
}У реальному застосунку DTO також можна доповнити валідаційними декораторами, але для цього прикладу достатньо опису полів.
Сервіс відповідає за бізнес-логіку:
пошук користувача;
перевірку логіна та пароля;
створення JWT;
пошук користувача за ідентифікатором після перевірки токена.
src/auth/auth.service.tsimport { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { LoginDto } from './dto/login.dto';
export interface AuthUser {
id: number;
username: string;
}
interface StoredUser extends AuthUser {
password: string;
}
@Injectable()
export class AuthService {
private readonly users: StoredUser[] = [
{
id: 1,
username: 'anna',
password: 'password123',
},
];
constructor(private readonly jwtService: JwtService) {}
validateCredentials(loginDto: LoginDto): AuthUser {
const user = this.users.find(
(item) =>
item.username === loginDto.username &&
item.password === loginDto.password,
);
if (!user) {
throw new UnauthorizedException('Неправильне ім’я користувача або пароль');
}
return this.removePassword(user);
}
async login(user: AuthUser) {
const payload = {
sub: user.id,
username: user.username,
};
return {
accessToken: await this.jwtService.signAsync(payload),
};
}
findById(id: number): AuthUser | undefined {
const user = this.users.find((item) => item.id === id);
return user ? this.removePassword(user) : undefined;
}
private removePassword(user: StoredUser): AuthUser {
return {
id: user.id,
username: user.username,
};
}
}Об’єкт користувача може містити пароль, але пароль не можна повертати у відповідях API або додавати до JWT.
Метод removePassword створює новий об’єкт лише з безпечними полями:
{
id: 1,
username: 'anna'
}У справжньому застосунку пароль не порівнюють напряму. Зазвичай у базі зберігають хеш і перевіряють його за допомогою спеціальної бібліотеки.
Контролер описує HTTP-маршрути:
POST /auth/login — вхід користувача;
GET /auth/profile — отримання профілю авторизованого користувача.
src/auth/auth.controller.tsimport {
Body,
Controller,
Get,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import type { Request } from 'express';
import { AuthGuard } from '@nestjs/passport';
import { AuthService, AuthUser } from './auth.service';
import { LoginDto } from './dto/login.dto';
type AuthenticatedRequest = Request & {
user: AuthUser;
};
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(@Body() loginDto: LoginDto) {
const user = this.authService.validateCredentials(loginDto);
return this.authService.login(user);
}
@Get('profile')
@UseGuards(AuthGuard('jwt'))
getProfile(@Req() request: AuthenticatedRequest) {
return request.user;
}
}Метод login не повинен самостійно:
шукати користувача в базі;
перевіряти пароль;
створювати JWT.
Це завдання сервісу. Контролер лише отримує дані запиту, викликає сервіс і повертає результат.
Декоратор @UseGuards(AuthGuard('jwt')) захищає маршрут profile. Перед викликом getProfile NestJS запустить JWT-стратегію.
Стратегія визначає, як перевірити токен і що зробити після успішної перевірки.
src/auth/jwt.strategy.tsimport { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService, AuthUser } from './auth.service';
interface JwtPayload {
sub: number;
username: string;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET ?? 'dev-secret',
});
}
validate(payload: JwtPayload): AuthUser {
const user = this.authService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException('Користувача не знайдено');
}
return user;
}
}ExtractJwt.fromAuthHeaderAsBearerToken() шукає токен у заголовку:
Authorization: Bearer <token>Passport перевіряє:
чи має токен правильний формат;
чи підписаний він правильним секретом;
чи не завершився його термін дії.
Після цього NestJS викликає метод validate. Значення, яке повертає validate, буде доступне як request.user у захищеному контролері.
У нашому прикладі validate шукає користувача за значенням sub з JWT payload.
Модуль з’єднує контролер, сервіс, JWT і стратегію.
src/auth/auth.module.tsimport { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET ?? 'dev-secret',
signOptions: {
expiresIn: '1h',
},
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}@Moduleimports — модулі, чиї залежності потрібні поточному модулю;
controllers — контролери цього модуля;
providers — сервіси та стратегії, якими керує NestJS;
exports — провайдери, доступні іншим модулям.
JwtModule потрібен для створення токенів через JwtService.
PassportModule потрібен для роботи AuthGuard('jwt').
JwtStrategy додається до providers, тому NestJS зможе створити її та зареєструвати стратегію в Passport.
src/app.module.tsimport { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [AuthModule],
})
export class AppModule {}Після цього маршрути модуля будуть доступні в застосунку:
POST /auth/login
GET /auth/profileЗапустіть застосунок:
npm run start:devВиконайте вхід:
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"anna","password":"password123"}'У відповіді буде JWT:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs..."
}Скопіюйте значення accessToken і використайте його для захищеного маршруту:
curl http://localhost:3000/auth/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Очікувана відповідь:
{
"id": 1,
"username": "anna"
}Якщо не передати заголовок Authorization або передати неправильний токен, NestJS поверне помилку авторизації.
Клієнт
↓
POST /auth/login
↓
AuthController
↓
AuthService.validateCredentials
↓
AuthService.login
↓
JWT-токенКонтролер приймає логін і пароль, сервіс перевіряє їх і створює токен.
Клієнт із Bearer-токеном
↓
GET /auth/profile
↓
JwtAuthGuard
↓
JwtStrategy
↓
AuthService.findById
↓
AuthController.getProfileGuard не містить логіки пошуку користувача. Він запускає стратегію, а стратегія делегує пошук сервісу.
Контролер повинен:
приймати HTTP-запити;
читати параметри, заголовки та тіло запиту;
викликати потрібний сервіс;
повертати результат.
Контролер не повинен містити складну логіку автентифікації.
Сервіс повинен:
перевіряти облікові дані;
працювати з репозиторієм або базою даних;
створювати токени;
знаходити користувача;
приховувати внутрішню реалізацію від контролера та стратегії.
Стратегія повинна:
отримати дані з токена;
перевірити підпис і термін дії через Passport;
знайти користувача;
повернути користувача для request.user.
Guard визначає, чи може запит виконати захищений маршрут.
У цьому прикладі використовується стандартний:
@UseGuards(AuthGuard('jwt'))Він пов’язує маршрут із JWT-стратегією.
Модуль:
реєструє залежності;
підключає JWT і Passport;
визначає контролери та провайдери;
за потреби експортує сервіс для інших модулів.
providersЯкщо JwtStrategy не зареєстрована в модулі, Passport не зможе її використати.
Правильно:
@Module({
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}Секрет для створення токена і секрет для його перевірки мають бути однаковими.
У прикладі обидві частини використовують:
process.env.JWT_SECRET ?? 'dev-secret'Для production-застосунку потрібно використовувати змінну середовища, а не значення за замовчуванням.
BearerТокен потрібно надсилати з префіксом Bearer:
Authorization: Bearer <token>Заголовок без цього префікса стратегія не розпізнає.
Не повертайте об’єкт із полем password. Навіть якщо пароль зберігається в хешованому вигляді, він не повинен потрапляти до відповіді API або JWT payload.
Такий код ускладнює тестування та повторне використання:
// Невдалий підхід: контролер сам перевіряє користувача
@Post('login')
login(@Body() body: LoginDto) {
// пошук користувача та перевірка пароля тут
}Краще передати цю відповідальність AuthService.
У NestJS модуль автентифікації можна організувати так:
AuthController працює з HTTP-маршрутами;
AuthService містить логіку перевірки користувача та створення токена;
JwtStrategy перевіряє JWT і повертає авторизованого користувача;
AuthGuard('jwt') захищає маршрути;
AuthModule реєструє всі залежності та об’єднує компоненти.
Такий розподіл відповідальності робить код зрозумілим: кожна частина має одну основну роль, а зміни в одному компоненті не вимагають переписування всього модуля.