目录
[0 前言](#0 前言)
[1 初始化](#1 初始化)
[2 注册登录](#2 注册登录)
[2.1 注册](#2.1 注册)
[2.1.1 功能:密码加密(2.3.3)](#2.1.1 功能:密码加密(2.3.3))
[2.1.1.1 操作](#2.1.1.1 操作)
[2.1.1.2 bcryptjs详解](#2.1.1.2 bcryptjs详解)
[2.1.2 插入新用户(2.3.4)](#2.1.2 插入新用户(2.3.4))
[2.1.3 优化:表单数据验证(2.5)](#2.1.3 优化:表单数据验证(2.5))
[2.1.3.1 过时代码修正](#2.1.3.1 过时代码修正)
[2.1.3.2 关键操作](#2.1.3.2 关键操作)
[2.2 登录](#2.2 登录)
[2.2.1 判断密码是否正确(2.6.3)](#2.2.1 判断密码是否正确(2.6.3))
[2.2.2 生成 JWT 的 Token 字符串的注意点(2.6.4)](#2.2.2 生成 JWT 的 Token 字符串的注意点(2.6.4))
[3 个人中心](#3 个人中心)
[3.1 更新用户基本信息](#3.1 更新用户基本信息)
[3.1.1 验证表单数据(3.2.2)](#3.1.1 验证表单数据(3.2.2))
[3.2 重置密码](#3.2 重置密码)
[3.2.1 验证表单数据(3.3.2)](#3.2.1 验证表单数据(3.3.2))
[3.3 更新头像](#3.3 更新头像)
[3.3.1 验证表单数据(3.4.2)](#3.3.1 验证表单数据(3.4.2))
[4 文章分类管理](#4 文章分类管理)
[4.1 根据Id更新文章分类数据](#4.1 根据Id更新文章分类数据)
[4.1.1 查询分类名称与别名是否被占用(4.5.4)](#4.1.1 查询分类名称与别名是否被占用(4.5.4))
[5 文章管理](#5 文章管理)
[5.1 发布新文章](#5.1 发布新文章)
[5.1.1 使用 multer 解析表单数据(5.2.3)](#5.1.1 使用 multer 解析表单数据(5.2.3))
[5.1.2 验证表单数据(5.2.4)](#5.1.2 验证表单数据(5.2.4))
[5.1.3 新建数据对象(5.2.5)](#5.1.3 新建数据对象(5.2.5))
0 前言
本章仅记录部分功能代码(以前文章中未涉及的新内容)以及所遇到的问题
详细内容见 项目首页 - api系统 - GitCode 中的指导文档
❗❗❗最终完整代码也会上传至GitCode中,收藏方便查找
注:部分标题后面跟了原文档中的序号,方便查看
1 初始化
此部分暂无内容
2 注册登录
2.1 注册
2.1.1 功能:密码加密(2.3.3)
2.1.1.1 操作
使用 bcryptjs 对用户密码进行加密
安装指定版本的 bcryptjs :
CoffeeScript
npm i bcryptjs@2.4.3
导入 bcryptjs :
javascript
const bcrypt = require('bcryptjs')
在注册用户的处理函数中,确认用户名可用之后,调用 bcrypt.hashSync(明文密码, 随机盐的长度) 方法,对用户的密码进行加密处理:
javascript
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
2.1.1.2 bcryptjs详解
bcrypt.hashSync 是 `bcrypt` 库中用于同步生成哈希值的函数,在 Node.js 中经常使用该库来对密码等敏感信息进行哈希处理。以下详细介绍 bcrypt.hashSync 函数的参数:
函数
javascript
bcrypt.hashSync(data, saltOrRounds);
参数说明
- data
类型:string
描述:需要进行哈希处理的数据,通常是用户的密码。这是一个必需的参数,代表你要加密的原始文本。
2. saltOrRounds类型:string 或 number
描述:该参数可以是一个盐值(string 类型),也可以是生成盐的轮数(number 类型)。
当 saltOrRounds 为 number 类型时它代表生成盐的轮数,也称为成本因子(cost factor)。这个值越大,生成盐和哈希值所花费的时间就越长,安全性也相对更高。推荐的值通常在 10 - 12 之间。
bcrypt 会根据这个轮数自动生成一个随机的盐值,然后使用这个盐值对 data 进行哈希处理。
当 saltOrRounds 为 string 类型时
它代表一个预先定义好的盐值。使用自定义盐值时,每次使用相同的盐和数据进行哈希处理,会得到相同的哈希结果。
一般情况下,不建议手动指定盐值,因为 bcrypt 自动生成的随机盐值可以更好地保证安全性。
返回值
bcrypt.hashSync 函数会返回一个包含盐值和哈希值的字符串,这个字符串可以安全地存储在数据库中,用于后续的密码验证。
2.1.2 插入新用户(2.3.4)
判断插入是否成功
javascript
if (results.affectedRows !== 1) {
return res.send({ status: 1, message: '注册用户失败,请稍后再试!' })
}
即判断影响数据行数是否为1
2.1.3 优化:表单数据验证(2.5)
2.1.3.1 过时代码修正
const joi = require('@hapi/joi') 的写法现在已经失效,
应该这样导入:const joi = require('joi')
2.1.3.2 关键操作
安装 @hapi/joi 包,为表单中携带的每个数据项,定义验证规则:
CoffeeScript
npm install @hapi/joi@17.1.0
安装 @escook/express-joi 中间件,来实现自动对表单数据进行验证的功能:
CoffeeScript
npm i @escook/express-joi
常用验证规则:
除此之外,本文下方的验证表单数据中还包含一些其他常用规则
javascript
/**
* string() 值必须是字符串
* alphanum() 值只能是包含 a-zA-Z0-9 的字符串
* min(length) 最小长度
* max(length) 最大长度
* required() 值是必填项,不能为 undefined
* pattern(正则表达式) 值必须符合正则表达式的规则
*/
注意:一定要先指定一种数据类型string()或者其他(包括any()),然后才能执行后续操作
导出规则
javascript
// 注册和登录表单的验证规则对象
exports.reg_login_schema = {
// 表示需要对 req.body 中的数据进行验证
body: {
username,
password,
},
}
使用规则
javascript
// 1. 导入验证表单数据的中间件
const expressJoi = require('@escook/express-joi')
// 2. 导入需要的验证规则对象
const { reg_login_schema } = require('../schema/user')
//...
router.post('/reguser', expressJoi(reg_login_schema), userHandler.regUser)
2.2 登录
2.2.1 判断密码是否正确(2.6.3)
使用加密密码的包bcrypt
返回值是布尔值(true 一致、false 不一致)
javascript
bcrypt.compareSync(用户提交的密码, 数据库中的密码)
示例:
javascript
// 拿着用户输入的密码,和数据库中存储的密码进行对比
const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
// 如果对比的结果等于 false, 则证明用户输入的密码错误
if (!compareResult) {
return res.cc('登录失败!')
}
// TODO:登录成功,生成 Token 字符串
2.2.2 生成 JWT 的 Token 字符串的注意点(2.6.4)
核心注意点:在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值
通过 ES6 的高级语法,快速剔除 密码 和 头像 的值:
javascript
// 剔除完毕之后,user 中只保留了用户的 id, username, nickname, email 这四个属性的值
const user = { ...results[0], password: '', user_pic: '' }
3 个人中心
3.1 更新用户基本信息
3.1.1 验证表单数据(3.2.2)
记录新属性
javascript
const id = joi.number().integer().min(1).required()
const nickname = joi.string().required()
const email = joi.string().email().required()
integer() 方法用于验证一个值是否为整数
3.2 重置密码
3.2.1 验证表单数据(3.3.2)
javascript
body: {
// 使用 password 这个规则,验证 req.body.oldPwd 的值
oldPwd: password,
newPwd: joi.not(joi.ref('oldPwd')).concat(password),
},
使用 joi.not(joi.ref('oldPwd')).concat(password) 规则,验证 req.body.newPwd 的值
解读:
joi.ref('oldPwd') 表示 newPwd 的值必须和 oldPwd 的值保持一致
joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值
.concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则
3.3 更新头像
3.3.1 验证表单数据(3.4.2)
javascript
// dataUri() 指的是如下格式的字符串数据:
// data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = joi.string().dataUri().required()
dataUrl() 方法用于验证一个字符串是否符合数据 URL 的格式
4 文章分类管理
4.1 根据Id更新文章分类数据
4.1.1 查询分类名称与别名是否被占用(4.5.4)
javascript
SELECT * FROM ev_article_cate WHERE Id <> ? AND (name = ? or alias = ?)
注意:已有数据的情况下进行更新内容,需要排除当前数据的内容,否做无法做到部分内容修改!
5 文章管理
5.1 发布新文章
5.1.1 使用 multer 解析表单数据(5.2.3)
URL-encoded:适用于传输简单的文本数据,例如表单中的文本字段
multipart/form-data:适用于上传文件或包含二进制数据的表单
注意:使用 express.urlencoded() 中间件无法解析 multipart/form-data 格式的请求体数据
当前项目,推荐使用 multer 来解析 multipart/form-data 格式的表单数据
安装:
CoffeeScript
npm i multer@1.4.2
创建与使用:
javascript
// 导入解析 formdata 格式表单数据的包
const multer = require('multer')
// 导入处理路径的核心模块
const path = require('path')
// 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径
const upload = multer({ dest: path.join(__dirname, '../uploads') })
// 发布新文章的路由
// upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据
// 将文件类型的数据,解析并挂载到 req.file 属性中
// 将文本类型的数据,解析并挂载到 req.body 属性中
router.post('/add', upload.single('cover_img'), article_handler.addArticle)
之后文本类型的数据,即字段会通过joi来进行规则验证,但是文件类型的数据不行,得额外用if判断
5.1.2 验证表单数据(5.2.4)
注意:先后顺序一定不能变,因为multer会将其他字段挂载到req.body上,如果在joi之后,会导致部分字段不会被joi检测
javascript
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入文章的验证模块
const { add_article_schema } = require('../schema/article')
// 发布新文章的路由
// 注意:在当前的路由中,先后使用了两个中间件:
// 先使用 multer 解析表单数据
// 再使用 expressJoi 对解析的表单数据进行验证
router.post('/add', upload.single('cover_img'), expressJoi(add_article_schema),
article_handler.addArticle)
验证文件类型:
javascript
// 发布新文章的处理函数
exports.addArticle = (req, res) => {
// 手动判断是否上传了文章封面
if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选
参数!')
// TODO:表单数据合法,继续后面的处理流程...
})
5.1.3 新建数据对象(5.2.5)
javascript
const articleInfo = {
// 标题、内容、状态、所属的分类Id
...req.body,
// 文章封面在服务器端的存放路径
cover_img: path.join('/uploads', req.file.filename),
// 文章发布时间
pub_date: new Date(),
// 文章作者的Id
author_id: req.user.id,
}
断更...