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月前
|
SQL Java 测试技术
在Spring boot中 使用JWT和过滤器实现登录认证
在Spring boot中 使用JWT和过滤器实现登录认证
279 0
|
1月前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
48 0
|
7月前
|
安全 Java 应用服务中间件
Shiro + JWT 进行登录验证
Shiro + JWT 进行登录验证
61 2
|
2月前
|
JavaScript
Node.js单点登录SSO详解:Session、JWT、CORS让登录更简单(二)
Node.js单点登录SSO详解:Session、JWT、CORS让登录更简单(一)
56 0
|
2月前
|
存储 JSON JavaScript
Node.js单点登录SSO详解:Session、JWT、CORS让登录更简单(一)
Node.js单点登录SSO详解:Session、JWT、CORS让登录更简单(一)
117 0
|
3月前
|
JSON 安全 数据安全/隐私保护
从0到1搭建权限管理系统系列三 .net8 JWT创建Token并使用
【9月更文挑战第22天】在.NET 8中,从零开始搭建权限管理系统并使用JWT(JSON Web Tokens)创建Token是关键步骤。JWT是一种开放标准(RFC 7519),用于安全传输信息,由头部、载荷和签名三部分组成。首先需安装`Microsoft.AspNetCore.Authentication.JwtBearer`包,并在`Program.cs`中配置JWT服务。接着,创建一个静态方法`GenerateToken`生成包含用户名和角色的Token。最后,在控制器中使用`[Authorize]`属性验证和解析Token,从而实现身份验证和授权功能。
249 3
|
4月前
【Azure APIM】在APIM中实现JWT验证不通过时跳转到Azure登录页面
【Azure APIM】在APIM中实现JWT验证不通过时跳转到Azure登录页面
|
4月前
|
JSON API 数据安全/隐私保护
Django 后端架构开发:JWT 项目实践与Drf版本控制
Django 后端架构开发:JWT 项目实践与Drf版本控制
95 0
|
4月前
|
安全 Nacos 数据安全/隐私保护
【技术干货】破解Nacos安全隐患:连接用户名与密码明文传输!掌握HTTPS、JWT与OAuth2.0加密秘籍,打造坚不可摧的微服务注册与配置中心!从原理到实践,全方位解析如何构建安全防护体系,让您从此告别数据泄露风险!
【8月更文挑战第15天】Nacos是一款广受好评的微服务注册与配置中心,但其连接用户名和密码的明文传输成为安全隐患。本文探讨加密策略提升安全性。首先介绍明文传输风险,随后对比三种加密方案:HTTPS简化数据保护;JWT令牌减少凭证传输,适配分布式环境;OAuth2.0增强安全,支持多授权模式。每种方案各有千秋,开发者需根据具体需求选择最佳实践,确保服务安全稳定运行。
427 0
|
5月前
|
存储 JavaScript 前端开发
文本,三步走构思,富文本点击提交能够存储到数据库当中(下),最快的方法,还是会看资料,因此会整合资料最好,直接看资料最快,因为是JWT的资料,我们要设置好登录的内容,看登录的地方怎样写的
文本,三步走构思,富文本点击提交能够存储到数据库当中(下),最快的方法,还是会看资料,因此会整合资料最好,直接看资料最快,因为是JWT的资料,我们要设置好登录的内容,看登录的地方怎样写的