Nest.js+MySql从零到壹搭建服务(三)

上一篇介绍了如何使用 Sequelize 连接 MySQL,接下来,在原来代码的基础上进行扩展,实现用户的注册和登录功能。

这里简单提一下 JWT:

一、编写加密的工具函数

安装crypto依赖包

npm i crypto

src 目录下,新建文件夹 utils,里面将存放各种工具函数,然后新建 cryptogram.ts 文件:

import * as crypto from 'crypto';
/**
 * Make salt
 */
export function makeSalt(): string {
  return crypto.randomBytes(3).toString('base64');
}

/**
 * Encrypt password
 * @param password 密码
 * @param salt 密码盐
 */
export function encryptPassword(password: string, salt: string): string {
  if (!password || !salt) {
    return '';
  }
  const tempSalt = Buffer.from(salt, 'base64');
  return (
    // 10000 代表迭代次数 16代表长度
    crypto.pbkdf2Sync(password, tempSalt, 10000, 16, 'sha1').toString('base64')
  );
}

上面写了两个方法,一个是制作一个随机盐(salt),另一个是根据盐来加密密码。

这两个函数将贯穿注册和登录的功能。

二、用户注册

在写注册逻辑之前,我们需要先修改一下上一篇写过的代码,即 user.service.ts 中的 findeOne() 方法:

import { Injectable } from '@nestjs/common';
import * as Sequelize from 'sequelize'; // 引入 Sequelize 库
import sequelize from '../../database/sequelize'; // 引入 Sequelize 实例

@Injectable()
export class UserService {
  async findOne(phone: string): Promise<any | undefined> {
    const sql = `SELECT * FROM user WHERE phone = ${phone}`; // 一段平淡无奇的 SQL 查询语句
    try {
      const res = await sequelize.query(sql, {
        type: Sequelize.QueryTypes.SELECT, // 查询方式
        raw: true, // 是否使用数组组装的方式展示结果
        logging: true, // 是否将 SQL 语句打印到控制台,默认为 true
      });
      const user = res[0]; // 查出来的结果是一个数组,我们只取第一个。
      return user; // 查到就返回,没查到就返回undefined
    //   if(user){
    //     return {
    //         code:1,
    //         data:{
    //             user
    //         },
    //         msg:'查询成功'
    //     }
    //   }else{
    //     return {
    //         code:0,
    //         msg:'用户不存在'
    //     }
    //   }
    } catch (error) {
        return {
            code: 503,
            msg: `服务器错误: ${error}`,
          };
    }
  }
}

现在,findOne() 的功能更符合它的方法名了,查到了,就返回用户信息,查不到,就返回 undefined

接下来,我们开始编写注册功能:

import { Injectable } from '@nestjs/common';
import * as Sequelize from 'sequelize'; // 引入 Sequelize 库
import sequelize from '../../database/sequelize'; // 引入 Sequelize 实例

import { encryptPassword, makeSalt } from 'src/utils/cryptogram'; //引入加密函数

@Injectable()
export class UserService {
  // 查询用户是否存在
  async findOne(phone: string): Promise<any | undefined> {...
  }

  //   注册
  async register(reqBody: any): Promise<any> {
    const { name, phone, pwd } = reqBody;
    const user = await this.findOne(phone);
    if (user) {
      return {
        code: 0,
        msg: '用户已存在',
      };
    }
    const salt = makeSalt(); //制作密码盐
    const hashPwd = encryptPassword(pwd, salt); //加密
    const regSql = `INSERT INTO user (name,phone,pwd,pwd_salt) VALUES ('${name}','${phone}','${hashPwd}','${salt}')`;

    try {
      await sequelize.query(regSql, { logging: false });
      return {
        code: 1,
        msg: '注册成功',
      };
    } catch (error) {
      return {
        code: 503,
        msg: `服务器错误: ${error}`,
      };
    }
  }
}

编写好后,在 user.controller.ts 中添加路由
在这里插入图片描述
现在,我们使用 Apifox来测试一下:
在这里插入图片描述
我们再去数据库看一下:
在这里插入图片描述
发现已经将信息插入表中了,而且密码也是加密后的,至此,注册功能已基本完成。

三、JWT 的配置与验证

1. 安装依赖包

npm i passport passport-jwt passport-local @nestjs/passport @nestjs/jwt -S

2. 创建 Auth 模块

nest g service auth admin
nest g module auth admin

3. 新建一个存储常量的文件
auth 文件夹下新增一个 constants.ts,用于存储各种用到的常量:
在这里插入图片描述

4. 编写 JWT 策略

auth 文件夹下新增一个 jwt.strategy.ts,用于编写 JWT 的验证策略:
在这里插入图片描述

5. 编写 auth.service.ts 的验证逻辑

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { encryptPassword } from 'src/utils/cryptogram';
import { UserService } from '../user/user.service';


@Injectable()
export class AuthService {
    constructor(private readonly userService:UserService,private readonly jwtService:JwtService){}
    // JWT验证-校验用户信息
    async validateUser(phone: string,pwd: string): Promise<any>{
        console.log('JWT验证 - Step 2: 校验用户信息');
        const user =await this.userService.findOne(phone);
        if(user){
            const hashedPwd = user.pwd;
            const salt = user.pwd_salt;
            // 通过密码盐,加密传参,再与数据库里的比较,判断是否相等
            const hashPwd = encryptPassword(pwd,salt)
            if(hashedPwd===hashPwd){
                // 密码正确
                return {
                    code:1,
                    user
                }
            }else{
                // 密码错误
                return {
                    code:0,
                    msg:'密码错误'
                }
            }
        }else{
            return {
                code:0,
                msg:'用户不存在'
            }
        }
    }

    // JWT验证-处理jwt签证
    async certificate(user:any){
        const payload = {name:user.name,id:user.id,phone:user.phone}
        console.log('JWT验证 - Step 3: 处理 jwt 签证');
        try {
            const token = this.jwtService.sign(payload)
            return{
                code:1,
                msg:'登录成功',
                data:{
                    token
                }
            }
        } catch (error) {
            return {
                code:503,
                msg:`系统错误:${error}`
            }
        }
    }
}

此时保存文件,控制台会报错:
在这里插入图片描述
可以先不管,这是因为还没有把 JwtService 和 UserService 关联到 auth.module.ts 中。

5. 编写本地策略

这一步非必须,根据项目的需求来决定是否需要本地策略

// src/admin/auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';


@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy){
    constructor(private readonly authService:AuthService){
        super()
    }

    async validate(phone:string,pwd:string):Promise<any> {
        const user = await this.authService.validateUser(phone,pwd);
        if(!user){
            throw new UnauthorizedException()
        }
        return user;
    }
}

6. 关联 Module
在这里插入图片描述
此时保存文件,若还有上文的报错,则需要去 app.module.ts,将 AuthServiceproviders 数组中移除,并在 imports 数组中添加 AuthModule 即可:
在这里插入图片描述

7. 编写 login 路由

此时,回归到 user.controller.ts,我们将组装好的 JWT 相关文件引入,并根据验证码来判断用户状态:

import { Controller,Post, Body } from '@nestjs/common';
import { AuthService } from '../auth/auth.service';
import { UserService } from './user.service';

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

    @Post('find-one')
    findOne(@Body() body:any){
        return this.userService.findOne(body.phone);
    }

    @Post('register')
    async register(@Body() body:any){
        return await this.userService.register(body);
    }

    // JWT验证-用户请求登录
    @Post('login')
    async login(@Body() body:any){
        console.log('JWT验证 - Step 1: 用户请求登录');
        const authReult = await this.authService.validateUser(body.phone,body.pwd)
        if(authReult.code==1){
        	return this.authService.certificate(authReult.user)
        }else{
        	return {
               code:0,
               msg:authReult.msg||'用户不存在或密码错误'
            }
		}
    }
}

此时保存文件,同样的报错又出现了:
在这里插入图片描述
这次我们先去 user.module.tscontrollers 注释掉:
在这里插入图片描述
此时看控制台,没有 User 相关的路由,我们需要去 app.module.tsController 添加回去:
在这里插入图片描述
这么做是因为如果在 user.module.ts 中引入 AuthService 的话,就还要将其他的策略又引入一次,个人觉得很麻烦,就干脆直接用 app 来统一管理了。

四、登录验证

接下来就是检验效果的时候了,我们就按照原来注册的信息,进行登录请求:
在这里插入图片描述
在这里插入图片描述
图中可以看到,已经返回了一长串 token 了,而且控制台也打印了登录的步骤和用户信息。前端拿到这个 token,就可以请求其他有守卫的接口了。

接下来我们试试输错账号或密码的情况:

在这里插入图片描述
在这里插入图片描述

五、守卫

既然发放了 Token,就要能验证 Token,因此就要用到 Guard(守卫)了。
我们拿之前的注册接口测试一下,修改 user.controller.ts 的代码,引入 UseGuardsAuthGuard,并在路由上添加 @UseGuards(AuthGuard('jwt')),并且使用Request获取jwt的签证信息:

// src/admin/user/user.controller.ts
import { Controller,Post, Body,UseGuards,Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '../auth/auth.service';
import { UserService } from './user.service';

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

    @Post('find-one')
    findOne(@Body() body:any){
    	...
    }

    @Post('register')
    async register(@Body() body:any){
    	...
    }

    // JWT验证-用户请求登录
    @Post('login')
    async login(@Body() body:any){
        ...
    }


    // TOKEN守卫校验
    @UseGuards(AuthGuard('jwt'))
    @Post('check')
    async check(@Body() body:any,@Request() req:any){
    	console.log(req.user,'req===----')
        return {
            code:1,
            msg:'校验成功'
        }
    }
}

然后,我们先来试试请求头没有带 token 的情况:
在这里插入图片描述

可以看到,返回 401 状态码,Unauthorized 表示未授权,也就是判断你没有登录。

现在,我们试试带 Token 的情况,把登录拿到的 Token 复制到 Apifox的 Authorzation 里(选择 Bearer Token):
在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/844744ba4dc74191bce538059d678f01.png
此时,已经可以正常访问了,再看看控制台打印的信息,步骤也正如代码中注释的那样:
在这里插入图片描述
至此,登录功能已基本完成。

总结

本篇介绍了如何使用 JWT 对用户登录进行 Token 签发,并在接受到含 Token 请求的时候,如何验证用户信息,从而实现了登录验证。
当然,实现登录验证并不局限于 JWT,还有很多方法,有兴趣的读者可以自己查阅。
这里也说一下 JWT 的缺点,主要是无法在使用同一账号登录的情况下,后登录的,挤掉先登录的,也就是让先前的 Token 失效,从而保证信息安全(至少我是没查到相关解决方法,如果有大神解决过该问题,还请指点),只能使用一些其他黑科技挤掉 Token(如 Redis)。
现在,注册、登录功能都有了,接下来应该完善一个服务端应有的其他公共功能。

代码放在仓库的chapter2 nest-init: 这是一个初始化Nest项目

下一篇将介绍拦截器、异常处理以及日志的收集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值