如何在NestJS中实现JWT认证(附代码)

862 阅读7分钟

认证是任何应用程序的最重要方面之一。它通过在允许用户访问应用程序的不同部分之前对其进行验证,从而提高了应用程序的安全性。认证也使公司能够跟踪有多少人在使用他们的产品。

正确配置认证是非常重要的。事实上,开放网络应用安全项目(OWASP)在其十大网络应用安全风险列表中指出了识别和认证失败。

本教程将展示在NestJS中实现JWT用户认证的逐步过程。

前提条件

本教程是一个实战演示。要跟上进度,请确保你已经安装了以下设备:

  • Node.jsv14及以上版本
  • MongoDB
  • 全局安装了Yarn;使用命令npm install --global yarn

什么是NestJS?

NestJS是一个用于Node.js的服务器端应用程序框架,允许你创建可扩展和高效的应用程序。它是用TypeScript编写的,并与Express.js一起构建,Express.js是一个轻量级的框架,其本身非常棒,但缺乏结构。

Nest支持面向对象编程、函数式编程和功能性反应式编程。如果你想在你的应用程序的后端有很多结构,这个框架工作是一个很好的选择。

Nest的语法和结构与Angular(一个前端框架)相似。它也采用了TypeScript、服务和依赖注入,就像Angular一样。Nest使用模块和控制器,并允许你使用命令行界面来创建文件的控制器。

开始使用

要设置该项目,你首先需要用以下命令在全球范围内安装Nest CLI:

npm i -g @nestjs/cli

安装完成后,创建一个新项目,像这样:

nest new auth-with-nest

接下来,你会被提示选择一个软件包管理器来安装依赖性。对于这个演示,我们将使用Yarn。

Terminal Yarn

选择yarn ,然后按回车键。现在,等待Yarn安装运行应用程序所需的所有依赖项。

设置MongoDB数据库

为了设置和连接你的数据库,用以下命令安装Mongoose包、bcrypt和NestJS包装器:

npm install --save @nestjs/mongoose @types/bcrypt mongoose bcrypt

现在,更新app.module.ts 文件并设置Mongoose,像这样:

import { MongooseModule } from '@nestjs/mongoose';
@Module({
  imports: [MongooseModule.forRoot('mongodb://localhost/authentication')],
})

在上面的片段中,我们将MongooseModule 导入到根AppModule

创建用户模块

为了保持你的代码整洁和良好的组织,通过运行以下命令,为NestJS CLI用户专门创建一个模块:

nest g module users

上面的代码创建了一个用户文件夹,里面有一个users.module.ts 文件和一个app.module.ts 更新文件。

创建用户模式

要创建一个用户模式,在src/users 文件夹中创建一个users.model.ts 文件并添加以下代码:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
  @Prop()
  username: string;

  @Prop()
  password: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

在这里,我们已经用@Schema() 装饰器和@Prop() 装饰器定义了我们的User 模式的形状。

Mongoose将把该模式映射到MongoDB集合中。该模式定义了该集合的文档形状。

现在,用以下代码替换user/user.module.ts 文件中的代码,并使userSchema 在导入中可用:

import { Module } from '@nestjs/common';
import { UsersService } from './user.service';
import { UsersController } from './user.controller';
import { MongooseModule } from "@nestjs/mongoose"
import { UserSchema } from "./user.model"

@Module({
  imports: [MongooseModule.forFeature([{ name: "user", schema: UserSchema }])],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserModule {}

创建用户服务

在创建了用户模式之后,运行下面的命令来创建一个用户服务:

nest g service users

这段代码创建了一个users.service.ts 文件并更新了app.module.ts 文件。

注意。 你可以选择手动创建你的文件和文件夹,但NestJS CLI会自动更新必要的文件夹,使你的生活更轻松。

现在,在users.service.ts 文件中添加以下代码:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './users.model';

@Injectable()
export class UsersService {
    constructor(@InjectModel('user') private readonly userModel: Model<UserDocument>) { }
    async createUser(username: string, password: string): Promise<User> {
        return this.userModel.create({
            username,
            password,
        });
    }
    async getUser(query: object ): Promise<User> {
        return this.userModel.findOne(query);
    }
}

在这里,我们使用@InjectModel() 装饰器,将userModel 注入到UsersService

创建用户控制器

现在让我们创建一个用户控制器来定义API路由:

nest g service users

将代码添加到users.controller.ts 文件中:

import { Body, Controller, Post, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './users.model';
import * as bcrypt from 'bcrypt';

@Controller('auth')
export class UsersController {
    constructor(private readonly usersService: UsersService) { }

    @Post('/signup')
    async createUser(
        @Body('password') password: string,
        @Body('username') username: string,
    ): Promise<User> {
        const saltOrRounds = 10;
        const hashedPassword = await bcrypt.hash(password, saltOrRounds);
        const result = await this.usersService.createUser(
            username,
            hashedPassword,
        );
        return result;
    }
}

在这里,我们定义了两个API路由,并消耗了我们创建的服务。我们使用bcrypt ,对用户密码进行哈希处理。

创建auth模块

让我们首先创建一个 auth 模块,像这样:

nest g module auth

这个命令将创建一个新的文件夹,auth ,并有一个auth.module.ts 文件;它还将更新app.module.ts 文件。

配置JWT

现在,让我们实现一个JSON网络令牌来验证用户进入应用程序。

为了开始,请安装以下依赖项:

npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt

接下来,创建一个新文件,local.auth.ts ,并添加以下代码:

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

在这里,我们实现了一个护照-本地策略来验证JSON网络令牌。默认情况下,passport-local策略期望在请求体中有usernamepassword 属性。

我们还实现了validate() 方法,Passport中间件将调用该方法,使用适当的策略特定的参数集验证用户。

接下来,用以下内容替换AuthModule 中的代码:

import { Module } from "@nestjs/common"
import { UserModule } from "src/user/user.module";
import { AuthService } from "./auth.service"
import { PassportModule } from "@nestjs/passport"
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UsersService } from "src/user/user.service";
import { MongooseModule } from "@nestjs/mongoose"
import { UserSchema } from "../user/user.model"
import { LocalStrategy } from './local-strategy';


@Module({
  imports: [UserModule, PassportModule, JwtModule.register({
    secret: 'secretKey',
    signOptions: { expiresIn: '60s' },
  }), MongooseModule.forFeature([{ name: "user", schema: UserSchema }])],
  providers: [AuthService, UsersService, LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule { }

在这里,我们将PassportModuleJwtModule 导入到导入的数组中。然后我们使用register 方法来注册JWT,提供秘密和过期时间。

我们还在导入中提供了UserSchema ,并将UserService 和我们的LocalStrategy 添加到提供者阵列中。

注意事项为了安全起见,一定要把你的JWT秘密保存在环境变量中。

创建认证服务和控制器

现在,让我们在应用程序中添加认证功能。

配置好JWT和Passport后,运行以下命令,在auth 文件夹中创建auth.service.tsauth.controller.ts 文件:

nest generate service auth
nest generate controller auth

接下来,打开auth/auth.service.ts 文件,用以下代码验证用户:

import { Injectable, NotAcceptableException } from '@nestjs/common';
import { UsersService } from 'src/user/user.service';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
    constructor(private readonly usersService: UsersService, private jwtService: JwtService) { }
    async validateUser(username: string, password: string): Promise<any> {
        const user = await this.usersService.getUser({ username });
        if (!user) return null;
        const passwordValid = await bcrypt.compare(password, user.password)
        if (!user) {
            throw new NotAcceptableException('could not find the user');
        }
        if (user && passwordValid) {
            return user;
        }
        return null;
    }
    async login(user: any) {
        const payload = { username: user.username, sub: user._id };
        return {
            access_token: this.jwtService.sign(payload),
        };
    }
}

在这里,我们创建了validateUser 方法来检查来自user.model 的用户是否与数据库中的用户记录匹配。如果没有匹配,该方法返回一个null 的值。

我们还创建了login 方法,该方法使用jwtService.sign 方法为从我们的validate 中返回的用户生成一个JWT访问令牌LocalStrategy

现在,将下面的代码片段添加到auth/auth.controller.ts 文件中,为用户创建一个路由login

import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';

@Controller()
export class AuthController {
    constructor(private authService: AuthService) { }

    @UseGuards(AuthGuard('local'))
    @Post('auth/login')
    async login(@Request() req) {
        return this.authService.login(req.user);
    }
}

在这里,我们使用了@UseGuards() 装饰器,在用户请求登录路由时强制执行认证。有了AuthGuard 类,我们就可以使用local 策略来验证用户。

测试应用程序

现在让我们用Postman来测试这个应用程序。我们将从signup route

首先,启动该应用程序:

npm run start

接下来,打开Postman,通过向端点发送一个post请求来测试注册路线。localhost:3000/users/signup.

Postman Signup Route

现在,通过向端点发送一个帖子请求来测试登录端点。localhost:3000/auth/login.

Post Request Endpoint

如果数据库中存在usernamepassword ,用户将收到一个access_token ,如上所示。有了access_token ,用户将能够访问API中受保护的路由。

总结

在本教程中,我们提供了NestJS的概述,然后演示了如何在NestJS API上实现JWT用户认证。

现在你有了这些知识,你将如何在你的下一个Nest项目中处理用户认证?要了解更多关于NestJS JWT认证的信息,请参考官方文档