Nest.js 系列——在 Nest.js 中使用JWT

前言

在接口调用中,我们经常会遇到需要用户登录的情况,这时候一般比较常用的就是 jwt 了。在 nest 中,可以使用 nest 提供的 jwt 模块来实现 jwt 的功能。来看看如何使用吧。

jwt

首先在 nest 中使用 jwt,需要先安装@nestjs/jwt 这个包

bash 复制代码
pnpm add @nestjs/jwt

安装完成后,我们需要在 app.module.ts 中引入 JwtModule 模块

typescript 复制代码
import { Module } from '@nestjs/common'
import { JwtModule } from '@nestjs/jwt'
import { AppController } from './app.controller'
import { AppService } from './app.service'

@Module({
  imports: [
    JwtModule.register({
      secret: 'water',
      signOptions: {
        expiresIn: '7d'
      }
    })
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

在这里我们使用了 register 方法来注册 jwt 模块,register 方法接收一个对象,对象中有两个属性,一个是 secret,一个是 signOptions。secret 是用来加密的,signOptions 是用来设置 token 的过期时间的。在这里我们设置了 7 天过期。当然还可以使用另外一种方式注册 jwt 模块,如下:

typescript 复制代码
import { Module } from '@nestjs/common'
import { JwtModule } from '@nestjs/jwt'
import { AppController } from './app.controller'
import { AppService } from './app.service'

@Module({
  imports: [
    JwtModule.registerAsync({
      async useFactory() {
        return {
          secret: 'water',
          signOptions: {
            expiresIn: '7d'
          }
        }
      }
    })
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

配置的参数和上面的一样,只是使用了 registerAsync 方法来注册 jwt 模块。registerAsync 方法接收一个对象,对象中有一个属性 useFactory,useFactory 是一个函数,函数中返回一个对象,对象中有两个属性,一个是 secret,一个是 signOptions。secret 是用来加密的,signOptions 是用来设置 token 的过期时间的。在这里我们设置了 7 天过期。

这样 jwt 模块就注册好了,接下来看如何使用

使用生成 token

在控制器中使用 jwt 模块,需要先引入 JwtService

typescript 复制代码
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common'
import { AppService } from './app.service'
import { JwtService } from '@nestjs/jwt'

@Controller()
export class AppController {
  constructor(private readonly appService: AppService, private readonly jwtService: JwtService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello()
  }
}

然后在控制器中使用 jwtService 的 sign 方法来生成 token

typescript 复制代码
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common'
import { AppService } from './app.service'
import { JwtService } from '@nestjs/jwt'

@Controller()
export class AppController {
  constructor(private readonly appService: AppService, private readonly jwtService: JwtService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello()
  }

  @Post('login')
  login(@Body() body) {
    const token = this.jwtService.sign({
      username: body.username,
      password: body.password
    })
    return {
      token
    }
  }
}

启动下生成 token 的测试项目,然后调用下 login 接口,可以看到返回了一个 token,如下:

使用验证 token

在接口调用的过程中,带上上面生成的 token,然后在控制器中使用 jwtService 的 verify 方法来验证 token

typescript 复制代码
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common'
import { AppService } from './app.service'
import { JwtService } from '@nestjs/jwt'

@Controller()
export class AppController {
  constructor(private readonly appService: AppService, private readonly jwtService: JwtService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello()
  }

  @Post('login')
  login(@Body() body) {
    const token = this.jwtService.sign({
      username: body.username,
      password: body.password
    })
    return {
      token
    }
  }

  @Get('profile')
  @UseGuards(AuthGuard('jwt'))
  getProfile(@Request() req) {
    return req.user
  }
}

在这里我们使用了 UseGuards 装饰器来使用 jwt 的验证,AuthGuard 的参数是 jwt,表示使用 jwt 的验证。然后在 getProfile 方法中使用 @Request() req 来获取用户信息。启动下验证 token 的测试项目,然后调用下 profile 接口。其实 AuthGuard('jwt')这个装饰器就是使用了 nest 提供的 AuthGuard 类,然后传入了 jwt,来看下 AuthGuard 类的源码

typescript 复制代码
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { JwtService } from '@nestjs/jwt'
import { Observable } from 'rxjs'
import { IS_PUBLIC_KEY } from '../decorators/public.decorator'

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector, private readonly jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler())
    if (isPublic) {
      return true
    }
    const request = context.switchToHttp().getRequest()
    const authHeader = request.headers.authorization
    if (!authHeader) {
      throw new UnauthorizedException('请登录')
    }
    const token = authHeader.split(' ')[1]
    try {
      const user = this.jwtService.verify(token)
      request.user = user
      return true
    } catch (error) {
      throw new UnauthorizedException('请登录')
    }
  }
}

通过封装的 AuthGuard 类,我们可以很方便的使用 jwt 的验证了。内部逻辑还是使用了 jwtService 的 verify 方法来验证 token 的。

使用 typeorm 结合 mysql 使用 jwt 实践登录注册功能

新建一个数据库 login_jwt,然后在新建一个 demo 项目。

bash 复制代码
nest new login-jwt -p pnpm

然后在项目中安装 typeorm 和 mysql2

bash 复制代码
pnpm add @nestjs/typeorm typeorm mysql2

然后在项目中配置好 typeorm

typescript 复制代码
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { AppController } from './app.controller'
import { AppService } from './app.service'

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '123456',
      database: 'login_jwt',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
      logging: true,
      poolSize: 10,
      connectorPackage: 'mysql2'
    })
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

然后在项目中创建一个 user 模块

bash 复制代码
nest g resource user

修改 user.entity.ts 实体的字段

typescript 复制代码
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column({
    length: 50,
    comment: '用户名'
  })
  username: string

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

  @CreateDateColumn({
    comment: '创建时间'
  })
  create_time: Date

  @UpdateDateColumn({
    comment: '更新时间'
  })
  update_time: Date
}

重新启动下项目就能看到 user 表了。

在 user 模块中使用 typeorm 的 Repository 来操作数据库,先在 user.module.ts 中引入 User 实体并注册到 typeorm 中

typescript 复制代码
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { UserController } from './user.controller'
import { UserService } from './user.service'
import { User } from './user.entity'

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService]
})
export class UserModule {}

然后在 user.service.ts 中使用 Repository 来操作数据库

typescript 复制代码
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './user.entity'
import { LoginDto } from './dto/login.dto'
import { RegisterDto } from './dto/register.dto'

@Injectable()
export class UserService {
  constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) {}

  async register(register) {
    return await this.userRepository.findOne(register)
  }

  async login(login) {
    return await this.userRepository.save(login)
  }
}

然后在 user.controller.ts 中使用 UserService 来操作数据库

typescript 复制代码
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common'
import { UserService } from './user.service'
import { JwtService } from '@nestjs/jwt'

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService, private readonly jwtService: JwtService) {}

  @Post('register')
  async register(@Body() registerDto: RegisterDto) {
    const user = await this.userService.register(registerDto)
  }

  @Post('login')
  async login(@Body() loginDto: LoginDto) {
    const user = await this.userService.login(loginDto)
  }
}

在 dto 文件夹中添加 login 和 register 的 dto 文件

typescript 复制代码
export class LoginDto {
  readonly username: string
  readonly password: string
}
ts 复制代码
export class RegisterDto {
  readonly username: string
  readonly password: string
}

上面的 user.service.ts 中只是定义了方法并没有具体实现,那么就先实现下注册逻辑,实现注册功能,然后在实现登录功能。

typescript 复制代码
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './user.entity'
import { LoginDto } from './dto/login.dto'
import { RegisterDto } from './dto/register.dto'
import * as crypto from 'crypto'

// 先定义一个md5加密的方法
function md5(s: string) {
  return crypto.createHash('md5').update(s).digest('hex')
}

@Injectable()
export class UserService {
  constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) {}

  async register(register: RegisterDto): Promise<User> {
    const findUser = await this.userRepository.findOneBy({ username: register.username })

    if (findUser) {
      throw new Error('用户已存在')
    }

    const newUser = new User()
    newUser.username = register.username
    newUser.password = md5(register.password)

    try {
      await this.userRepository.save(newUser)
      return '注册成功'
    } catch (error) {
      throw new Error('注册失败')
    }

    // const user = await this.userRepository.create(register)
    // return await this.userRepository.save(user)
  }
}

使用 postman 调试下注册接口看下返回结果

看下数据库中的 user 表数据和 sql

如果在注册同一个用户就会提示用户已存在

然后实现下登录逻辑,登录逻辑就是先根据用户名查找用户,然后判断密码是否正确,如果正确就返回 token,如果不正确就提示密码错误。完善下上面的 login 控制器

typescript 复制代码
@Post('login')
  async login(@Body() loginDto: LoginDto) {
    const user = await this.userService.login(loginDto)
  }

然后在 user.service.ts 中实现登录逻辑

typescript 复制代码
async login(loginDto: LoginDto): Promise<User> {
    const findUser = await this.userRepository.findOneBy({ username: loginDto.username })
    if (!findUser) {
      throw new HttpException('用户不存在',200)
    }
    if (findUser.password !== md5(loginDto.password)) {
      throw new HttpException('密码错误',200)
    }
    return findUser
  }

使用 postman 调试下登录接口看下返回结果

可以看到登录的时候从数据库中查到这个用户的信息。并直接返回了这条数据,但是正常不会直接返回数据的,而是 token,所以需要在登录的时候返回 token。在登录的时候返回 token,需要在 user.module.ts 中引入 JwtModule 模块

安装 @nestjs/jwt

bash 复制代码
pnpm add @nestjs/jwt

然后在 app.module.ts 中引入 JwtModule 模块

typescript 复制代码
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { JwtModule } from '@nestjs/jwt'
import { AppController } from './app.controller'
import { AppService } from './app.service'

@Module({
  imorts:[
      TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
        password: '123456',
        database: 'login_jwt',
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: true,
        logging: true,
        poolSize: 10,
        connectorPackage: 'mysql2',
      }),
      JwtModule.register({
        global:true,
        secret: 'water',
        signOptions: {
          expiresIn: '7d'
        }
      })
  ]
})

在 user.controller.ts 引入 JwtService 并在登录成功的时候生成 token

typescript 复制代码
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common'
import { UserService } from './user.service'
import { JwtService } from '@nestjs/jwt'

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService, private readonly jwtService: JwtService) {}

  @Post('login')
  async login(@Body() loginDto: LoginDto) {
    const findUser = await this.userService.login(loginDto)

    if (findUser) {
      const token = this.jwtService.sign({
        username: findUser.username,
        password: findUser.password
      })
      return {
        token
      }
    } else {
      throw new HttpException('密码错误', 200)
    }
  }
}

使用 postman 重新调试下登录接口看下返回结果

可以看到返回了 token,然后在登录的时候返回 token。如果想在接口上加接口的 token 鉴权,那就和上面封装一个守卫类似,具体逻辑就是取出接口请求中的 token 进行验证。

小结

从使用 jwt 生成 token 到配合 mysql 进行登录注册的练习,学习了如何在 nest 中使用 token 的方式,希望对你有帮助。

相关推荐
欧宸雅几秒前
HTML语言的空值合并
开发语言·后端·golang
方瑾瑜25 分钟前
Visual Basic语言的物联网
开发语言·后端·golang
赖皮猫44 分钟前
PIKIE-RAG 本地部署实践
后端·python·flask
Asthenia04121 小时前
面试回顾:Java RMI 问题解析(续)
后端
无名之逆1 小时前
[特殊字符] Hyperlane 框架:高性能、灵活、易用的 Rust 微服务解决方案
运维·服务器·开发语言·数据库·后端·微服务·rust
Asthenia04121 小时前
面试回顾:Java RMI 问题解析
后端
uhakadotcom1 小时前
Python 中的 @staticmethod 和 @classmethod 详解
后端·面试·github
uhakadotcom2 小时前
单点登录的两大核心技术:SAML和OIDC
后端·面试·github
Asthenia04122 小时前
正则表达式详解与 Java 实践-预定义字符类/重复类/反义类/分组/零宽断言
后端
慕离桑2 小时前
SQL语言的物联网
开发语言·后端·golang