Вход по токену JWT
API не хранит сессий: каждый запрос должен сам доказать, кто его отправил. Для этого при входе сервер выдаёт токен JWT (JSON Web Token) — строку, в которой записан номер пользователя и которая подписана секретным ключом сервера. Клиент отправляет токен в заголовке каждого запроса, а гард проверяет подпись. Подделать токен без ключа нельзя.
Установка и модуль
npm i @nestjs/jwt
nest g module auth
nest g service auth
nest g controller auth// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
imports: [
UsersModule,
JwtModule.registerAsync({
global: true,
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.getOrThrow<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}Секрет берётся из .env (переменная JWT_SECRET из урока о настройках), токен действует один час. global: true делает JwtService доступным во всех модулях.
Выдача токена
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async login(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new UnauthorizedException('Неверная почта или пароль');
}
const payload = { sub: user.id, email: user.email, role: user.role };
return { accessToken: await this.jwtService.signAsync(payload) };
}
}payload — данные, которые попадут в токен. Поле sub (subject) по соглашению хранит номер пользователя. Пароль в токен класть нельзя: содержимое JWT может прочитать любой.
// src/auth/auth.controller.ts
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@HttpCode(200)
login(@Body() dto: LoginDto) {
return this.authService.login(dto.email, dto.password);
}
}LoginDto содержит поля email (@IsEmail()) и password (@IsString()). Запрос POST /auth/login с верными данными вернёт {"accessToken":"eyJhbGciOi..."}.
Гард, проверяющий токен
// src/auth/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const [type, token] = request.headers.authorization?.split(' ') ?? [];
if (type !== 'Bearer' || !token) {
throw new UnauthorizedException();
}
try {
request['user'] = await this.jwtService.verifyAsync(token);
} catch {
throw new UnauthorizedException();
}
return true;
}
}Токен приходит в заголовке Authorization: Bearer <токен>. verifyAsync() проверяет подпись и срок действия и возвращает payload, который гард сохраняет в request.user.
Защита маршрута
Поставьте @UseGuards(AuthGuard) над методом create() в PostsController вместо гарда с ключом API. Теперь статью создаст только вошедший пользователь:
curl -X POST http://localhost:3000/posts \
-H "Authorization: Bearer eyJhbGciOi..." \
-H "Content-Type: application/json" \
-d '{"title":"С токеном","content":"Работает"}'Без заголовка или с истёкшим токеном придёт 401.
Частая ошибка
Секрет JWT записывают прямо в код, например secret: 'secret', и он попадает в репозиторий. Любой, кто его прочитает, сможет выпускать токены от имени администратора. Храните секрет только в переменной окружения, делайте его длинным и случайным и разным на компьютере разработчика и на сервере.