上一篇介绍了如何使用 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
,将 AuthService
从 providers
数组中移除,并在 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.ts
将 controllers
注释掉:
此时看控制台,没有 User
相关的路由,我们需要去 app.module.ts
将 Controller
添加回去:
这么做是因为如果在 user.module.ts
中引入 AuthService
的话,就还要将其他的策略又引入一次,个人觉得很麻烦,就干脆直接用 app 来统一管理了。
四、登录验证
接下来就是检验效果的时候了,我们就按照原来注册的信息,进行登录请求:
图中可以看到,已经返回了一长串 token 了,而且控制台也打印了登录的步骤和用户信息。前端拿到这个 token,就可以请求其他有守卫的接口了。
接下来我们试试输错账号或密码的情况:
五、守卫
既然发放了 Token,就要能验证 Token,因此就要用到 Guard(守卫)
了。
我们拿之前的注册接口测试一下,修改 user.controller.ts
的代码,引入 UseGuards
和 AuthGuard
,并在路由上添加 @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项目
下一篇将介绍拦截器、异常处理以及日志的收集。