用 Node.js 从零开始搭建用户认证系统:从注册到密码修改的全流程实现

用 Node.js 从零开始搭建用户认证系统:从注册到密码修改的全流程实现

在前后端分离架构中,用户认证系统是基础且核心的模块。本文将复盘我基于 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 → 返回 “身份认证失败”

四、踩坑总结与优化建议

  1. 异步逻辑顺序问题:初期曾将密码更新逻辑写在数据库查询回调外,导致 “用户不存在” 时仍执行更新,通过将代码嵌套在回调内(或使用 async/await)解决。

  2. SQL 语法错误:拼写错误、占位符 ? 未用数组传递,导致 SQL 解析失败,需严格检查 SQL 语句和参数传递格式。

  3. 安全性优化

    • 密码加密必须用不可逆算法(bcrypt),避免 MD5 等可破解算法。
    • 用户 ID 从 Token 中获取,而非前端传递,防止越权修改他人密码。
    • 敏感接口必须验证 Token,且 Token 有效期不宜过长。
  4. 代码可维护性:采用分层架构(路由、业务逻辑、数据校验分离),后期新增功能(如用户信息修改)时无需重构整体代码。

五、总结

本项目通过 Node.js + Express 实现了用户认证的核心功能,从参数校验、密码加密到 JWT 认证,覆盖了后端开发的常见场景。关键在于理解 “分层思想” 和 “安全编码原则”—— 前者让代码结构清晰,后者避免用户数据泄露。

后续可扩展功能:用户信息管理、权限控制、Token 刷新机制等,逐步完善为企业级认证系统。

希望本文能为后端入门同学提供参考,如有问题欢迎留言讨论!

转载请说明出处内容投诉
CSS教程网 » 用 Node.js 从零开始搭建用户认证系统:从注册到密码修改的全流程实现

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买