likes
comments
collection
share

Nest 登录注册模块

作者站长头像
站长
· 阅读数 33

前言

在学完 数据库,jwt 之后,我们可以试着用 Nest 来实现登录注册,用到的依赖有如下:

"@nestjs/jwt": "^10.1.0",   // jwt
"@nestjs/typeorm": "^10.0.0", // nest 操作 typeorm
"class-transformer": "^0.5.1",  // 管道校验参数
"class-validator": "^0.14.0", // 管道校验参数
"mysql2": "^3.6.0",   // 数据库
"typeorm": "^0.3.17"  // ORM框架操作数据库

步骤:

  1. 在 App 模块中,注入 typeormjwt 模块

  2. 创建 user 模块,并完善 用户表(user)实体(entity) 以及需要使用到的 DTO 类型

  3. 实现 user 模块的 /register/login 接口,并实现 service

  4. 创建**管道(Pipe)校验参数,创建路由守卫(Guard)**用于鉴权

  5. 处理成功的响应(Interceptor 拦截器)失败的响应(ExceptionFilter 异常处理)

1. 注入模块

// app.module.ts 文件

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'ip地址',
      port: 3306,
      username: '数据库账户',
      password: '数据库密码',
      database: '库名',
      synchronize: true,
      logging: true,
      entities: [User], // User模块的实体
      connectorPackage: 'mysql2', // 声明我们需要连接的数据库的包名,需要安装这个
      extra: {
        authPlugin: 'sha256_password',
      },
    }),

    JwtModule.register({
      secret: 'pengnima', // 盐
      global: true, // 声明为全局模块,之后就可以在所有模块中直接注入 jwtService
      signOptions: {
        expiresIn: '1d',
      },
    }),

    UserModule,
  ],
})
export class AppModule {}

2. 编写实体,DTO

// user.entity.ts 文件

@Entity()
export class User {
  @PrimaryGeneratedColumn({
    type: 'bigint',
    comment: '用户ID,自增',
  })
  id: number

  @Column({
    length: 50,
    comment: '用户昵称',
  })
  nick_name: string

  @Column({
    length: 20,
    comment: '用户名',
  })
  login_name: string

  @Column({
    length: 50,
    comment: '密码',
  })
  password: string

  @Column({
    comment: '创建时间',
    type: 'bigint',
    nullable: true,
  })
  create_time: number

  @Column({
    comment: '更新时间',
    type: 'bigint',
    nullable: true,
  })
  update_time: number

  // 要触发 BeforeInsert 装饰器,那么需要在 service 中通过 create 或者 new User 创建一个实体实例
  @BeforeInsert()
  setCreateTime() {
    const now = Date.now()

    this.create_time = this.create_time || now
    this.update_time = this.update_time || now
  }

  @BeforeUpdate()
  setUpdateTime() {
    this.update_time = Date.now()
  }
}

这里要注意的是,我使用 number 类型的时间戳 来存储时间的,且这 2 个时间应该是自动设置的,所以用了 @BeforeInsert@BeforeUpdate 这 2 个注解。

// create-user.dto.ts

export class CreateUserDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @IsString({ message: '用户名必须为字符串' })
  @Length(3, 50, { message: '用户名长度必须在3到50个字符之间' })
  readonly login_name: string

  @IsOptional()
  @IsString({ message: '昵称必须为字符串' })
  @Length(3, 50, { message: '昵称长度必须在3到50个字符之间' })
  readonly nick_name?: string

  @IsNotEmpty({ message: '密码不能为空' })
  @IsString({ message: '密码必须为字符串' })
  @Matches(/^\w{6,9}$/, {
    message: '密码为字母,数字,长度在6 - 9位之间',
  })
  readonly password: string
}

这里是 注册用户的 DTO 部分,用户只需要传递 login_namepassword 即可。这里的 nick_name 因为有 @IsOptional 装饰器,所以可以不传入,但如果传入,则需要满足 @IsString@Length 的条件。

3. 实现接口,service

// user.controller.ts
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  // 注册接口,管道校验 DTO 参数
  @Post('/register')
  async create(@Body(ValidatePipe) createUserDto: CreateUserDto) {
    return await this.userService.create(createUserDto)
  }

  @Post('/login')
  login(@Body(ValidatePipe) data: LoginDot) {
    return this.userService.login(data)
  }

  @Get('/list')
  @UseGuards(LoginGuard) // 路由守卫,鉴权
  findAll() {
    return this.userService.findAll()
  }
}

路由守卫管道校验 就是在接口的 Controller 层中使用,接下来就看看如何实现。

4. 创建管道,路由守卫

// validate.pipe.ts
@Injectable()
export class ValidatePipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const object = plainToInstance(metadata.metatype, value)
    const errors = await validate(object)

    if (errors.length > 0) {
      throw new BadRequestException({
        errors: (errors ?? [])?.map((v) => ({
          field: v.property,
          message: Object.values(v.constraints).join(','),
        })),
        message: '参数校验失败',
      })
    }
    return value
  }
}
// login.guard.ts

// 鉴权用
@Injectable()
export class LoginGuard implements CanActivate {
  @Inject(JwtService)
  private jwtService: JwtService

  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const req = context.switchToHttp().getRequest()
    const token = req.header('token')

    if (!token) {
      throw new UnauthorizedException('token 不存在')
    }

    try {
      const info = this.jwtService.verify(token)

      return true
    } catch (error) {
      throw new UnauthorizedException('token 已失效')
    }
  }
}

5. 响应处理,错误处理

// transform.interceptor.ts

// 成功响应的处理
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp()
    const res = ctx.getResponse()
    res.status(HttpStatus.OK)
    return next.handle().pipe(
      map((data) => {
        if (isString(data) || Array.isArray(data)) {
          return {
            data,
            code: 200,
            message: 'Success',
          }
        }

        const message = data?.message ? data?.message : 'Success'

        let newData = {
          ...data,
          message: undefined,
        }
        const values = Object.values(newData).filter(Boolean)
        if (values.length == 1) {
          newData = values?.[0]
        }
        return {
          data: newData,
          code: 200,
          message,
        }
      })
    )
  }
}
// exception.filter.ts

// 异常处理
@Catch()
export class HttpExceptionFilter<T> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {
    const ctx = host.switchToHttp()

    const response = ctx.getResponse<Response>()

    console.log({ exception })

    const isBadRequest = exception instanceof HttpException

    const code = isBadRequest ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR

    const message = isBadRequest ? exception.message : '网络异常,请稍后重试'

    const error = {
      code,
      message,
      errors: isBadRequest ? (exception.getResponse() as any)?.errors : (exception as any).driverError,
    }

    response.status(isBadRequest ? HttpStatus.OK : HttpStatus.INTERNAL_SERVER_ERROR)
    response.send(error)
  }
}

由于这 2 个是通用、统一的。所以可以直接在 main.ts 中注入成全局的

app.useGlobalInterceptors(new TransformInterceptor())
app.useGlobalFilters(new HttpExceptionFilter())

总结

从使用角度总结下流程就是:

调用接口时,需要用管道(Pipe)校验下参数,校验的规则写在参数的类型(DTO)中,然后有些接口还可以有守卫,有守卫的接口先执行守卫鉴权再执行管道校验

校验不通过 或者有代码逻辑错误,则触发 异常处理(HttpException)

校验通过,则继续下去,走到 service 的代码,通过 userService 去操作数据库,然后通过拦截器(Interceptor) 返回统一格式的数据。