海边的小溪鱼

vuePress-theme-reco 海边的小溪鱼    2017 - 2023
海边的小溪鱼 海边的小溪鱼

Choose mode

  • dark
  • auto
  • light
首页
分类
  • CSS
  • HTTP
  • Axios
  • jQuery
  • NodeJS
  • JavaScript
  • Vue2
  • Server
  • Vue3
标签
时间轴
更多
  • Gitee (opens new window)
Getee (opens new window)
author-avatar

海边的小溪鱼

28

文章

14

标签

首页
分类
  • CSS
  • HTTP
  • Axios
  • jQuery
  • NodeJS
  • JavaScript
  • Vue2
  • Server
  • Vue3
标签
时间轴
更多
  • Gitee (opens new window)
Getee (opens new window)

Api接口开发(Express + MySQL)

vuePress-theme-reco 海边的小溪鱼    2017 - 2023

Api接口开发(Express + MySQL)

海边的小溪鱼 2017-07-14 Mysql

基于 Express + MySQL 实现 Api 接口的开发

  • 1. 初始化项目
    • 1-1. 创建项目
    • 1-2. 配置 cors 跨域
    • 1-3. 配置解析表单数据的中间件
    • 1-4. 初始化路由相关的文件夹
    • 1-5. 初始化用户路由模块
    • 1-6. 抽离用户路由模块中的处理函数
    • 1-7. 安装并配置 mysql 模块
  • 2. 用户注册
    • 2-1. 检测表单数据是否合法
    • 2-2. 检测用户名是否被占用
    • 2-3. 对密码进行加密处理
    • 2-4. 添加新用户(用户注册)
    • 2-5. 优化 res.send() 代码
    • 2-6. 优化表单数据验证(表单验证规则)
  • 3. 用户登录
    • 3-1. 检测登录表单的数据是否合法
    • 3-2. 判断用户输入的密码是否正确
    • 3-3. 生成 JWT 的 Token 字符串
    • 3-4. 配置解析 Token 的中间件
  • 获取用户信息
  • 更新用户信息
  • 重置密码
  • 更新用户头像

# 1. 初始化项目

蓝奏云下载地址:初始化项目完整代码 (opens new window)

在初始化项目中,需要安装的 依赖包 如下:

npm install express@4.18.1
npm install cors@2.8.5
npm install mysql@2.18.1

# 1-1. 创建项目

(1). 新建 api_server 文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:

npm init -y
1

(2). 在 package.json 文件中,添加 "type": "module" 启用 ES6 模块化:

{
  "type": "module",
  "name": "api_server",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}
1
2
3
4
5
6
7

(2). 运行如下的命令,安装特定版本的 express:

npm install express@4.18.1
1

(3). 在项目根目录中新建 app.js 作为整个项目的入口文件,并初始化如下的代码:

// 导入 express 模块
import express from "express"
// 创建 express 的服务器实例
const app = express()

// write your code here...

// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(3007, function () {
  console.log('api server running at http://127.0.0.1:3007')
})
1
2
3
4
5
6
7
8
9
10
11

# 1-2. 配置 cors 跨域

(1). 运行如下的命令,安装 cors 中间件:

npm install cors@2.8.5
1

(2). 在 app.js 中导入并配置 cors 中间件:

// 导入 cors 中间件
import cors from "cors"
// 将 cors 注册为全局中间件
app.use(cors())
1
2
3
4

# 1-3. 配置解析表单数据的中间件

(1). 通过如下的代码,配置解析 application/x-www-form-urlencoded 格式的表单数据的中间件:

app.use(express.urlencoded({ extended: false }))
1

# 1-4. 初始化路由相关的文件夹

(1). 在项目根目录中,新建 router 文件夹,用来存放所有的路由模块

注意:路由模块中,只存放客户端的请求与处理函数之间的映射关系

(2). 在项目根目录中,新建 controller 文件夹,用来存放所有的 路由处理函数模块

注意:路由处理函数模块中,专门负责存放每个路由对应的处理函数

# 1-5. 初始化用户路由模块

(1). 在 router 文件夹中,新建 user.js 文件,作为用户的路由模块,并初始化代码如下:

import express from "express"
// 创建路由对象
const router = express.Router()

// 注册新用户
router.post('/reguser', (req, res) => {
  res.send('reguser OK')
})

// 登录
router.post('/login', (req, res) => {
  res.send('login OK')
})

// 将路由对象共享出去
export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

(2). 在 app.js 中,导入并使用 用户路由模块 :

// 导入并注册用户路由模块
import userRouter from "./router/user.js"
app.use('/api', userRouter)
1
2
3

# 1-6. 抽离用户路由模块中的处理函数

目的:为了保证 路由模块 的纯粹性,所有的 路由处理函数,必须抽离到对应的 路由处理函数模块 中

(1). 在 /controller/user.js 中,使用 exports 对象,分别向外共享如下两个 路由处理函数 :

// 封装用户相关的路由处理函数,供 /router/user.js 模块进行调用
const user = {
    // 用户注册的处理函数
    reguser(req, res) {
        res.send("reguser OK")
    },
    // 用户登录的处理函数
    login(req, res) {
        res.send("login OK")
    }
}
export default user
1
2
3
4
5
6
7
8
9
10
11
12

(2). 将 /router/user.js 中的代码修改为如下结构:

import express from "express"
const router = express.Router()
// 导入自封装的模块
import user from "../controller/user.js" // 用户相关的路由处理函数

// 用户注册
router.post("/reguser", user.reguser)

// 用户登录
router.post("/login", user.login)

export default router
1
2
3
4
5
6
7
8
9
10
11
12

# 1-7. 安装并配置 mysql 模块

在 API 接口项目中,需要安装并配置 mysql 这个第三方模块,来连接和操作 MySQL 数据库

(1). 创建一个名为 my_db_01 的数据库,并新建 ev_users 表,如下: image (2). 项目中执行如下命令,安装 mysql 模块

npm install mysql@2.18.1
1

(3). 在项目根目录中新建 /db/index.js 文件,在此自定义模块中创建数据库的连接对象:

// 导入 mysql 模块
import mysql from "mysql"

// 创建数据库连接对象
const db = mysql.createPool({
    host: "127.0.0.1", // 数据库地址
    user: "root", // 数据库用户名(登录数据库的账号)
    password: "root", // 数据库密码(登录数据库的密码)
    database: "my_db_01", // 数据库名
    prot: 3306, // 数据库端口
})

// 向外共享 db 数据库连接对象
export default db
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(4). 在 /controller/user.js 中导入数据库操作模块,来对数据库进行操作

// 导入数据库操作模块
import db from "../db/index.js"

const user = {
    // 用户注册
    reguser(req, res) {
        // 这里写 sql 语句,对数据库进行操作
        // db.query("select * from ev_users", (err, results) => {
        //     if (err) return console.log(err.message)
        //     // select 查询语句:返回的是一个数组,里面包含用户的对象信息
        //     console.log(results);
        // })
        res.send("reguser OK")
    },
    // 用户登录
    login(req, res) {
        res.send("login OK")
    }
}

export default user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2. 用户注册

用户注册:需要安装的 依赖包 如下:

npm install bcryptjs@2.4.3
npm install joi@17.6.0
npm install @escook/express-joi@1.1.1

# 2-1. 检测表单数据是否合法

(1). 判断用户名和密码是否合法

const user = {
    reguser(req, res) {
        // 检测表单数据是否合法(判断用户名和密码是否合法)
        const userinfo = req.body
        if (!userinfo.username || !userinfo.password) {
            return res.send({ status: 1, message: '用户名或密码不能为空!' })
        }
    },
}
1
2
3
4
5
6
7
8
9

# 2-2. 检测用户名是否被占用

(1). 导入数据库操作模块执行 sql 语句,来判断用户名是否被占用

// 导入数据库操作模块
import db from "../db/index.js"
const user = {
    reguser(req, res) {
        // ...
        // 定义 SQL 语句
        const sql = `select * from ev_users where username=?`
        // 判断用户名是否被占用
        db.query(sql, [userinfo.username], function (err, results) {
            // 执行 SQL 语句失败
            if (err) {
                return res.send({ status: 1, message: err.message })
            }
            // 判断用户名是否被占用
            if (results.length > 0) {
                return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })
            }
            // TODO: 用户名可用,继续后续流程...
        })
    },
}
export default user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2-3. 对密码进行加密处理

注意:为了保证密码的安全性,不建议在数据库以 明文 的形式保存用户密码,推荐对密码进行 加密存储

在当前项目中,使用 bcryptjs 对用户密码进行加密,优点:
(1). 加密之后的密码,无法被逆向破解
(2). 同一明文密码多次加密,得到的加密结果各不相同,保证了安全性

(1). 运行如下命令,安装指定版本的 bcryptjs

npm i bcryptjs@2.4.3
1

(2). 在 /controller/user.js 中,导入 bcryptjs :

import bcrypt from "bcryptjs"
1

(3). 在注册用户的处理函数中,确认用户名可用之后,对用户的密码进行加密处理:

// 格式:bcrypt.hashSync(明文密码, 随机盐的长度)
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
userinfo.password = bcrypt.hashSync(userinfo.password, 10)
1
2
3

# 2-4. 添加新用户(用户注册)

const user = {
    reguser(req, res) {
        db.query(sql, [userinfo.username], function (err, results) {
            // ...
            userinfo.password = bcrypt.hashSync(userinfo.password, 10)
            // 注册用户(添加新用户)
            const adduser = 'insert into ev_users set ?'
            db.query(adduser, { username: userinfo.username, password: userinfo.password }, (err, results) => {
                if (err) return res.dend({ status: 1, message: err.message })
                if (results.affectedRows !== 1) return res.send({ status: 1, message: "注册用户失败,请稍后再试" })
                res.send({ status: 0, message: "注册成功" })
            })
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2-5. 优化 res.send() 代码

// 这是未优化前的代码

const user = {
    // 用户注册
    reguser(req, res) {
        // 2-1. 检测表单数据是否合法(判断用户名和密码是否合法)
        const userinfo = req.body
        if (!userinfo.username || !userinfo.password) {
            return res.send({ status: 1, message: '用户名或密码不能为空!' })
        }
        // 2-2. 检测用户名是否被占用
        const sql = `select * from ev_users where username=?` // 定义 SQL 语句
        db.query(sql, [userinfo.username], function (err, results) {
            // 执行 SQL 语句失败
            if (err) {
                return res.send({ status: 1, message: err.message })
            }
            // 判断用户名是否被占用
            if (results.length > 0) {
                return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })
            }
            // 2-3. 调用 bcrypt.hashSync() 对,密码进行加密(返回值是加密后的字符串)
            userinfo.password = bcrypt.hashSync(userinfo.password, 10)
            // 2-4. 注册用户(添加新用户)
            const adduser = 'insert into ev_users set ?'
            db.query(adduser, { username: userinfo.username, password: userinfo.password }, (err, results) => {
                if (err) return res.dend({ status: 1, message: err.message })
                if (results.affectedRows !== 1) return res.send({ status: 1, message: "注册用户失败,请稍后再试" })
                res.send({ status: 0, message: "注册成功" })
            })
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

我们发现,在 reguser 处理函数中,多次调用了 res.send() 向客户端响应处理的结果,为了简化代码,可以手动封装一个 res.cc() 函数

(1). 在 app.js 中注册一个 res.cc() 的全局中间件(所有路由之前),用于简化处理函数中的 res.send() 的代码

// 响应数据的中间件:用于简化"处理函数"中的 res.send() 的代码
app.use((req, res, next) => {
    // 为 res 对象挂载一个 cc() 函数
    res.cc = function (err, status = 1) { // status 默认为 1 (表示失败); 0 表示成功
        res.send({
            status, // 状态
            message: err instanceof Error ? err.message : err, // 状态描述,判断 err 是 错误对象 还是 字符串
        })
    }
    next()
})

// 用户相关的路由模块
app.use('/api', userRouter)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(2). 简化 /controller/user.js 处理函数中的 res.send() 的代码

// 这是优化后的代码

const user = {
    // 用户注册
    reguser(req, res) {
        // 2-1. 检测表单数据是否合法(判断用户名和密码是否合法)
        const userinfo = req.body
        if (!userinfo.username || !userinfo.password) {
            return res.cc("用户名或密码不能为空!")
        }
        // 2-2. 检测用户名是否被占用
        const sql = `select * from ev_users where username=?` // 定义 SQL 语句
        db.query(sql, [userinfo.username], function (err, results) {
            // 执行 SQL 语句失败
            if (err) {
                return res.cc(err)
            }
            // 判断用户名是否被占用
            if (results.length > 0) {
                return res.cc("用户名被占用,请更换其他用户名!")
            }
            // 2-3. 调用 bcrypt.hashSync() 对,密码进行加密(返回值是加密后的字符串)
            userinfo.password = bcrypt.hashSync(userinfo.password, 10)
            // 2-4. 注册用户(添加新用户)
            const adduser = 'insert into ev_users set ?'
            db.query(adduser, { username: userinfo.username, password: userinfo.password }, (err, results) => {
                if (err) return res.cc(err)
                if (results.affectedRows !== 1) return res.cc("注册用户失败,请稍后再试")
                res.cc(0, "注册成功")
            })
        })
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 2-6. 优化表单数据验证(表单验证规则)

表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容

在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且,后端做为数据合法性验证的最后一个关口,在拦截非法数据方面,起到了至关重要的作用

(1). 安装 joi 包,为表单中携带的每个数据项,定义验证规则:

npm install joi@17.6.0
1

(2). 安装 @escook/express-joi 中间件,来实现自动对表单数据进行验证的功能:

npm i @escook/express-joi@1.1.1
1

(3). 新建 /rule/user.js 用户信息验证规则模块,并初始化代码如下:

import joi from "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()

// 注册和登录表单的验证规则对象
const reg_login_rule = {
    body: {
        username,
        password
    }
}

export default reg_login_rule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

(4). 修改 /router/user.js 中的代码如下:

import express from "express"
const router = express.Router()
// 导入自封装的模块
import user from "../controller/user.js" // 用户相关的路由处理函数

// 1. 导入验证表单数据的中间件
import expressJoi from "@escook/express-joi"
// 2. 导入需要的验证规则对象
import reg_login_rule from "../rule/user.js"

// 用户注册
// 3.1 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证
// 3.2 数据验证通过后,会把这次请求流转给后面的路由处理函数
// 3.3 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理
router.post("/reguser", expressJoi(reg_login_rule), user.reguser)

// 用户登录
router.post("/login", user.login)

export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

(5). 在 app.js 的全局错误级别中间件中,捕获验证失败的错误,并把验证失败的结果响应给客户端:

import joi from "joi"

// 错误中间件(定义在路由的后面)
app.use((err, req, res, next) => {
    // 数据验证失败
    if (err instanceof joi.ValidationError) return res.cc(err)
    // 未知错误
    res.cc(err)
})
1
2
3
4
5
6
7
8
9

# 3. 用户登录

用户登录:需要安装的 依赖包 如下:

npm install jsonwebtoken@8.5.1
npm install express-jwt@5.3.3

# 3-1. 检测登录表单的数据是否合法

(1). 将 /router/user.js 中 "登录"的路由代码修改如下:

// 用户登录
router.post("/login", expressJoi(reg_login_rule), user.login)
1
2

(2). 在 controller/user.js "登录"路由函数中,执行 sql 语句(根据用户名查询用户的数据)

const user = {
    // 用户注册...

    // 用户登录
    login(req, res) {
        // 3-1. 判断用户名是否存在(根据用户名查询用户的数据)
        const userinfo = req.body
        const sql = `select * from ev_users where username=?`
        db.query(sql, userinfo.username, function (err, results) {
            // 执行 SQL 语句失败
            if (err) return res.cc(err)
            // 执行 SQL 语句成功,但是查询到数据条数不等于 1
            if (results.length !== 1) return res.cc('用户不存在')
            // TODO:判断用户输入的登录密码是否和数据库中的密码一致
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 3-2. 判断用户输入的密码是否正确

核心实现思路:调用 bcrypt.compareSync(用户提交的密码, 数据库中的密码) 方法,比较密码是否一致,返回值是布尔值

const user = {
    // 用户注册...

    // 用户登录
    login(req, res) {
        // 3-1. 判断用户名是否存在(根据用户名查询用户的数据)
        const userinfo = req.body
        const sql = `select * from ev_users where username=?`
        db.query(sql, userinfo.username, function (err, results) {
            // 执行 SQL 语句失败
            if (err) return res.cc(err)
            // 执行 SQL 语句成功,但是查询到数据条数不等于 1
            if (results.length !== 1) return res.cc('用户不存在')

            // 3-2. 判断密码是否正确:用户输入的密码 和 数据库中存储的密码,进行比较(返回值是布尔值)
            const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
            if (!compareResult) {
                return res.cc('密码错误')
            }
            // TODO:登录成功,生成 Token 字符串
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3-3. 生成 JWT 的 Token 字符串

核心注意点:在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值

(1). 运行如下的命令,安装生成 Token 字符串的包:

npm i jsonwebtoken@8.5.1
1

(2). 在 /controller/user.js 模块的头部区域,导入 jsonwebtoken 包:

import jwt from "jsonwebtoken"
1

(3). 使用 jwt.sing() 方法,对用户信息对象进行加密,生成一个加密后的 token 字符串,并响应给客户端

const user = { ...results[0], password: '', avatar: '' } // 剔除密码和头像的值
const secretKey = "Hello World ^_^" // 密钥
const tokenStr = jwt.sign(user, secretKey, { expiresIn: '24h', }) // token 有效期为24个小时
res.send({
  status: 0,
  message: '登录成功!',
  token: 'Bearer ' + tokenStr, // 将加密后的 Token 字符串响应给客户端
})
1
2
3
4
5
6
7
8

# 3-4. 配置解析 Token 的中间件

(1). 运行如下的命令,安装解析 Token 的中间件:

npm i express-jwt@5.3.3
1

(2). 在 app.js 中用户相关的路由模块之前,配置解析 Token 的中间件:

import expressJWT  from "express-jwt"

// 解析 Token 的中间件:expressJWT() 用于把加密后的用户信息,解密出来并挂载到 req.user 属性上
// 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证
app.use(expressJWT({ secret: "Hello World ^_^" }).unless({ path: [/^\/api\//] }))
1
2
3
4
5

(3). 在 app.js 中的 错误级别中间件 里面,捕获并处理 Token 认证失败后的错误:

// 错误中间件:捕获登录/注册表单时验证失败的错误,并把验证失败的结果响应给客户端
app.use((err, req, res, next) => {
    // 数据验证失败...

    // 捕获 jwt 解析失败后产生的错误(Token 字符串不合法/过期)
    if (err.name === 'UnauthorizedError') return res.cc('token不合法或已过期')

    // 未知错误...
})
1
2
3
4
5
6
7
8
9

# 获取用户信息

# 更新用户信息

# 重置密码

# 更新用户头像

帮助我们改善此页面! (opens new window)