Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Вход по токену 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', и он попадает в репозиторий. Любой, кто его прочитает, сможет выпускать токены от имени администратора. Храните секрет только в переменной окружения, делайте его длинным и случайным и разным на компьютере разработчика и на сервере.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.