在上一篇Koa2微服务器搭建的基础上,很多初学者问:如何给接口加登录认证?如何连接数据库实现数据持久化?

今天就带大家把Koa2项目升级为“可落地的实战项目”:整合MySQL(最常用关系型数据库)+ JWT认证,全程采用公共封装思想,代码结构清晰、复用性高,新手跟着敲就能跑通!

📌 核心目标(新手先明确)

  1. 数据库层封装:统一管理连接、SQL执行,避免重复写连接逻辑;

  2. JWT认证封装:登录生成token、接口校验token、token刷新/过期处理;

  3. 实战落地:实现“用户注册-登录-获取用户信息”完整流程;

  4. 封装原则:核心逻辑抽离成公共模块,接口层只关注业务,新手易维护。

🛠 环境准备

先安装本次实战所需依赖(补充上篇基础依赖):

  # 数据库相关
  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校验 → 路由。

九、总结

核心要点回顾

  1. 分层封装:数据库操作封装成Model、JWT封装成工具、认证封装成中间件,业务接口只关注逻辑;

  2. 敏感信息隔离:用.env管理数据库密钥、JWT密钥,避免硬编码;

  3. 安全规范:密码哈希加密、token只存非敏感信息、接口校验token;

  4. 错误处理:全局错误捕获+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搭建后台管理框架 ]👆

👆👆点击即可关注 “前端早间课”

在这里插入图片描述

总结:

前端路上 | 所知甚少,唯善学。
各位小伙伴有什么疑问,欢迎留言探讨。

— 关注我:前端路上不迷路 —

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐