Skip to content

NestJS 用户注册与登录

流程图

User 模块

  • 利用 emial 模块 发送验证码

  • 利用 bcrypt 模块 加密密码 实现用户注册

  • 利用 passport 模块 实现用户登录

  • 利用 rbac 实现权限分级

创建 User 模块

  • 前面已经按照最终流程汇总完成架构设计

创建 user 模块

bash
nest g resource user

修改 user.module.ts

ts
import { Module } from "@nestjs/common";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
// 增加Excel 动态模块
import { ExcelModule } from "../../commonModules/excel/excel.module";
// 增加上传模块
import { UploadModule } from "../../commonModules/upload/upload.module";
// 增加邮箱模块
import { EmailModule } from "../../commonModules/email/email.module";
// 增加加密解密模块
import { CryptoModule } from "../../commonModules/crypto/crypto.module";
@Module({
  imports: [
    ExcelModule.forRoot({ name: "testdemoexcel" }),
    UploadModule,
    EmailModule,
    CryptoModule,
  ],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

user 模块下面的 dto

  • user/dto/login-user.dto.ts
ts
import { IsNotEmpty } from "class-validator";

/**
 * 登录用户数据传输对象(DTO)
 * 用于验证和传输登录相关的用户数据
 */
export class LoginUserDto {
  /**
   * 用户名属性
   * 使用class-validator进行验证,确保用户名不为空
   * @type {string}
   */
  @IsNotEmpty({
    message: "用户名不能为空",
  })
  userName: string;

  /**
   * 用户ID属性
   * 使用class-validator进行验证,确保用户ID不为空
   * @type {string}
   */
  @IsNotEmpty({
    message: "id不能为空",
  })
  userId: string;
}
  • user/dto/register-user.dto.ts
ts
import { IsEmail, IsNotEmpty, MinLength, IsPhoneNumber } from "class-validator";

export class RegisterUserDto {
  /**
   * 用户名字段
   * 使用class-validator进行验证,确保用户名不为空
   */
  @IsNotEmpty({
    message: "用户名不能为空",
  })
  username: string;

  /**
   * 昵称字段
   * 使用class-validator进行验证,确保昵称不为空
   */
  @IsNotEmpty({
    message: "昵称不能为空",
  })
  nick_name: string;

  /**
   * 密码字段
   * 使用class-validator进行验证,确保密码不为空且长度不少于6位
   */
  @IsNotEmpty({
    message: "密码不能为空",
  })
  @MinLength(6, {
    message: "密码不能少于 6 位",
  })
  password: string;

  /**
   * 邮箱字段
   * 使用class-validator进行验证,确保邮箱不为空且格式正确
   */
  @IsNotEmpty({
    message: "邮箱不能为空",
  })
  @IsEmail(
    {},
    {
      message: "不是合法的邮箱格式",
    }
  )
  email: string;

  /**
   * 验证码字段
   * 必填字段,用于验证用户注册的真实性和安全性
   * 使用class-validator进行验证,确保验证码不为空
   */
  @IsNotEmpty({
    message: "验证码不能为空",
  })
  captcha: string;

  /**
   * 手机号字段
   * 使用class-validator进行验证,确保手机号不为空且格式正确
   */
  @IsNotEmpty({
    message: "手机号不能为空",
  })
  @IsPhoneNumber("CN", {
    message: "请输入正确的手机号",
  })
  phone_number: string;

  /**
   * 头像字段
   * 使用class-validator进行验证,确保头像不为空
   */
  @IsNotEmpty({
    message: "头像不能为空",
  })
  head_pic: string;
}

/**
 * 发送邮件的数据传输对象(DTO)类
 * 用于验证和传输发送邮件所需的数据
 * 使用TypeScript类实现,结合class-validator进行数据验证
 */
export class SendEmailDto {
  /**
   * 邮箱地址
   * @example user@example.com
   */
  @IsNotEmpty({
    message: "用户名不能为空",
  })
  @IsEmail(
    {},
    {
      message: "请输入正确的邮箱",
    }
  )
  email: string;
}

user 模块下面的 entities

  • user/entities/Loginuser.entity.ts
ts
export class LoginuserEntity {
  userName: string;
  userId: string;
}
  • user/entities/Permissions.entity.ts
ts
export class PermissionsEntity {
  code: string;
  description: string;
}
  • user/entities/Registeruser.entity.ts
ts
export class RegisteruserEntity {
  username: string;
  nick_name: string;
  password: string;
  email: string;
  captcha: string;
  head_pic: string;
  phone_number: string;
  is_admin?: number;
  is_frozen?: number;
  user_roles?: any;
}
  • user/entities/Rules.entity.ts
ts
export class RulesEntity {
  name: string;
  roles_permissions: any;
}

user 模块下面的 guard

  • user/guard/userregister.guard.ts (用于验证验证码)
ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { GlobalGuardException } from "../../../common/globalguard.exception";
// 调用redis服务
import { RedisService } from "../../../commonModules/redis/redis.service";
@Injectable()
export class UserRegisterGuard implements CanActivate {
  @Inject()
  private readonly redisService: RedisService;
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    // 获取到验证码 去 redis 中验证
    const { captcha, email } = request.body;
    if (captcha) {
      // 有 就去redis中验证
      const redisCaptcha = await this.redisService.getstr(`captcha_${captcha}`);

      if (redisCaptcha === email) {
        return true;
      } else {
        throw new GlobalGuardException("验证码无效");
      }
    } else {
      throw new GlobalGuardException("请输入验证码");
    }
  }
}

user 模块下面的 controller

  • user/user.controller.ts
ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  UseGuards,
  Req,
} from "@nestjs/common";
import { UserService } from "./user.service";

// 引入验证机制
import { AuthGuard } from "@nestjs/passport";

// 引入DTO
import { RegisterUserDto, SendEmailDto } from "./dto/register-user.dto";
import { LoginUserDto } from "./dto/login-user.dto";
// 引入守卫
import { UserRegisterGuard } from "../user/guard/userregister.guard";
import { ByGuardpass } from "src/common/guard_pass.decorator";
import { RuleGuard } from "../../common/rule.guard";

// 引入装饰器
import { RBAC } from "../../common/rbac.decorator";
@Controller({
  path: "user",
  version: ["1"],
})
export class UserController {
  constructor(private readonly userService: UserService) {}

  // 发送邮件
  /**
   *
   * @api {POST} /v1/user/sendEmail 发送邮件
   * @apiName 发送邮件
   * @apiGroup user
   * @apiVersion  0.0.0
   *
   *
   * @apiBody  {String} email 邮箱地址
   *
   * @apiSuccess {Object[]} response 响应数据
   * @apiSuccess {Number} response.code 200
   * @apiSuccess {String} response.message 操作成功
   * @apiSuccess {Object} response.data 发送成功
   *
   * @apiParamExample  {type} Request-Example:
   * {
   *     email : 26650599@qq.com
   * }
   *
   *
   * @apiSuccessExample {type} Success-Response:
   * {
   *     code : 200
   *     message : 操作成功
   *     data : 发送成功
   * }
   *
   *
   */
  @Post("sendEmail")
  @ByGuardpass()
  sendEmail(@Body() body: SendEmailDto) {
    const result = this.userService.sendEmail(body.email);
    return result;
  }

  // 注册
  /**
   *
   * @api {POST} /v1/user/register 注册
   * @apiName 注册
   * @apiGroup user
   * @apiVersion  0.0.0
   *
   *
   * @apiBody  {String} username 用户名
   * @apiBody  {String} nick_name 昵称
   * @apiBody  {String} password 密码
   * @apiBody  {String} email 邮箱
   * @apiBody  {String} captcha 验证码
   * @apiBody  {String} phone_number 手机号
   * @apiBody  {String} head_pic 头像
   * @apiSuccess {Object[]} response 响应数据
   * @apiSuccess {Number} response.code 200
   * @apiSuccess {String} response.message 操作成功
   * @apiSuccess {Object} response.data 返回数据
   * @apiSuccess {String} response.data.token 生成token
   *
   * @apiParamExample  {type} Request-Example:
   * {
   *     email : 26650599@qq.com
   * }
   *
   *
   * @apiSuccessExample {type} Success-Response:
   * {
   *     token : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiYWRtaW4iLCJpYXQiOjE3NjY5MTQxNTksImV4cCI6MTc2NjkxNzc1OX0.LUYrzPWzh6OXF-tCiV-euGvIjrHFxsbbE6092WMh894
   * }
   *
   *
   */
  @Post("register")
  @UseGuards(UserRegisterGuard)
  @ByGuardpass()
  register(@Body() body: RegisterUserDto) {
    const result = this.userService.register(body);
    return result;
  }

  // 登录
  @ByGuardpass()
  @UseGuards(AuthGuard("local"))
  @Post("login")
  async login(@Req() req: any) {
    //获取到user信息知道是谁了
    console.log(req.user);
    const result: LoginUserDto = {
      userName: req.user.username,
      userId: req.user.id,
    };
    return this.userService.login({
      userName: result.userName,
      userId: result.userId,
    });
  }

  // 初始化数据
  @ByGuardpass()
  @Post("init")
  async init() {
    await this.userService.init();
    return "初始化数据成功";
  }

  // 测试权限分级
  @UseGuards(RuleGuard)
  @RBAC(["guest"])
  @Post("rbac")
  async rbac(@Body() body: any) {
    console.log("----");
    console.log(body);
    console.log("-------");
    return "测试";
  }
}

user 模块下面的 service

  • user/user.service.ts

注意

注册五部曲:

  1. 查询
  2. 密码加密
  3. 插入数据库
  4. 生成长短 token
  5. 删除 redis 中的验证码
  • 第二部

注意

通过 passport 的 local 策略验证用户名和密码

  • 第三部 权限

注意

  1. 通过 JWT 解析出来你是谁获取 userId(全局守卫)

  2. 先去 redis 中查找 --- 找不到就走数据库 ---- 数据库取出来存到 redis 中

  3. 先通过 userId 查出这个用户拥有的角色 ----- 查询出这个角色拥有的权限 ----- 查出这个角色 Id 对应的权限列表 --- 判断这个权限是否在权限列表中

  • 代码如下
ts
import { Inject, Injectable } from "@nestjs/common";
import { PrismadbService } from "../../commonModules/prisma/prisma.service";
import { RedisService } from "../../commonModules/redis/redis.service";
import { JwtAllService } from "../../commonModules/jwt/jwt.service";
import { ExcelService } from "../../commonModules/excel/excel.service";
import { UploadService } from "../../commonModules/upload/upload.service";
// 邮件发送
import { EmailService } from "../../commonModules/email/email.service";
import { customAlphabet } from "nanoid";
// 加密解密
import { CryptoService } from "../../commonModules/crypto/crypto.service";

// 实例化
import { RegisteruserEntity } from "./entities/Registeruser.entity";
import { LoginuserEntity } from "./entities/Loginuser.entity";
import { PermissionsEntity } from "./entities/Permissions.entity";
import { RulesEntity } from "./entities/Rules.entity";

// 抛出异常
import { GlobalCheckException } from "../../common/globalcheck.exception";

@Injectable()
export class UserService {
  // 数据库
  @Inject()
  private readonly prisma: PrismadbService;

  // redis
  @Inject()
  private readonly redisService: RedisService;

  // JWT
  @Inject()
  private readonly jwtAllService: JwtAllService;

  // Excel
  @Inject()
  private readonly ExcelService: ExcelService;

  // 上传

  @Inject()
  private readonly UploadService: UploadService;

  // 邮件发送
  @Inject()
  private readonly emailService: EmailService;

  // 加密解密
  @Inject()
  private readonly cryptoService: CryptoService;

  // 初始化数据
  async init() {
    // 用户列表
    const user1 = new RegisteruserEntity();
    user1.username = "admin";
    user1.password = this.cryptoService.md5Encrypt("123456");
    user1.email = "admin@qq.com";
    user1.phone_number = "19000000001";
    user1.nick_name = "jsopy_admin";
    user1.head_pic = "https://file.jsopy.com/IMAGES/avatarsmoke.jpg";
    user1.is_admin = 1; // 超级管理员
    user1.is_frozen = 2; // 未冻结

    const user2 = new RegisteruserEntity();
    user2.username = "guest";
    user2.password = this.cryptoService.md5Encrypt("123456");
    user2.email = "guest@qq.com";
    user2.phone_number = "19000000001";
    user2.head_pic = "https://file.jsopy.com/IMAGES/avatarsmoke.jpg";
    user2.is_admin = 2; //普通用户
    user2.is_frozen = 2; // 未冻结
    user2.nick_name = "jsopy_guest";

    // 权限
    const role1 = new RulesEntity();
    role1.name = "超级管理员";

    const role2 = new RulesEntity();
    role2.name = "普通用户";

    // 权限列表
    const permissions1 = new PermissionsEntity();
    permissions1.code = "admin";
    permissions1.description = "admin";

    const permissions2 = new PermissionsEntity();
    permissions2.code = "guest";
    permissions2.description = "user";

    // 添加角色哪个没有外连哪个优先插入 creatMany 不支持嵌套插入 所有有外联不适用
    const permissionarr = [permissions1, permissions2];
    const permissionresult: any[] = [];
    for (let i = 0; i < permissionarr.length; i++) {
      const item = permissionarr[i];
      const result = await this.prisma.permissions.create({ data: item });
      permissionresult.push(result);
    }

    // 角色
    role1.roles_permissions = {
      create: [
        {
          permission_id: permissionresult[0].id,
        },
        {
          permission_id: permissionresult[1].id,
        },
      ],
    };

    role2.roles_permissions = {
      create: [
        {
          permission_id: permissionresult[1].id,
        },
      ],
    };

    // 添加角色
    const rolearr = [role1, role2];
    const roleresult: any[] = [];
    for (let i = 0; i < rolearr.length; i++) {
      const item = rolearr[i];
      const result = await this.prisma.roles.create({ data: item });
      roleresult.push(result);
    }
    console.log(roleresult);

    // 外链 1
    user1.user_roles = {
      create: [
        {
          role_id: roleresult[0].id,
        },
      ],
    };
    // 外链 2
    user2.user_roles = {
      create: [
        {
          role_id: roleresult[1].id,
        },
      ],
    };
    // 添加用户
    const userarr = [user1, user2];
    const userresult: any[] = [];
    for (let i = 0; i < userarr.length; i++) {
      const item = userarr[i];
      const result = await this.prisma.users.create({ data: item });
      userresult.push(result);
    }
    console.log(userresult);
    return "初始化成功";
  }
  // 注册
  async register(body: RegisteruserEntity) {
    console.log(body.email);
    // 先去查询数据库里面有没有
    const result = await this.prisma.users.findFirst({
      where: {
        email: body.email,
      },
    });
    if (!result) {
      // 去掉验证码 直接插入
      let { captcha, ...result } = body;
      // 密码必须md5 加密
      result.password = this.cryptoService.md5Encrypt(result.password);
      // 默认普通用户
      const resultdata = {
        ...result,
        user_roles: {
          create: [
            {
              role_id: 2,
            },
          ],
        },
      };
      // 注册
      const userresult = await this.prisma.users.create({
        data: resultdata,
      });
      // 短 token
      const token = await this.jwtAllService.setToken({
        userId: userresult.id,
        userName: userresult.username,
      });
      // 长 token
      const refreshToken = await this.jwtAllService.setRefreshToken({
        userId: userresult.id,
        userName: userresult.username,
      });
      // 用过一次就删除
      await this.redisService.delkey(`captcha_${body.captcha}`);
      // 生成jwt
      return {
        token,
        refreshToken,
      };
    } else {
      throw new GlobalCheckException("邮箱已经存在");
    }
  }

  // 发送邮件
  async sendEmail(emialAddress: string) {
    // 验证码
    const nanoid = customAlphabet(
      "1234567890abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ",
      6
    );
    const randomId = nanoid();
    const result = await this.emailService.sendEmail({
      to: emialAddress, // 你要发送的邮件账号
      subject: "邮箱", // 标题
      html: `<h1>这是邮箱验证码${randomId}</h1>`, // 邮件内容
    });
    if (result.messageId) {
      // 存入redis
      const result = await this.redisService.setstr({
        key: `captcha_${randomId}`,
        value: emialAddress,
      });
      return "发送成功";
    } else {
      return "发送失败";
    }
  }

  // 登录
  async login(body: LoginuserEntity) {
    // 短 token
    const token = await this.jwtAllService.setToken({
      userId: body.userId,
      userName: body.userName,
    });
    // 长 token
    const refreshToken = await this.jwtAllService.setRefreshToken({
      userId: body.userId,
      userName: body.userName,
    });
    return {
      token,
      refreshToken,
    };
  }
}