Vue3 + Nest 实现权限管理系统 后端篇(二):使用 JWT 实现注册与登录

简介: Vue3 + Nest 实现权限管理系统 后端篇(二):使用 JWT 实现注册与登录

上一篇文章已经对 Nest 做了一个基本的配置,本篇文章将带领大家完成一个小的实战,使用 JWT 实现注册登录的接口。(PS:最近更新可能不会那么频繁,因为我在学习大佬刚出的小册Nest 通关秘籍),等我学完了再给大家继续更新这一个专栏,当然,如果你也想从零学习 Nest 的话也可以看一下Nest 通关秘籍


注册



先看一下注册的实现,先用nest的命令创建一个 user 模块

nest g res user

然后就生成了 user 模块,在它的实体中创建一个用户表user.entity.ts,包含 uuid,用户名,密码,头像,邮箱等等一些字段,这里便于测试先将除了用户名和密码的其它字段都设置为可以为空的状态

import {
  Column,
  Entity,
  PrimaryGeneratedColumn,
  BeforeInsert,
  BeforeUpdate,
} from 'typeorm';
@Entity('user')
export class User import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('user')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: number; // 标记为主键,值自动生成
  @Column({ length: 30 })
  username: string; //用户名
  @Column({ nullable: true })
  nickname: string; //昵称
  @Column()
  password: string; //密码
  @Column({ nullable: true })
  avatar: string; //头像
  @Column({ nullable: true })
  email: string; //邮箱
  @Column({ nullable: true })
  role: string; //角色
  @Column({ nullable: true })
  salt: string;
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  create_time: Date;
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  update_time: Date;
}

接下来看一下注册基本逻辑的实现。注册无非就是新增一个用户,在user.controller.ts规定一个路由/user/register接收用户传来的参数

import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}
  @Post('register')
  create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }
}

并将CreateUserDto暂时规定传两个参数

//create-user.dto
export class CreateUserDto {
  username: string;
  password: string;
}

user.service.ts写注册的逻辑

首先判断用户是否存在,存在的话抛出一个业务异常返回给前端一个自定义的业务状态码ApiErrorCode.USER_EXIST,不存在的话再向数据库添加一条数据

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApiException } from 'src/common/filter/http-exception/api.exception';
import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}
  async create(createUserDto: CreateUserDto) {
    const { username } = createUserDto;
    const existUser = await this.userRepository.findOne({
      where: { username },
    });
    if (existUser)
      throw new ApiException('用户已存在', ApiErrorCode.USER_EXIST);
    try {
      const newUser = await this.userRepository.create(createUserDto);
      return await this.userRepository.save(newUser);
    } catch (error) {
      throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }
}

启动项目,使用apifox模拟一个注册请求试一下

image.png

可以看到我们的数据添加成功了,而且 user 表中也有了我们刚刚注册的用户数据

image.png

但是,这么简单的注册肯定不能用于实际项目中的,在实际项目中用户注册的时候密码是需要进行加密的,并且后台保存的也只是加密后的密码,这时候我们可以使用crypto进行密码的加密,首先安装crypto

pnpm i crypto

然后新建 src/utils/crypto.ts,写一个加密的工具函数,使用sha256算法进行加密

import * as crypto from 'crypto';
export default (value: string, salt: string) =>
  crypto.pbkdf2Sync(value, salt, 1000, 18, 'sha256').toString('hex');

这个加密应该写在什么时候呢? 聪明的小伙伴肯定会说写在 service 中插入数据库之前对密码进行加密。这种做法肯定是对的,但是我们还可以写在user.entity.ts数据库实例中,只需要引入一个装饰器BeforeInsert即可执行数据库插入之前的操作,注意这里还保存了加密所需要的 salt 到数据库中,因为登录的时候需要用到这个 salt 对密码加密与数据库中的密码进行对比

import { Column, Entity, PrimaryGeneratedColumn, BeforeInsert } from 'typeorm';
import encry from '../../utils/crypto';
import * as crypto from 'crypto';
@Entity('user')
export class User {
  //这里省略部分代码
  @BeforeInsert()
  beforeInsert() {
    this.salt = crypto.randomBytes(4).toString('base64');
    this.password = encry(this.password, this.salt);
  }
}

这时候我们在测试一下注册的效果

image.png

可以看到插入的密码已经进行了加密,这里我们可以看到返回给前端的是数据库插入的数据,但是注册接口前端并不需要这些数据,我们可以自定义一下返回成功的数据,只需要修改一下 service 即可

image.png


登录



一般登录的流程是

用户输入用户名和密码,后端根据用户名获取到该用户盐(salt)和加密后的密码,然后使用这个 salt 以同样的方式对用户输入的密码进行加密,最后比较这两个加密后的密码是否一致。如果一致则代表用户登录成功,此时需要用到 jwt 根据用户名和密码生成一个限时的 token 返回给前端,前端获取到 token 缓存下来,后面需要登录验证的接口都添加带有这个 token 的请求头,后端来根据请求头的 token 来决定是否放行

接下来我们先安装@nestjs/jwt

pnpm i @nestjs/jwt

然后在.env 文件中配置一下 JWT 的密钥

# JWT密码
JWT_SECRET = bacdefg

使用nest g res auth生成一个 auth 模块来写我们的登录

auth.module.ts导入 JWT 模块以及user模块(这样就能调用 user 模块的方法了)

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { UserModule } from 'src/user/user.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
  controllers: [AuthController],
  providers: [AuthService],
  imports: [
    UserModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        return {
          secret: configService.get('JWT_SECRET'),
          global: true,
          signOptions: {
            expiresIn: '2h',
          },
        };
      },
    }),
  ],
})
export class AuthModule {}

注意 这里使用工厂模式导入了 JwtModule,我们可以从 ConfigService 获取.env 中的值

auth.controller.ts定义一个auth/login接口用于接收登录传来的参数

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginAuthDto } from './dto/login-auth.dto';
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Post('login')
  login(@Body() loginAuthDto: LoginAuthDto) {
    return this.authService.login(loginAuthDto);
  }
}

由于后面后调用 user 模块中查询用户是否存在的方法,所以先在user.service.ts加一个查询用户信息的方法

async findOne(username: string) {
    const user = await this.userRepository.findOne({
      where: { username },
    });
    if (!user) throw new HttpException('用户名不存在', HttpStatus.BAD_REQUEST);
    return user;
  }

同时需要在user.module.tsUserService导出才能给别的模块使用


@
Module({
  controllers: [UserController],
  providers: [UserService],
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.register({ secret: process.env.JWT_SECRET }),
  ],
  exports: [UserService],
})

然后在auth.service.ts写登录逻辑,这里首先判断用户是否存在,然后再使用该用户对应的 salt 进行加密比较,最后生成一个 token 返回给前端

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from 'src/user/user.service';
import { LoginAuthDto } from './dto/login-auth.dto';
import encry from '../utils/crypto';
@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwtService: JwtService,
  ) {}
  async login(loginAuthDto: LoginAuthDto) {
    const { username, password } = loginAuthDto;
    const user = await this.userService.findOne(username);
    if (user?.password !== encry(password, user.salt)) {
      throw new HttpException('密码错误', HttpStatus.UNAUTHORIZED);
    }
    const payload = { username: user.username, sub: user.id };
    return await this.jwtService.signAsync(payload);
  }
}

我们试一下这个接口,发现获取到了返回的 token

image.png


使用守卫 guard 验证 token



当前端拿到 token 的时候,会将 token 放在后续接口的请求头上,后端要做的就是验证这个 token 是否有效,无效返回给前端重新登录。

在 nest 中可以使用 guard 守卫进行拦截

执行nest g gu auth生成一个 auth 守卫auth.guard.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

当返回 true 时则放行,否则将拦截,所以我们可以在这里写校验 token 的逻辑,这里规定前端请求头字段为authorization,并且以Bearer ${token}这种形式传值

import {
  CanActivate,
  ExecutionContext,
  HttpException,
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,
    private configService: ConfigService,
  ) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) throw new HttpException('验证不通过', HttpStatus.FORBIDDEN);
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.get('JWT_SECRET'),
      });
      request['user'] = payload;
    } catch {
      throw new HttpException('token验证失败', HttpStatus.FORBIDDEN);
    }
    return true;
  }
  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

然后我们在auth.controller.ts定义一个测试接口使用一下这个守卫,这里用到了装饰器UseGuards

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginAuthDto } from './dto/login-auth.dto';
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Post('login')
  login(@Body() loginAuthDto: LoginAuthDto) {
    return this.authService.login(loginAuthDto);
  }
  @UseGuards(AuthGuard)
  @Post('/test')
  test() {
    return 1;
  }
}

不加 token 请求一下

image.png

我们发现验证没有通过。 我们通过 login 接口获取一个 token 加到请求头中再试一下

image.png

验证通过了


全局守卫 guard



上面固然可以实现我们所需要的结果,但是几乎所有接口都要加守卫,这样会显得很麻烦,所以我们可以将其改造为全局守卫即可解决

auth.module.ts引入APP_GUARD并将 AuthGuard 注入,这样它就成为了全局守卫

//..
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from '../auth/auth.guard';
@Module({
  controllers: [AuthController],
  providers: [
    AuthService,
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
  //...
})
export class AuthModule {}

auth.controller.ts中的装饰器UseGuards去掉,发现守卫依然有效。这样的话问题又来了,如果我们有的接口不要验证登录怎么办,比如注册和登录接口,这里可以通过设置元数据处理

执行nest g d public生成一个 public.decorator.ts 创建一个装饰器设置一下元数据 isPublic 为 true


i
mport { SetMetadata } from '@nestjs/common';
export const Public = () => SetMetadata('isPublic', true);

然后在 auth.guard.ts 中通过Reflector取出当前的 isPublic,如果为 true(即用了@Public 进行装饰过),则直接放行


import { Reflector } from '@nestjs/core';
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private jwtService: JwtService,
    private configService: ConfigService,
    private reflector: Reflector,
  ) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
      //即将调用的方法
      context.getHandler(),
      //controller类型
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
  //...
}

我们在 test 接口加上@Public测试一下

@Public()
  @Post('test')
  test() {
    return 1;
  }

image.png

会发现没有加 token 接口同样返回值了,这样我们就可以在不需要登录验证的接口上加@Public修饰即可


写在最后



到这里 nestjs 的登录注册功能基本完成了,由于篇幅原因,里面可能涉及到很多东西并没有详细讲解。当然如果你想更深层次的学习 NestJS 的话强烈建议你学习Nest 通关秘籍这本小册。同时欢迎关注专栏Vue3+NestJS 全栈开发后台权限管理系统,我会慢慢进行更新的~


源码地址



源码地址


相关文章
|
3月前
|
缓存 前端开发 Go
从4开始,在后端系统中增加用户注册和登录功能
从4开始,在后端系统中增加用户注册和登录功能
22 0
|
3月前
|
JSON Go 数据格式
从1开始,扩展Go语言后端业务系统的RPC功能
从1开始,扩展Go语言后端业务系统的RPC功能
34 0
|
2月前
|
人工智能 运维 监控
构建高性能微服务架构:现代后端开发的挑战与策略构建高效自动化运维系统的关键策略
【2月更文挑战第30天】 随着企业应用的复杂性增加,传统的单体应用架构已经难以满足快速迭代和高可用性的需求。微服务架构作为解决方案,以其服务的细粒度、独立性和弹性而受到青睐。本文将深入探讨如何构建一个高性能的微服务系统,包括关键的设计原则、常用的技术栈选择以及性能优化的最佳实践。我们将分析微服务在处理分布式事务、数据一致性以及服务发现等方面的挑战,并提出相应的解决策略。通过实例分析和案例研究,我们的目标是为后端开发人员提供一套实用的指南,帮助他们构建出既能快速响应市场变化,又能保持高效率和稳定性的微服务系统。 【2月更文挑战第30天】随着信息技术的飞速发展,企业对于信息系统的稳定性和效率要求
|
1天前
|
jenkins 持续交付
基于Jeecgboot前后端分离的平台后端系统采用jenkins发布
基于Jeecgboot前后端分离的平台后端系统采用jenkins发布
|
11天前
|
API 开发者 UED
构建高效微服务架构:后端开发的新趋势移动应用与系统:开发与优化的艺术
【4月更文挑战第30天】 随着现代软件系统对可伸缩性、灵活性和敏捷性的日益需求,传统的单体应用架构正逐渐向微服务架构转变。本文将探讨微服务架构的核心概念,分析其优势,并着重讨论如何利用最新的后端技术栈实现一个高效的微服务系统。我们将涵盖设计模式、服务划分、数据一致性、服务发现与注册、API网关以及容器化等关键技术点,为后端开发者提供一份实操指南。 【4月更文挑战第30天】 在数字化时代的浪潮中,移动应用和操作系统的紧密交织已成为日常生活和商业活动的基石。本文将深入探讨移动应用开发的关键技术、跨平台开发工具的选择以及移动操作系统的架构和性能优化策略。通过分析当前移动应用开发的挑战与机遇,我们将
|
12天前
|
JavaScript 前端开发 数据安全/隐私保护
【好用】推荐10套后端管理系统前端模板
选择合适的模板可以大大提高开发效率,减少重复劳动,让开发者能够专注于业务逻辑的实现和功能的优化。开发者应根据项目的具体需求、团队的技术栈熟悉度以及产品的长远规划来选择最合适的模板,问题来了,这10款模板你更喜欢用哪个呢,欢迎交流。
|
12天前
|
机器学习/深度学习 Kubernetes 微服务
后端技术发展及其在高性能系统中的应用研究
后端技术发展及其在高性能系统中的应用研究
18 0
|
12天前
|
JSON 前端开发 Java
管理系统总结(前端:Vue-cli, 后端Jdbc连接mysql数据库,项目部署tomcat里)
管理系统总结(前端:Vue-cli, 后端Jdbc连接mysql数据库,项目部署tomcat里)
|
15天前
|
安全 关系型数据库 MySQL
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
node实战——后端koa结合jwt连接mysql实现权限登录(node后端就业储备知识)
22 3
|
1月前
|
JSON 安全 关系型数据库
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证
SpringCloud Gateway 实现自定义全局过滤器 + JWT权限验证