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

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 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 全栈开发后台权限管理系统,我会慢慢进行更新的~


源码地址



源码地址


相关文章
|
4月前
|
缓存 监控 安全
构建高效后端系统的最佳实践
本文将深入探讨如何构建一个高效的后端系统,从设计原则、架构选择到性能优化等方面详细阐述。我们将结合实际案例和理论分析,帮助读者了解在构建后端系统时需要注意的关键点,并提供一些实用的建议和技巧。
64 2
|
1月前
|
机器学习/深度学习 前端开发 算法
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
97 3
|
1月前
|
机器学习/深度学习 人工智能 算法
【AI系统】AI 编译器后端优化
AI编译器采用多层架构,首先通过前端优化将不同框架的模型转化为统一的Graph IR并进行计算图级别的优化,如图算融合、内存优化等。接着,通过后端优化,将优化后的计算图转换为TensorIR,针对单个算子进行具体实现优化,包括循环优化、算子融合等,以适应不同的硬件架构,最终生成高效执行的机器代码。后端优化是提升算子性能的关键步骤,涉及复杂的优化策略和技术。
62 3
|
1月前
|
机器学习/深度学习 人工智能 算法
【AI系统】LLVM 后端代码生成
本文介绍 LLVM 后端的代码生成过程,包括将优化后的 LLVM IR 转换为目标代码的关键步骤,如指令选择、寄存器分配、指令调度等,以及后端如何支持不同硬件平台的代码生成。
44 6
|
3月前
|
存储 缓存 监控
构建高效后端系统的关键要素
【10月更文挑战第12天】 本文将探讨如何通过合理的架构设计、数据库优化、缓存策略和性能调优等措施,构建一个稳定、高效的后端系统。我们将分享一些实用的技巧和最佳实践,帮助开发者提升后端系统的性能和用户体验。
47 1
|
3月前
|
存储 缓存 测试技术
构建高效后端系统的关键策略##
【10月更文挑战第2天】 在当今数字化时代,后端系统的性能和可扩展性对于任何技术驱动的企业都至关重要。本文将深入探讨如何通过优化数据库设计、实施缓存机制、采用微服务架构以及自动化测试等措施,构建一个既高效又可靠的后端系统。我们将从基础的代码优化开始,逐步讨论到架构级别的改进,最终目标是提供一个全面的指导方案,帮助开发者和企业提升其后端系统的性能和可维护性。 ##
67 2
|
3月前
|
数据可视化 测试技术 Linux
基于Python后端构建多种不同的系统终端界面研究
【10月更文挑战第10天】本研究探讨了利用 Python 后端技术构建多样化系统终端界面的方法,涵盖命令行界面(CLI)、图形用户界面(GUI)及 Web 界面。通过分析各种界面的特点、适用场景及关键技术,展示了如何使用 Python 标准库和第三方库(如 `argparse`、`click`、`Tkinter` 和 `PyQt`)实现高效、灵活的界面设计。旨在提升用户体验并满足不同应用场景的需求。
|
3月前
|
Web App开发 JavaScript API
构建高效后端系统:Node.js与Express框架的实践之路
【9月更文挑战第37天】在数字化时代的浪潮中,后端开发作为技术架构的核心,承载着数据处理和业务逻辑的重要职责。本文将深入探讨如何利用Node.js及其强大的Express框架来搭建一个高效、可扩展的后端系统。我们将从基础概念讲起,逐步引导读者理解并实践如何设计、开发和维护一个高性能的后端服务。通过实际代码示例和清晰的步骤说明,本文旨在为初学者和有经验的开发者提供一个全面的指南,帮助他们在后端开发的旅途上走得更远。
61 3
|
3月前
|
JavaScript 前端开发
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
本文提供了一个Vue 3教程,讲解了如何使用axios库手动从后端获取数据,包括安装axios、配置后端访问地址、编写路由地址、发起HTTP请求以及在组件中读取和打印响应数据的步骤。
607 0
vue3教程,如何手动获取后端数据(入门到精通3,新人必学篇)
|
4月前
|
关系型数据库 Java 数据库
探索后端技术:构建高效系统的秘诀
在当今信息技术飞速发展的时代,后端开发作为软件架构中的核心部分,其重要性不言而喻。本文将深入探讨后端技术的各个方面,从基础概念到高级应用,帮助读者全面了解后端开发的世界,揭示构建高效系统的秘诀。

热门文章

最新文章