Node.js + Koa2实战进阶:整合数据库+JWT认证,新手上手封装方案
在上一篇Koa2微服务器搭建的基础上,很多初学者问:如何给接口加登录认证?如何连接数据库实现数据持久化?
今天就带大家把Koa2项目升级为“可落地的实战项目”:整合MySQL(最常用关系型数据库)+ JWT认证,全程采用公共封装思想,代码结构清晰、复用性高,新手跟着敲就能跑通!
📌 核心目标(新手先明确)
-
数据库层封装:统一管理连接、SQL执行,避免重复写连接逻辑;
-
JWT认证封装:登录生成token、接口校验token、token刷新/过期处理;
-
实战落地:实现“用户注册-登录-获取用户信息”完整流程;
-
封装原则:核心逻辑抽离成公共模块,接口层只关注业务,新手易维护。
🛠 环境准备
先安装本次实战所需依赖(补充上篇基础依赖):
# 数据库相关
npm install mysql2 --save # MySQL驱动(比mysql包更稳定)
# JWT相关
npm install jsonwebtoken --save # 生成/验证JWT
npm install koa-jwt --save # Koa专属JWT中间件(简化校验)
# 加密相关(密码加密)
npm install bcryptjs --save # 密码哈希加密
# 其他工具
npm install dotenv --save # 环境变量管理(避免硬编码密钥/数据库配置)
一、项目目录升级(结构化是封装的基础)
在上篇基础上补充目录,核心新增db/(数据库)、utils/(工具封装)、config/(环境配置):
koa2-demo/
├── app.js # 服务入口(挂载中间件、路由)
├── .env # 环境变量(数据库、JWT密钥等)
├── config/ # 配置目录
│ └── index.js # 读取.env配置,统一导出
├── db/ # 数据库层
│ ├── index.js # 数据库连接封装
│ └── user.model.js # 用户表操作封装(增删改查)
├── middleware/ # 中间件目录
│ ├── errorHandler.js # 全局错误处理(上篇已有)
│ ├── response.js # 统一响应(上篇已有)
│ └── auth.js # JWT认证中间件(新增)
├── router/ # 路由目录
│ ├── index.js # 路由注册(上篇已有)
│ └── user.router.js # 用户接口(注册/登录/信息)
├── utils/ # 工具封装
│ ├── jwt.js # JWT生成/验证封装
│ └── encrypt.js # 密码加密封装
└── package.json # 依赖配置
二、基础配置封装(环境变量+全局配置)
1. 配置.env文件(敏感信息隔离)
创建.env文件,存放数据库、JWT等敏感配置(不要提交到代码仓库):
# .env文件
# 服务器配置
PORT=3000
NODE_ENV=development
# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=你的MySQL密码
DB_NAME=koa2_demo
# JWT配置
JWT_SECRET=koa2_demo_2025_secret # 加密密钥(自定义,越复杂越好)
JWT_EXPIRES_IN=2h # token过期时间(2小时)
JWT_REFRESH_EXPIRES_IN=7d # 刷新token过期时间(7天)
2. 封装配置读取模块(config/index.js)
统一读取.env配置,避免各处直接读取环境变量:
// config/index.js
require('dotenv').config() // 加载.env文件
module.exports = {
// 服务器配置
port: process.env.PORT || 3000,
env: process.env.NODE_ENV || 'development',
// 数据库配置
db: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 10// 数据库连接池大小
},
// JWT配置
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN,
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN
}
}
三、数据库层封装(MySQL)
1. 数据库连接封装(db/index.js)
使用连接池管理MySQL连接,避免频繁创建/销毁连接:
// db/index.js
const mysql = require('mysql2/promise') // 异步连接(支持async/await)
const config = require('../config')
// 创建数据库连接池
const pool = mysql.createPool({
host: config.db.host,
port: config.db.port,
user: config.db.user,
password: config.db.password,
database: config.db.database,
connectionLimit: config.db.connectionLimit,
charset: 'utf8mb4'// 支持emoji等特殊字符
})
// 测试数据库连接
asyncfunctiontestConnection() {
try {
const connection = await pool.getConnection()
console.log('✅ 数据库连接成功!')
connection.release() // 释放连接
} catch (error) {
console.error('❌ 数据库连接失败:', error.message)
process.exit(1) // 连接失败退出进程
}
}
// 执行SQL的通用方法(封装,简化调用)
asyncfunctionexecuteSQL(sql, params = []) {
try {
const [rows] = await pool.execute(sql, params)
return rows
} catch (error) {
console.error('❌ SQL执行失败:', sql, error.message)
throw error // 抛出错误,让上层处理
}
}
// 初始化执行(项目启动时测试连接)
testConnection()
module.exports = {
pool,
executeSQL
}
2. 业务表操作封装(db/user.model.js)
针对用户表封装增删改查,接口层无需关心SQL细节:
// db/user.model.js
const { executeSQL } = require('./index')
/**
* 用户表操作模型
*/
classUserModel {
/**
* 根据用户名查询用户
* @param {string} username 用户名
* @returns {Promise<Object>} 用户信息
*/
staticasyncfindByUsername(username) {
const sql = 'SELECT * FROM users WHERE username = ?'
const rows = awaitexecuteSQL(sql, [username])
return rows[0] || null// 返回第一个用户(用户名唯一)
}
/**
* 根据ID查询用户
* @param {number} id 用户ID
* @returns {Promise<Object>} 用户信息
*/
staticasyncfindById(id) {
const sql = 'SELECT id, username, nickname, email FROM users WHERE id = ?'// 不返回密码
const rows = awaitexecuteSQL(sql, [id])
return rows[0] || null
}
/**
* 新增用户
* @param {Object} user {username, password, nickname, email}
* @returns {Promise<number>} 新增用户ID
*/
staticasynccreate(user) {
const { username, password, nickname, email } = user
const sql = `
INSERT INTO users (username, password, nickname, email, create_time)
VALUES (?, ?, ?, ?, NOW())
`
const result = awaitexecuteSQL(sql, [username, password, nickname, email])
return result.insertId
}
}
// 先创建users表(新手可直接执行这段SQL)
// CREATE TABLE IF NOT EXISTS users (
// id INT PRIMARY KEY AUTO_INCREMENT,
// username VARCHAR(50) NOT NULL UNIQUE,
// password VARCHAR(100) NOT NULL,
// nickname VARCHAR(50) DEFAULT '',
// email VARCHAR(100) DEFAULT '',
// create_time DATETIME NOT NULL,
// update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
// ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
module.exports = UserModel
四、JWT认证封装(核心)
1. 密码加密封装(utils/encrypt.js)
密码不能明文存储,封装bcrypt加密/校验方法:
// utils/encrypt.js
const bcrypt = require('bcryptjs')
/**
* 密码加密
* @param {string} password 明文密码
* @returns {string} 加密后的密码
*/
functionencryptPassword(password) {
const salt = bcrypt.genSaltSync(10) // 生成盐值
return bcrypt.hashSync(password, salt)
}
/**
* 验证密码
* @param {string} password 明文密码
* @param {string} hashPassword 加密后的密码
* @returns {boolean} 是否匹配
*/
functionverifyPassword(password, hashPassword) {
return bcrypt.compareSync(password, hashPassword)
}
module.exports = {
encryptPassword,
verifyPassword
}
2. JWT工具封装(utils/jwt.js)
封装token生成、验证、刷新方法:
// utils/jwt.js
const jwt = require('jsonwebtoken')
const config = require('../config')
/**
* 生成JWT token
* @param {Object} payload 载荷(存储用户ID等非敏感信息)
* @param {string} expiresIn 过期时间(默认使用配置)
* @returns {string} token
*/
functiongenerateToken(payload, expiresIn = config.jwt.expiresIn) {
return jwt.sign(payload, config.jwt.secret, { expiresIn })
}
/**
* 验证JWT token
* @param {string} token token字符串
* @returns {Object} 解码后的载荷
*/
functionverifyToken(token) {
try {
return jwt.verify(token, config.jwt.secret)
} catch (error) {
thrownewError(`Token验证失败:${error.message}`)
}
}
/**
* 生成刷新token(有效期更长)
* @param {Object} payload 载荷
* @returns {string} 刷新token
*/
functiongenerateRefreshToken(payload) {
returngenerateToken(payload, config.jwt.refreshExpiresIn)
}
module.exports = {
generateToken,
verifyToken,
generateRefreshToken
}
3. JWT认证中间件(middleware/auth.js)
封装Koa中间件,校验接口是否需要token:
// middleware/auth.js
const koaJwt = require('koa-jwt')
const config = require('../config')
const { fail } = require('./response') // 复用统一响应
// 白名单:不需要token的接口(登录/注册)
const whiteList = ['/api/v1/user/login', '/api/v1/user/register']
/**
* JWT认证中间件
* 白名单接口放行,其他接口校验token
*/
const authMiddleware = koaJwt({
secret: config.jwt.secret
}).unless({
path: whiteList // 白名单接口不校验
})
/**
* JWT错误处理中间件(必须放在authMiddleware之后)
*/
constjwtErrorHandler = async (ctx, next) => {
try {
awaitnext()
} catch (error) {
if (error.name === 'UnauthorizedError') {
// 根据错误类型返回不同提示
const msg = error.message === 'jwt expired'
? 'token已过期,请重新登录'
: '请先登录'
fail.call(ctx, msg, 401) // 复用统一失败响应
} else {
throw error // 其他错误交给全局错误中间件
}
}
}
module.exports = {
authMiddleware,
jwtErrorHandler
}
五、业务接口实现(用户注册/登录/信息)
1. 用户路由(router/user.router.js)
接口层只关注业务逻辑,数据库操作、JWT生成调用封装好的模块:
// router/user.router.js
constRouter = require('@koa/router')
constUserModel = require('../db/user.model')
const { encryptPassword, verifyPassword } = require('../utils/encrypt')
const { generateToken, generateRefreshToken } = require('../utils/jwt')
const userRouter = newRouter({ prefix: '/user' })
// 1. 用户注册
userRouter.post('/register', async (ctx) => {
const { username, password, nickname, email } = ctx.request.body
// 基础校验
if (!username || !password) {
return ctx.fail('用户名和密码不能为空', 400)
}
// 检查用户名是否已存在
const existUser = awaitUserModel.findByUsername(username)
if (existUser) {
return ctx.fail('用户名已存在', 400)
}
// 密码加密
const hashPassword = encryptPassword(password)
// 新增用户
const userId = awaitUserModel.create({
username,
password: hashPassword,
nickname: nickname || username,
email
})
ctx.success({ userId, username }, '注册成功')
})
// 2. 用户登录
userRouter.post('/login', async (ctx) => {
const { username, password } = ctx.request.body
// 基础校验
if (!username || !password) {
return ctx.fail('用户名和密码不能为空', 400)
}
// 查询用户
const user = awaitUserModel.findByUsername(username)
if (!user) {
return ctx.fail('用户名或密码错误', 400)
}
// 验证密码
const isPasswordValid = verifyPassword(password, user.password)
if (!isPasswordValid) {
return ctx.fail('用户名或密码错误', 400)
}
// 生成token(只存用户ID,避免敏感信息)
const payload = { id: user.id }
const token = generateToken(payload)
const refreshToken = generateRefreshToken(payload)
ctx.success({
token,
refreshToken,
user: {
id: user.id,
username: user.username,
nickname: user.nickname
}
}, '登录成功')
})
// 3. 获取用户信息(需要token)
userRouter.get('/info', async (ctx) => {
// koa-jwt会把解码后的payload放到ctx.state.user
const { id } = ctx.state.user
// 查询用户信息(不返回密码)
const user = awaitUserModel.findById(id)
if (!user) {
return ctx.fail('用户不存在', 404)
}
ctx.success(user, '获取用户信息成功')
})
module.exports = userRouter
2. 注册路由(router/index.js)
挂载用户路由,和上篇逻辑一致:
// router/index.js
constRouter = require('@koa/router')
const config = require('../config')
const userRouter = require('./user.router')
const rootRouter = newRouter({ prefix: config.apiPrefix })
rootRouter.use(
userRouter.routes(),
userRouter.allowedMethods()
)
module.exports = rootRouter
六、整合所有模块(app.js)
按顺序挂载中间件,注意JWT中间件的顺序:
// app.js
constKoa = require('koa')
const { koaBody } = require('koa-body')
const config = require('./config')
const rootRouter = require('./router')
// 中间件
const { errorHandlerMiddleware } = require('./middleware/errorHandler')
const { responseMiddleware } = require('./middleware/response')
const { authMiddleware, jwtErrorHandler } = require('./middleware/auth')
const app = newKoa()
// 中间件挂载顺序(核心!)
// 1. 全局错误处理
app.use(errorHandlerMiddleware)
// 2. 解析请求体
app.use(koaBody())
// 3. 统一响应格式
app.use(responseMiddleware)
// 4. JWT认证(先处理错误,再校验token)
app.use(jwtErrorHandler)
app.use(authMiddleware)
// 5. 挂载路由
app.use(rootRouter.routes())
// 启动服务
app.listen(config.port, () => {
console.log(`✅ Koa2服务启动成功!http://localhost:${config.port}`)
})
// 全局异常捕获
process.on('uncaughtException', (err) => {
console.error('❌ 未捕获异常:', err)
})
七、效果演示(配图说明)
配图1:用户注册接口测试
使用Postman调用POST /api/v1/user/register,传入参数后返回注册成功:
响应示例:
{
"code":200,
"msg":"注册成功",
"data":{
"userId":1,
"username":"test123"
}
}
配图2:登录接口返回token
调用POST /api/v1/user/login,返回token和用户信息:
响应示例:
{
"code":200,
"msg":"登录成功",
"data":{
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user":{
"id":1,
"username":"test123",
"nickname":"测试用户"
}
}
}
配图3:带token访问用户信息接口
调用GET /api/v1/user/info,请求头携带Authorization: Bearer <token>,返回用户信息:
响应示例:
{
"code":200,
"msg":"获取用户信息成功",
"data":{
"id":1,
"username":"test123",
"nickname":"测试用户",
"email":""
}
}
八、新手必知的核心知识点
1. 数据库封装关键
-
使用连接池而非单次连接,提升性能;
-
业务表操作封装成Model,接口层不写SQL;
-
SQL执行统一捕获错误,避免单个接口崩溃导致服务挂掉。
2. JWT认证核心
-
token只存非敏感信息(如用户ID),不存密码、手机号等;
-
密码必须哈希加密(bcrypt不可逆),禁止明文存储;
-
白名单接口(登录/注册)一定要排除,否则无法登录;
-
token过期后,可通过刷新token机制实现无感刷新(新手可后续扩展)。
3. 中间件顺序关键
Koa中间件是洋葱模型,顺序错误会导致功能异常:
✅ 正确顺序:错误处理 → 请求体解析 → 统一响应 → JWT错误处理 → JWT校验 → 路由。
九、总结
核心要点回顾
-
分层封装:数据库操作封装成Model、JWT封装成工具、认证封装成中间件,业务接口只关注逻辑;
-
敏感信息隔离:用.env管理数据库密钥、JWT密钥,避免硬编码;
-
安全规范:密码哈希加密、token只存非敏感信息、接口校验token;
-
错误处理:全局错误捕获+JWT错误单独处理,保证服务稳定。
新手扩展建议
-
增加刷新token接口:token过期后用refreshToken重新生成;
-
增加权限控制:基于用户角色(admin/普通用户)拦截接口;
-
增加日志模块:用
koa-logger记录请求日志,便于排查问题; -
增加数据校验:用
joi封装参数校验中间件,避免脏数据入库。
踩坑提醒
-
❌ 数据库密码明文存储:一定要用.env+加密;
-
❌ JWT密钥简单易猜:密钥越复杂越好,生产环境定期更换;
-
❌ 中间件顺序错误:JWT校验要放在路由前、响应中间件后;
-
❌ 忘记释放数据库连接:使用连接池可避免,单次连接需手动release。
这套整合方案是中小型Node.js项目的通用架构,新手掌握后可直接复用,大型项目也能在此基础上扩展(如分库分表、Redis缓存token等)。
如果你有更多实战问题(如如何整合MongoDB、如何做接口限流),欢迎在评论区交流~ 关注我,后续分享更多Node.js实战技巧!
🚀🚀 往期项目搭建,可前往
![]()
往期项目搭建,可前往[ Node.js + Koa2 微服务器搭建:初学者也能上手的实战”]👆
![]()
往期项目搭建,可前往[ WebSocket 入门指南:让浏览器和服务器“实时聊天”]👆
![]()
往期项目搭建,可前往[ Vue3 Hooks封装实战:告别重复代码,写出高复用、]👆
![]()
往期项目搭建,可前往[ Vue3路由实战:优雅封装+灵活拦截,解锁路由配置新]👆
![]()
往期项目搭建,可前往[ vue3 + TS + vite 搭建中后台管理系统(完整项目)]👆
![]()
往期项目搭建,可前往[ vue3 + Electron + vite 搭建桌面端应用(完整项目)]👆
![]()
往期项目搭建,可前往[ vue3 + vite + ts + element-ui搭建后台管理框架 ]👆
👆👆点击即可关注 “前端早间课”

总结:
前端路上 | 所知甚少,唯善学。
各位小伙伴有什么疑问,欢迎留言探讨。
— 关注我:前端路上不迷路 —
更多推荐
所有评论(0)