1.项目初始化
1.1 创建项目
- 新建
21-api_server文件夹作为项目的根目录,并在项目根目录中运行如下命令,初始化包管理配置文件:
npm init -y
2.运行npm i express@4.17.1命令,安装特定版本的express
3.在项目根目录中新建app.js文件作为整个项目的入口文件,使用如下代码初始化一个服务器:
// 导入express
const express = require("express");
// 创建服务器的实例对象
const app = express();
// 启动服务器
app.listen(3007, () => {
console.log("api_server running at 127.0.0.1:3007");
});
1.2配置cors跨域
- 运行
npm install cors@2.8.5安装cors中间件 - 在
app.js文件中导入并配置cors中间件
// 配置cors,解决跨域问题
const cors = require("cors");
// 将cors注册为全局中间件
app.use(cors());
1.3配置解析表单数据的中间件
通过如下代码,配置解析application/x-www-form-urlencoded格式的表单数据的中间件:
// 配置解析表单数据的中间件,
// 注意:这个中间件只能解析 application/x-www-form-urlencoded 格式的表单数据
app.use(express.urlencoded({ extended: false }));
1.4 初始化路由相关的文件夹
- 在项目根目录中,新建
router文件夹,用来存放所有的路由模块
路由模块中只存放客户端与处理函数之间的映射关系
- 在项目根目录中,新建
router_handler文件夹,用来存放所有的路由处理函数模块
路由处理函数模块中,专门负责存放每个路由对应的处理函数
1.5初始化用户路由模块
- 在router文件夹中,新建
user.js文件,作为用户的路由模块,并初始化代码如下:
/*用户的路由模块*/
// 导入express
const express = require("express");
// 创建路由对象
const router = express.Router();
// 注册新用户
router.post("/reguser",(req,res)=>{
res.send(reguser is ok!)
});
// 登录
router.post("/login", (req,res)=>{
res.send(login is ok!)
});
// 将路由对象router共享出去
module.exports = router;
- 在
app.js文件中,导入并使用用户路由模块
// 导入并使用用户路由模块
const userRouter = require("./router/user");
app.use("/api", userRouter);
1.6抽离用户路由模块中的处理函数
目的:为了保证
路由模块的纯粹性,所有的路由处理函数,必须抽离到对应的路由处理函数模块之中
- 在
/router_handler目录下,新建user.js文件,使用exports对象,分别向外共享如下两个路由处理函数:
/* 路由对应的路由处理函数模块 ,供/router/user.js调用*/
// 导入数据库的操作模块
const db = require("../db/index");
// 导入 密码加密模块 -bcryptjs
const bcrypt = require("bcryptjs");
// 注册新用户的处理函数
exports.regUser = (req, res) => {
res.send('regUser is OK~')
};
// 登录的处理函数
exports.login = (req, res) => {
res.send("login is OK~");
};
2.修改/router/user.js文件中的代码
/*用户的路由模块*/
const express = require("express");
// 创建路由对象
const router = express.Router();
// 导入用户路由对应的路由处理函数
const userHandler = require("../router_handler/user");
// 注册新用户
router.post("/reguser",userHandler.regUser);
// 登录
router.post("/login", userHandler.login);
// 共享出去router
module.exports = router;
2.登录注册
2.1新建ev_users表
在my_db_01数据库中,新建ev_users表如下:
2.2安装并配置mysql模块
在API接口项目中,需要安装并配置mysql这个第三方模块,来连接和操作MySQL数据库
- 运行
npm install mysql@2.18.1命令,安装mysql模块 - 在项目根目录中新建
/db/index.js文件,在此自定义模块中创建数据库的连接对象
// 导入mysql模块
const mysql = require('mysql')
// 调用mysql提供的createPool方法
const db = mysql.createPool({
host:'127.0.0.1', // 数据库的IP地址
user:'root', // 登录数据库的账号
password:'admin123', // 登录数据库的密码
database:'my_db_01' // 指定要操作的哪个数据库
})
module.exports = db
2.3开发注册用户的接口
2.3.0分析实现步骤
- 检测客户端提交的表单数据是否合法
- 检测用户名是否被占用
- 对用户提交的密码进行加密处理
- 给ev_users表中插入新用户
2.3.1检测表单数据是否合法
在注册新用户的路由处理函数中判断用户名或者密码是否为空
// 注册新用户的处理函数
exports.regUser = (req, res) => {
// 获取用户提交到服务器的注册信息
const userInfo = req.body;
// console.log('userInfo`',userInfo)
// 对表单中的数据进行合法性的校验
if (!userInfo.username || !userInfo.password) {
return res.send({ status: 1, message: "用户名或密码不能为空!" });
}
}
2.3.2 检测用户名是否被占用
导入mysql模块 -----> 定义SQL语句 ----->执行SQL语句并根据结果判断用户名是否被占用
- 导入数据库模块
const db = require('../db/index') - 定义SQL语句
const sql = 'select * from ev_users where username=?' - 执行SQL语句并根据结果判断用户名是否被占用
db.query(sqlStr, userInfo.username, (err, results) => {
// 执行SQL语句失败
if (err) {
return res.send({ status: 1, message: err.message });
}
// results是一个数组,如果长度大于0,就代表查找到了当前username,说明用户名已被占用
if (results.length > 0) {
return res.send({ status: 1, message: "用户名被占用,请更换其他用户名!" });
}
// TODO: 用户名可以使用,继续后续的流程...
2.3.3对密码进行加密处理
为了保证用户密码的安全性,不建议在数据库以
明文的形式保存用户密码,推荐对密码进行加密存储
当前项目中,使用bcryptjs对用户密码进行加密,优点:
- 加密之后的密码,无法被逆向破解
- 同一明文密码多次加密,得到的
加密结果各不相同,保证了安全性
- 运行
npm install bcryptjs@2.4.3命令,安装依赖包 - 在
router_handler/user.js文件中,导入bcryptjs - 当查询用户名未被占用可以继续使用之后,调用
bcrypt.hashSync(明文密码,随机盐的长度)方法,对用户密码加密处理
// 加密密码:调用bcrypt.hashSync()方法对密码加密,返回值是加密之后的密码字符串
userInfo.password = bcrypt.hashSync(userInfo.password, 10);
盐(Salt) 在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
2.3.4给ev_users表中插入新用户
// 定义插入新用户的SQL语句
const sql = "insert into ev_users set ?";
db.query(sql,{ username: userInfo.username, password: userInfo.password },(err, results)=>{
//执行SQL语句失败
if (err) {
return res.send({ status: 1, message: err.message });
}
// 执行SQL语句成功,但影响行数不为1
if (results.affectedRows !== 1) {
return res.send({ status: 1, message: "注册用户失败,请稍后再试 !" });
}
res.send({ status: 0, messsage: "用户注册成功" });
});
2.4 优化res.send()代码
我们看到,在路由处理函数中,需要多次调用
res.send()方法向客户端响应不同的失败处理结果,为了简化代码,可以手动封装一个res.cc()函数 在app.js文件中,所有路由之前声明一个全局中间件,为res对象挂载一个res.cc()函数
// 在所有路由之前,声明一个全局中间件,为res对象挂载一个res.cc()函数
app.use(function (req, res, next) {
//status = 1,表示失败的情况,
//err的值可能是一个错误对象也可能是一个错误的描述字符串
res.cc = function (err, status = 1) {
res.send({
status,
message: err instanceof Error ? err.message : err,
});
};
next();
});
// TODO:替换路由处理函数中的res.send为res.cc函数
//例如:res.send({ status: 1, message: "用户名或密码不合法!" })替换为 res.cc("用户名或密码不合法!");
// res.send({ status: 1, message: err.message })----->res.cc(err)
2.5优化表单数据验证
在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且
后端作为数据合法性验证的最后一个关口,在拦截非法数据方面,尤为重要
单纯的使用if...else...的形式对数据合法性进行验证,效率低下,出错率高,维护性差,因此推荐使用第三方数据验证模块,来降低出错率.提高验证的效率与可维护性
- 安装
express-joi中间件来实现自动对表单数据进行验证的功能npm install @escook/express-joi - 安装依赖包
joi为表单中携带的每个数据项定义验证规则npm install joi@17.4.0 - 在项目根目录中新建
/schems/user.js用户信息验证规则模块,并初始化代码如下
// 导入 Joi 来定义验证规则
const joi = require("joi");
/**
* string() 值必须是字符串
* alphanum() 值只能是包含 a-zA-Z0-9 的字符串
* min(length) 最小长度 * max(length) 最大长度
* required() 值是必填项,不能为 undefined
* pattern(正则表达式) 值必须符合正则表达式的规则
*/
//定义用户名和密码的验证规则
const username = joi.string().alphanum().min(1).max(10).required();
const password = joi
.string()
.pattern(/^[\s]{6,12}$/)
.required();
// 定义验证注册和登录表单数据的规则对象
exports.reg_login_schema = {
body: { username, password },
};
- 修改
/router/user.js代码如下
const express = require("express");
// 创建路由对象
const router = express.Router();
// 导入用户路由对应的路由处理函数
const userHandler = require("../router_handler/user");
// 导入验证数据的中间件
const expressJoi = require("@escook/express-joi");
// 导入需要的验证规则对象
const { reg_login_schema } = require("../schema/user");
// 注册新用户
router.post("/reguser",expressJoi(reg_login_schema), userHandler.regUser);
// 登录
router.post("/login", userHandler.login);
// 共享出去router
module.exports = router;
5.在app.js中路由后面定义全局错误级别中间件,捕获验证失败的错误,并把验证失败的结果响应个客户端
// 定义错误级别的中间件(捕获全局错误)
app.use((err, req, res, next) => {
if (err instanceof joi.ValidationError) {
return res.cc(err);
}
// 未知的错误
res.cc(err);
});
2.6开发登录用户的接口
2.6.0分析实现步骤
- 检测用户提交的表单数据是否合法
- 根据用户名查询用户的数据
- 判断用户输入的密码是否正确
- 生成
JWT的Token字符串
2.6.1检测登录的表单数据是否合法
// 登录接口
router.post("/login", expressJoi(reg_login_schema), userHandler.login);
2.6.2根据用户名查询用户数据
// 登录的处理函数
exports.login = (req, res) => {
// 接收表单数据
const userInfo = req.body;
// 定义sql语句(根据用户名去查询)
const sql = `select * from ev_users where username=?`;
// 执行sql语句
db.query(sql, userInfo.username, (err, results) => {
// 执行失败
if (err) {
return res.cc(err);
}
// 执行语句成功,但获取到的数据条数不为1,没查到信息
if (results.length !== 1) {
return res.cc("登录失败!");
}
// TODO:判断密码是否正确
// res.send("登录成功!");
});
2.6.3判断用户输入的密码是否正确
核心实现思路:调用
bcrypt.compareSync()方法比较密码是否一致,返回值是布尔值(true一致,false不一致)
// 拿到客户端提交的密码和数据库中的密码作比较
// bcrypt.compareSync(用户提交密码,数据库存储密码)
const compareResult = bcrypt.compareSync(
userInfo.password,
results[0].password
);
if (!compareResult) {
return res.cc("登录失败!");
}
// TODO:在服务器端生成Token的字符串
2.6.4 生成JWT的Token字符串
核心注意点:在生成Token字符串的时候,一定要剔除
密码和头像的值
- 通过ES6的高级语法,快速提出
密码和头像的值
// 在服务器端生成Token字符串
const user = { ...results[0], user_pic: "", password: "" };
- 运行
npm installjsonwebtoken@8.5.1命令安装生成Token字符串的依赖包 - 在
/router_handler/user.js模块中,导入jsonwebtoken包:const jwt = require('jsonwebtoken') - 创建
config.js全局配置文件
module.exports = {
// 加密和解密token的密钥
jwtSecrteKey:'Estella',
// token的有效期
expiresIn:'10h'
}
5.将用户信息对象加密成Token字符串:
// 导入生成token的包
const jwt = require("jsonwebtoken");
// 导入全局的配置文件
const config = require("../config");
// 对用户信息进行加密,生成Token字符串
const tokenStr = jwt.sign(user, config.jwtSecrteKey, {
expiresIn: config.expiresIn,
});
6.将生成的Token字符串响应给客户端
// 调用res.send()将token响应给客户端
res.send({
status: 0,
message: "登录成功",
token: 'Bearer ' + tokenStr,
});
2.7配置解析Token的中间件
- 运行
npm i express-jwt@5.3.3安装解析Token的中间件 - 在
app.js中注册路由之前,配置解析Token的中间件
// 一定要在路由之前 配置解析token的中间件
// 导入配置文件
const expressJwt = require('express-jwt')
// 解析token的中间件
const config = require('./config')
// 使用unless({path:[/^\/api/]})指定哪些接口不需要进行Token的身份认证
app.use(expressJwt({secret:config.jwtSecrteKey}).unless({path:[/^\/api/]}))
- 在
app.js中的错误级别中间件中,捕获并处理token认证失败后的错误
// 定义错误级别的中间件(捕获全局错误)
app.use((err, req, res, next) => {
if (err instanceof joi.ValidationError) {
return res.cc(err);
}
// 捕获token认证失败的错误
if (err.name === 'UnauthorizedError') return res.cc('身份认证失败')
// 未知的错误
res.cc(err);
});
3.个人中心
3.1获取用户的基本信息
3.1.0分析实现步骤
- 初始化
路由模块 - 初始化
路由处理函数模块 - 获取用户的基本信息
3.1.1 初始化路由模块和路由处理函数模块
// 个人中心模块路由 /router/userinfo.js
const express = require("express");
// 生成路由实例
const router = express.Router();
// 挂载路由
// 导入获取用户信息的路由处理函数
const { getUserInfo } = require("../router_handler/userinfo");
// 获取用户基本信息的接口
router.get("/userinfo", getUserInfo);
module.exports = router;
/* 个人中心模块 路由处理函数目录: /router_handler/userinfo.js 供 /router/userinfo.js调用*/
exports.getUserInfo = (req, res) => {
res.send('userinfo is ok!')
};
3.1.2 获取用户的基本信息
const db = require("../db/index");
exports.getUserInfo = (req, res) => {
// 定义查询用户信息的sql语句
// 注意:为了防止用户的密码被泄露,需要排除掉password字段
const sql =
"select id,username,nickname,email,user_pic from ev_users where id=?";
//注意:只要身份认证成功,中间件express会在req上就会被挂载一个user属性(是一个对象),存放用户id
db.query(sql, req.user.id, (err, results) => {
if (err) return res.cc(err);
if (results.length !== 1) {
return res.cc("获取用户信息失败");
}
// 将用户信息响应给客户端
res.send({
status: 0,
message: "获取用户信息成功!~",
data: results[0],
});
});
};
3.2 更新用户的基本信息
3.2.0分析实现步骤
- 定义
路由和路由处理函数 - 验证提交的表单数据
- 实现更新用户基本信息的功能
3.2.1定义路由和处理函数
在/router/userinfo.js文件中,新增更新用户信息的路由
const { getUserInfo ,updateUserInfo } = require("../router_handler/userinfo");
// 更新用户的基本信息的路由
router.post("/userinfo",updateUserInfo);
在router_handler/userinfo.js文件中,定义并向外共享更新用户信息的路由处理函数
// 更新用户基本信息的路由处理函数
exports.updateUserInfo = (req, res) => {
res.send('updateUserInfo is ok!~')
};
3.2.2 定义表单验证规则
在/schema/user.js文件中,新增更新用户基本信息的验证规则对象并暴露出去
// 定义id,nickname,email的验证规则
const id = joi.number().integer().min(1).required(); // 数字 整数 最小值1 必传
const nickname = joi.string().required(); // 字符串 必传
const email = joi.string().email().required(); // 字符串 邮箱验证格式 必传
// 定义修改用户信息的验证规则对象
exports.update_userinfo_schema = {
// 指需要对req.dody里的数据进行验证
body: { id, nickname, email },
};
在router/userinfo.js文件中导入数据合法性验证的中间件,并导入验证规则对象
// 导入验证数据的中间件
const expressJoi = require("@escook/express-joi");
// 导入验证数据的规则对象
const { update_userinfo_schema } = require("../schema/user");
// 修改更新用户的基本信息
router.post("/userinfo", expressJoi(update_userinfo_schema), updateUserInfo);
3.2.3 实现更新用户信息的功能
在/router_handler/userinfo.js定义更新信息的sql语句,调用db.query()方法执行sql语句
// 更新用户基本信息
exports.updateUserInfo = (req, res) => {
// 定义sql语句
const sql = "update ev_users set ? where id=?";
db.query(sql, [req.body, req.body.id], (err, results) => {
if (err) return res.cc(err);
if (results.affectedRows !== 1) {
return res.cc("修改用户基本信息失败");
}
return res.cc("修改用户基本信息成功", 0);
});
};
3.3开发重置密码的接口
3.3.0 分析实现步骤
- 定义
路由和路由处理函数 - 验证表单数据
- 实现重置密码的功能
3.3.1定义重置密码接口的路由和路由处理函数
在/router/userinfo.js文件中,新增更新用户密码的路由
const { getUserInfo ,updateUserInfo,updatePassword } = require("../router_handler/userinfo");
// 重置密码
router.post("/updatepwd", updatePassword);
在/router_handler/userinfo.js文件中,定义并向外共享更新用户密码的路由处理函数
//重置密码路由处理函数
exports.updatePassword = (req, res) => {
res.send('updateUserInfo is ok!~')
};
3.3.2 定义表单验证规则
注意:修改密码核心验证思路:
旧密码与新密码不能一致,且必须符合密码的验证规则
const password = joi.string().pattern(/^[\S]{6,12}$/).required();
// 定义修改密码的验证规则对象
exports.update_password_schema = {
body: {
oldPwd: password,
// 符合密码验证规则且不和旧密码一致
newPwd: joi.not(joi.ref("oldPwd")).concat(password),
},
};
修改在router/userinfo.js定义的重置密码路由
// 导入验证数据的规则对象
const {getUserInfo, update_userinfo_schema, update_password_schema } = require("../schema/user");
// 重置密码
router.post("/updatepwd", expressJoi(update_password_schema), updatePassword);
3.3.3实现重置密码的功能
// 导入处理密码的模块
const bcrypt = require("bcryptjs");
const sql = "select * from ev_users where id=?";
db.query(sql, req.user.id, (err, results) => {
if (err) return res.cc(err);
if (results.length !== 1) return res.cc("用户不存在");
// TODO:判断用户输入的旧密码是否正确
const compareResult = bcrypt.compareSync(
req.body.oldPwd,
results[0].password
);
if (!compareResult) return res.cc("原密码错误");
// TODO:更新数据库的密码
const sql = "update ev_users set password=? where id=?";
// 对新密码进行处理
const newPwd = bcrypt.hashSync(req.body.newPwd, 10);
db.query(sql, [newPwd, req.user.id], (err, results) => {
if (err) return res.cc(err);
if (results.affectedRows !== 1) {
return res.cc("修改密码失败,请稍后再试");
}
res.cc("更新密码成功 ok!~", 0);
});
});
3.4开发更换头像的接口
3.4.0 分析实现步骤
- 定义
路由和路由处理函数 - 验证表单数据
- 实现更换头像的功能
3.4.1定义路由和路由处理函数
在/router/userinfo.js文件中,新增更新用户密码的路由
const { getUserInfo, updateUserInfo, updatePassword, updateAvatar} =require("../router_handler/userinfo");
// 更换头像
router.post("/update/avatar", updateAvatar);
在/router/userinfo.js文件中,新增更新用户密码的路由处理函数
// 更新用户基本信息
exports.updateUserInfo = (req, res) => {
res.send('updateUserInfo is ok!~')
};
3.4.2定义表单验证规则
// 定义avatar的验证规则
// dataUri()指的是base64形式的图片,比如如下格式的字符串:
// data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = joi.string().dataUri().required();
// 定义更新头像的验证规则对象
exports.update_avatar_schema = {
body: {
avatar,
},
};
修改在router/userinfo.js定义的更换头像路由
// 更换头像
router.post("/update/avatar", expressJoi(update_avatar_schema), updateAvatar);
3.4.3实现更换头像的功能
// 更新用户基本信息
exports.updateUserInfo = (req, res) => {
// 定义sql语句
const sql = "update ev_users set ? where id=?";
db.query(sql, [req.body, req.body.id], (err, results) => {
if (err) return res.cc(err);
if (results.affectedRows !== 1) {
return res.cc("修改用户基本信息失败");
}
return res.cc("修改用户基本信息成功", 0);
});
};