在前后端分离架构中,用户认证系统是基础且核心的模块。本文将复盘我基于 Node.js + Express 实现的用户登录、注册、密码修改功能的全过程,包含技术选型、核心功能实现细节及踩坑经验。
一、项目背景与技术栈选型
为什么选择这些技术?
- Node.js + Express:轻量高效的后端框架,适合快速开发 API 服务,非阻塞 I/O 模型能高效处理并发请求。
- MySQL:关系型数据库,适合存储结构化的用户数据(用户名、密码、ID 等),支持事务和复杂查询。
- bcrypt:密码加密库,通过哈希算法(带盐值)不可逆加密密码,避免明文存储风险。
- Joi + @escook/express-joi:Joi 用于定义参数校验规则,搭配中间件快速实现请求数据合法性校验,减少冗余代码。
- express-jwt:解析 JWT Token 实现无状态认证,替代传统 Session,更适合分布式系统。
- cors:解决前后端跨域问题,允许前端(如 React/Vue)跨域请求 API。
二、核心功能详解与实现步骤
1. 项目初始化与基础配置
目标:搭建项目骨架,配置必要的中间件,确保服务能正常启动并处理请求。
-
初始化项目:
bash
mkdir node-auth-system && cd node-auth-system npm init -y # 生成 package.json npm install express mysql2 bcrypt joi @escook/express-joi cors express-jwt nodemon # 安装依赖(nodemon 用于开发时自动重启服务,提升效率)
-
配置 Express 与中间件:在
app.js中完成基础配置:const express = require('express'); const cors = require('cors'); const app = express(); // 解决跨域:允许所有来源的请求(生产环境需限制具体域名) app.use(cors()); // 解析 application/x-www-form-urlencoded 格式的请求体(如表单提交) app.use(express.urlencoded({ extended: false })); // 自定义响应中间件:简化成功/失败响应格式 app.use((req, res, next) => { res.*** = (message, status = 1) => { res.send({ status, message }); }; next(); }); // 导入并使用用户路由 const userRouter = require('./router/user'); app.use('/user', userRouter); // 启动服务 app.listen(3000, () => { console.log('Server running at http://127.0.0.1:3000'); });
2. 数据库设计与连接
目标:设计用户表结构,通过连接池高效连接 MySQL,避免频繁创建连接的性能损耗。
-
用户表设计:在 MySQL 中创建
ev_user表,包含核心字段:sql
CREATE TABLE ev_user ( id INT PRIMARY KEY AUTO_INCREMENT, # 自增ID(用户唯一标识) username VARCHAR(20) NOT NULL UNIQUE, # 用户名(唯一) password VARCHAR(100) NOT NULL, # 加密后的密码(bcrypt 加密后长度较长) create_time DATETIME DEFAULT CURRENT_TIMESTAMP, # 创建时间 update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP # 更新时间 ); -
数据库连接池配置:新建
db/index.js,使用mysql2创建连接池:const mysql = require('mysql2/promise'); // 推荐使用 promise 版本,支持 async/await const pool = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'your-mysql-password', # 替换为你的数据库密码 database: 'node_auth_db', # 数据库名称 waitForConnections: true, connectionLimit: 10, # 最大连接数 queueLimit: 0 }); module.exports = pool; # 导出连接池供全局使用
3. 用户注册功能
核心逻辑:校验用户名是否已存在 → 加密密码 → 插入数据库。
-
参数校验规则(
schema/user.js):使用 Joi 定义用户名和密码的校验规则,避免无效数据写入数据库:const Joi = require('joi'); // 用户名:3-20位字母/数字;密码:6-12位非空字符 module.exports = { regSchema: Joi.object({ username: Joi.string().alphanum().min(3).max(20).required(), password: Joi.string().pattern(/^[\S]{6,12}$/).required() }) }; -
路由与业务逻辑:
- 路由层(
router/user.js):通过中间件绑定校验规则javascript
运行
const express = require('express'); const router = express.Router(); const { regSchema } = require('../schema/user'); const expressJoi = require('@escook/express-joi'); const userHandler = require('../router-handle/user'); // 注册接口:先校验参数,再执行业务逻辑 router.post('/reguser', expressJoi(regSchema), userHandler.reguser); module.exports = router; - 业务逻辑层(
router-handle/user.js):javascript
运行
const db = require('../db'); const bcrypt = require('bcrypt'); exports.reguser = async (req, res) => { const { username, password } = req.body; try { // 1. 检查用户名是否已存在 const [user] = await db.query('SELECT * FROM ev_user WHERE username = ?', [username]); if (user.length > 0) { return res.***('用户名已被占用,请更换'); } // 2. 加密密码(盐值 rounds=10,值越大加密越慢但越安全) const encryptedPwd = bcrypt.hashSync(password, 10); // 3. 插入新用户 const [result] = await db.query('INSERT INTO ev_user (username, password) VALUES (?, ?)', [username, encryptedPwd]); if (result.affectedRows !== 1) { return res.***('注册失败,请重试'); } res.***('注册成功', 0); // status=0 表示成功 } catch (err) { res.***(err.message); // 捕获数据库错误并返回 } };
- 路由层(
4. 用户登录功能
核心逻辑:校验用户名是否存在 → 比对密码 → 生成 JWT Token 返回。
-
JWT 配置(
config.js):定义 Token 密钥和有效期(密钥需保密,建议环境变量存储):module.exports = { jwtSecretKey: 'your-secret-key-123', # 自定义密钥(生产环境需复杂且保密) expiresIn: '1h' # Token 有效期 1 小时 }; -
登录业务逻辑:
const db = require('../db'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const config = require('../config'); exports.login = async (req, res) => { const { username, password } = req.body; try { // 1. 查询用户是否存在 const [user] = await db.query('SELECT * FROM ev_user WHERE username = ?', [username]); if (user.length !== 1) { return res.***('用户名或密码错误'); // 模糊提示,避免泄露信息 } // 2. 比对密码(bcrypt.***pareSync 自动处理盐值) const isPwdValid = bcrypt.***pareSync(password, user[0].password); if (!isPwdValid) { return res.***('用户名或密码错误'); } // 3. 生成 Token(排除密码等敏感信息) const userInfo = { id: user[0].id, username: user[0].username }; const token = jwt.sign(userInfo, config.jwtSecretKey, { expiresIn: config.expiresIn }); // 4. 返回 Token 和成功信息 res.send({ status: 0, message: '登录成功', token: `Bearer ${token}` // 按规范添加 Bearer 前缀 }); } catch (err) { res.***(err.message); } }; -
配置 JWT 解析中间件(
app.js):让需要认证的接口自动解析 Token 并验证:const { expressjwt: expressJWT } = require('express-jwt'); const config = require('./config'); // 解析 Token:除了登录/注册接口,其他接口需验证 Token app.use( expressJWT({ secret: config.jwtSecretKey, algorithms: ['HS256'] // 必须指定算法(与生成时一致) }).unless({ path: [/^\/user\/reguser/, /^\/user\/login/] // 排除无需认证的路由 }) ); // 捕获 JWT 验证错误(如 Token 过期、无效) app.use((err, req, res, next) => { if (err.name === 'UnauthorizedError') { return res.***('身份认证失败,请重新登录'); } res.***('服务器错误'); });
5. 修改密码功能
核心逻辑:验证用户身份(通过 Token)→ 校验原密码 → 加密新密码并更新。
-
业务逻辑实现:
exports.updatepwd = async (req, res) => { const { oldpwd, newpwd } = req.body; const userId = req.user.id; // 从解析的 Token 中获取用户 ID(安全可靠) try { // 1. 查询用户信息(验证用户存在) const [user] = await db.query('SELECT * FROM ev_user WHERE id = ?', [userId]); if (user.length !== 1) { return res.***('用户不存在'); } // 2. 校验原密码 const isOldPwdValid = bcrypt.***pareSync(oldpwd, user[0].password); if (!isOldPwdValid) { return res.***('原密码错误'); } // 3. 加密新密码并更新 const newEncryptedPwd = bcrypt.hashSync(newpwd, 10); const [result] = await db.query('UPDATE ev_user SET password = ? WHERE id = ?', [newEncryptedPwd, userId]); if (result.affectedRows !== 1) { return res.***('修改密码失败'); } res.***('修改密码成功', 0); } catch (err) { res.***(err.message); } }; -
路由配置(需认证):
// 修改密码接口:需登录(Token 验证) router.post('/updatepwd', userHandler.updatepwd);
三、功能测试与验证
使用 Postman 或 Apifox 测试各接口,确保覆盖正常与异常场景:
| 接口 | 方法 | 参数示例 | 预期结果 |
|---|---|---|---|
/user/reguser |
POST | { "username": "test", "password": "123456" } |
注册成功,数据库新增用户,密码加密存储 |
/user/login |
POST | 同上 | 返回 Token,格式为 Bearer xxx
|
/user/updatepwd |
POST |
{ "oldpwd": "123456", "newpwd": "654321" }(需在请求头携带 Token) |
密码更新成功,数据库密码字段变化 |
异常场景测试:
- 注册已存在的用户名 → 返回 “用户名已被占用”
- 登录时密码错误 → 返回 “用户名或密码错误”
- 修改密码时原密码错误 → 返回 “原密码错误”
- 未携带 Token 访问
/updatepwd→ 返回 “身份认证失败”
四、踩坑总结与优化建议
-
异步逻辑顺序问题:初期曾将密码更新逻辑写在数据库查询回调外,导致 “用户不存在” 时仍执行更新,通过将代码嵌套在回调内(或使用 async/await)解决。
-
SQL 语法错误:拼写错误、占位符
?未用数组传递,导致 SQL 解析失败,需严格检查 SQL 语句和参数传递格式。 -
安全性优化:
- 密码加密必须用不可逆算法(bcrypt),避免 MD5 等可破解算法。
- 用户 ID 从 Token 中获取,而非前端传递,防止越权修改他人密码。
- 敏感接口必须验证 Token,且 Token 有效期不宜过长。
-
代码可维护性:采用分层架构(路由、业务逻辑、数据校验分离),后期新增功能(如用户信息修改)时无需重构整体代码。
五、总结
本项目通过 Node.js + Express 实现了用户认证的核心功能,从参数校验、密码加密到 JWT 认证,覆盖了后端开发的常见场景。关键在于理解 “分层思想” 和 “安全编码原则”—— 前者让代码结构清晰,后者避免用户数据泄露。
后续可扩展功能:用户信息管理、权限控制、Token 刷新机制等,逐步完善为企业级认证系统。
希望本文能为后端入门同学提供参考,如有问题欢迎留言讨论!