使用 Passport 在 NestJS 中实现 JWT 身份验证(第 1 部分)

第一步

`嵌套 g 资源认证`

这将进一步要求您进行选择

`❯ REST API

GraphQL(代码优先)

GraphQL(模式优先)

微服务(非 HTTP)

WebSockets`

选择 REST API,这将为您生成带有 dtos 服务控制器和模块的整个模块

注册用户

由于我们正在实施基于电子邮件/密码的身份验证,因此第一步我们将注册用户。

  • 首先验证以确保数据合法,并添加密码强度验证以减轻暴力攻击。
  • 之后进行清理以确保数据可以安全使用。
  • 检查用户记录是否已经存在于数据库中,如果存在,则意味着用户已经有帐户,因此发送响应,表示该电子邮件已注册。
  • 如果上述检查失败,则意味着我们需要注册一个用户,获取用户密码,并使用 bcrypt 或 argon2 等好的哈希库对其进行哈希处理
  • 散列后在数据库中插入用户记录。
  • 向用户发送电子邮件以验证电子邮件是否合法。
  • 添加路由速率限制以避免 DDoS 攻击
  • 1 验证传入数据

    由于 Nest js 与类验证器等推荐的验证包具有很强的集成性,但根据我以前的经验,我在 React js 前端中使用 Zod 进行大量验证,因此我找到了一个很棒的

    Nest js 生态系统的解决方案称为 Nest Zod,所以我现在更愿意使用这个。首先安装库

    `npm 和 nestjs-zod`

    import { createZodDto } from 'nestjs-zod';
    import { z } from 'zod';
    const passwordStrengthRegex =
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
    const registerUserSchema = z
      .object({
        email: z.string().email(),
        password: z
          .string()
          .min(8)
          .regex(
            passwordStrengthRegex,
            'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character',
          ),
        confirmPassword: z.string().min(8),
      })
      .refine((data) => data.password === data.confirmPassword, {
        message: 'Passwords do not match',
      });
    
    export class RegisterUserDto extends createZodDto(registerUserSchema) {}

    然后在路线上应用验证管道

    import { Controller, Post, Body, Version, UsePipes } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { RegisterUserDto } from './dto/register.dto';
    import { ZodValidationPipe } from 'nestjs-zod';
    
    @Controller('auth')
    export class AuthController {
      constructor(private readonly authService: AuthService) {}
    
      @Version('1')
      @Post()
      @UsePipes(ZodValidationPipe)
      async registerUser(@Body() registerUserDto: RegisterUserDto) {
        return await this.authService.registerUser(registerUserDto);
      }
    }

    如果我们提供所有正确的输入

    Image description

    我们已经完成了第一步

    让我们净化数据

    我们有三个输入

  • 密码:通常密码不应该被净化,因为它们永远不会被发送和显示到前端,即使有人向密码发送恶意脚本,最终它也会被散列,不需要
  • 确认密码:与上面相同
  • 电子邮件:是的,电子邮件会发送并呈现给客户端,因此必须对电子邮件字段进行清理,以减轻注入和脚本攻击
  • 但是我们明确添加了电子邮件:z.string().email(),这对于这种用例来说已经足够了

    Image description

    但为了增加额外的安全性,我们可以添加一个消毒层

    import { createZodDto } from 'nestjs-zod';
    import { z } from 'zod';
    import * as xss from 'xss'; 
    
    const passwordStrengthRegex =
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
    
    const registerUserSchema = z
      .object({
        email: z.string().transform((input) => xss.filterXSS(input)), // Sanitizing input using xss
        password: z
          .string()
          .min(8)
          .regex(
            passwordStrengthRegex,
            'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character',
          ),
        confirmPassword: z.string().min(8),
      })
      .refine((data) => data.password === data.confirmPassword, {
        message: 'Passwords do not match',
      });
    
    export class RegisterUserDto extends createZodDto(registerUserSchema) {}
    Image description

    这是一个我们再次添加的测试

    `电子邮件:z

    。细绳()

    .电子邮件()`

    步骤3,4,5

    import {
      BadRequestException,
      Injectable,
      InternalServerErrorException,
    } from '@nestjs/common';
    import { RegisterUserDto } from './dto/register.dto';
    import { PrismaService } from 'src/prismaModule/prisma.service';
    import * as argon2 from 'argon2';
    
    @Injectable()
    export class AuthService {
      constructor(private readonly prismaService: PrismaService) {}
      async registerUser(registerUserDto: RegisterUserDto) {
        // data is validate and sanitized by the registerUserDto
        const { email, password } = registerUserDto;
    
        try {
          // check if user already exists
          const user = await this.prismaService.user.findFirst({
            where: {
              email,
            },
          });
    
          if (user) {
            throw new BadRequestException('user already eists ');
          }
          //if use not exists lets hash user password
          const hashedPassword = await argon2.hash(registerUserDto.password);
    
          // time to create user
          const userData = await this.prismaService.user.create({
            data: {
              email,
              password: hashedPassword,
            },
          });
    
          if (!userData) {
            throw new InternalServerErrorException(
              'some thing went wrong while registring user',
            );
          }
    
          // if user is created successfully then  send email to user for email varification
          return {
            success: true,
            message: 'user created successfully',
          };
        } catch (error) {
          throw error;
        }
      }
    }

    需要注意的重点是,我刚刚返回了一条成功消息,但没有相关数据

    给用户 ID 或电子邮件,因为它不需要在此步骤中将数据发送回用户。注册后,用户将被重定向到登录页面以填写详细信息,因此避免发送不必要的数据是一种很好的安全做法

    Image description

    速率限制

    在 nestjs 中实现速率限制非常容易,只需安装 nestjs/throttler 并进行全局配置即可。

    要安装软件包,请运行“npm i --save @nestjs/throttler”

    @Module({
      imports: [
        ThrottlerModule.forRoot([{
          ttl: 60000,
          limit: 10,
        }]),
      ],
    })
    export class AppModule {}

    然后将 nestjs throttle guard 添加为全局守卫

    providers: [
        AppService,
    
        {
          provide: APP_GUARD,
          useClass: ThrottlerGuard,
        },
      ],

    下面是

    import { Controller, Post, Body, Version, UsePipes, Req } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { RegisterUserDto } from './dto/register.dto';
    import { ZodValidationPipe } from 'nestjs-zod';
    import { Throttle } from '@nestjs/throttler';
    
    @Controller('auth')
    export class AuthController {
      constructor(private readonly authService: AuthService) {}
      @Throttle({
        default: {
          ttl: 100000, // 1 minute
          limit: 5, // 5 requests per minute
        },
      })
      @Version('1')
      @Post()
      @UsePipes(ZodValidationPipe)
      async registerUser(@Body() registerUserDto: RegisterUserDto, @Req() req) {
        return await this.authService.registerUser(registerUserDto, req);
      }
    }

    由于注册用户端点是一个敏感端点,因此暴力破解

    否则可能会发生字典攻击,因此我们严格限制速率

    发送验证邮件

    用于向用户发送验证电子邮件,Resend 是一项非常易于使用的服务。但我决定为整个通知服务创建一个单独的章节,以便每个人都能更轻松地理解它