第七节:商户列表、登录、登出、验证码接口设计

10 阅读7分钟

商户列表、登录、登出、验证码接口设计文档

一、系统架构概述

1. 登录流程设计

graph TD
    A[用户请求] --> B[验证码校验]
    B --> C[用户查询]
    C --> D[密码验证]
    D --> E[生成Token]
    E --> F[记录日志]
    F --> G[返回响应]

2. 依赖安装

npm install jsonwebtoken svg-captcha crypto-js jsencrypt redis express-session silly-datetime --save

依赖说明:

  • jsonwebtoken:JWT Token生成与验证
  • svg-captcha:图形验证码生成
  • crypto-js/jsencrypt:参数加解密处理
  • redis/express-session:会话数据存储
  • silly-datetime:日期时间格式化

二、JWT认证配置

1. Redis Token黑名单管理

// redis/token.js
/**
 * Token黑名单管理模块
 * 用于记录已注销的Token,增强安全性
 */
const redisClient = require('./index')
const jwt = require('./../jwt')
const prefix = 'token:'

/**
 * 将Token加入黑名单
 * @param {string} token - JWT Token
 * @param {number} expires - 过期时间(秒),默认使用JWT过期时间
 */
const addToBlacklist = async (token, expires = jwt.JWT_EXPIRES_IN) => {
  const client = await redisClient.getClient()
  await client.set(`${prefix}blacklist:${token}`, '1', 'EX', expires)
}

/**
 * 检查Token是否在黑名单中
 * @param {string} token - 要检查的Token
 * @returns {boolean} 是否在黑名单中
 */
const isInBlacklist = async token => {
  const client = await redisClient.getClient()
  const result = await client.get(`${prefix}blacklist:${token}`)
  return result !== null
}

/**
 * 从黑名单中移除Token
 * @param {string} token - 要移除的Token
 */
const removeFromBlacklist = async token => {
  const client = await redisClient.getClient()
  await client.del(`${prefix}blacklist:${token}`)
}

module.exports = {
  addToBlacklist,
  isInBlacklist,
  removeFromBlacklist
}

2. JWT核心配置

// jwt.js
/**
 * JWT认证管理模块
 * 负责Token的生成、验证和过期管理
 */
const jwt = require('jsonwebtoken')
const { promisify } = require('util')
const { JWT_SECRET } = require('./config')
const HTTP_STATUS = require('./utils/httpStatus')
const { isInBlacklist } = require('./redis/token')

const tojwt = promisify(jwt.sign)
const verifyjwt = promisify(jwt.verify)

// JWT配置
const JWT_EXPIRES_IN = 60 * 60 * 24 * 7 // 7天有效期

/**
 * 创建JWT Token
 * @param {object} payload - Token负载数据
 * @returns {string} JWT Token
 */
const createToken = async payload => {
  return await tojwt(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN })
}

/**
 * Token验证中间件
 * @param {boolean} required - 是否强制要求Token验证
 * @returns {Function} Express中间件
 */
const verifyToken = (required = true) => {
  return async (req, res, next) => {
    // 如果不需要验证,直接放行
    if (!required) {
      return next()
    }

    // 从请求头中提取Token
    const headers = req.headers
    let token = headers.authorization
    token = token ? token.split('Bearer ')[1] : null

    // Token不存在的情况
    if (!token) {
      return res.error('未登录', HTTP_STATUS.UNAUTHORIZED)
    }

    try {
      // 检查Token是否在黑名单中
      const isBlacklisted = await isInBlacklist(token)
      if (isBlacklisted) {
        return res.error('token已被注销', HTTP_STATUS.UNAUTHORIZED)
      }

      // 验证Token有效性
      const payload = await verifyjwt(token, JWT_SECRET)
      req.user = payload // 将用户信息挂载到请求对象
      next()
    } catch (err) {
      return res.error('无效的token', HTTP_STATUS.UNAUTHORIZED)
    }
  }
}

module.exports = { createToken, verifyToken, JWT_EXPIRES_IN }

3. 配置文件更新

// config/index.js
/**
 * 应用配置统一管理
 */
module.exports = {
  // ... 其他配置
  
  // JWT配置
  JWT_SECRET: 'your-jwt-secret-key-change-in-production',
  JWT_EXPIRES_IN: '7d',
  
  // RSA私钥(用于参数解密)
  PRIVATE_KEY: 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY...',
}

三、验证码系统

1. Redis验证码存储

// redis/captcha.js
/**
 * 验证码存储管理
 * 使用Redis存储验证码,支持自动过期
 */
const redisClient = require('./index')

const prefix = 'captcha:'

/**
 * 设置验证码到Redis
 * @param {string} key - 验证码键(通常为sessionId)
 * @param {object} value - 验证码数据
 * @param {number} expires - 过期时间(秒),默认5分钟
 */
const setCaptcha = async (key, value, expires = 300) => {
  const client = await redisClient.getClient()
  await client.set(`${prefix}${key}`, JSON.stringify(value), 'EX', expires)
}

/**
 * 从Redis获取验证码
 * @param {string} key - 验证码键
 * @returns {object|null} 验证码数据或null
 */
const getCaptcha = async key => {
  const client = await redisClient.getClient()
  const value = await client.get(`${prefix}${key}`)
  if (value) {
    return JSON.parse(value)
  }
  return null
}

/**
 * 删除验证码
 * @param {string} key - 验证码键
 */
const deleteCaptcha = async key => {
  const client = await redisClient.getClient()
  await client.del(`${prefix}${key}`)
}

module.exports = {
  setCaptcha,
  getCaptcha,
  deleteCaptcha
}

四、商户管理模块

1. 商户数据模型

// models/auth/tenant.js
/**
 * 商户(租户)数据模型
 */
const mongoose = require('mongoose')
const { AtModel } = require('../../models/base')

const tenantSchema = new mongoose.Schema({
  ...AtModel,
  companyName: {
    type: String,
    default: '',
    required: [true, '公司名称不能为空'],
    trim: true
  },
  domain: {
    type: String,
    default: '',
    unique: true,
    trim: true,
    lowercase: true
  },
  tenantId: {
    type: String,
    default: '',
    required: [true, '租户ID不能为空'],
    unique: true,
    index: true
  },
  status: {
    type: String,
    enum: ['active', 'inactive', 'suspended'],
    default: 'active'
  }
})

// 创建模型
const tenantModel = mongoose.model('Tenants', tenantSchema)

module.exports = tenantModel

2. 商户控制器

// controllers/auth/tenant.js
/**
 * 商户管理控制器
 */
const {
  AuthModel: { TenantModel }
} = require('../../models')

const createError = require('http-errors')

const getTenantList = async (req, res, next) => {
  try {
    const tenantList = await TenantModel.find()
    res.success({
      tenantEnabledL: true,
      voList: tenantList
    })
  } catch (error) {
    next(createError(500, error.message))
  }
}

module.exports = {
  getTenantList
}


/**
 * 商户初始化脚本(可选)
 */
// scripts/initTenantList.js
// 用于初始化默认商户数据

module.exports = {
  getTenantList
}

五、认证控制器

// controllers/auth/index.js
/**
 * 认证相关控制器
 * 包含验证码、登录、登出等功能
 */
const svgCaptcha = require('svg-captcha')
const { createToken, JWT_EXPIRES_IN } = require('./../../jwt')
const captchaRedis = require('./../../redis/captcha')
const tokenRedis = require('./../../redis/token')
const sd = require('silly-datetime')
const createError = require('http-errors')
const { UsersModel } = require('../../models/system')

/**
 * 生成图形验证码
 */
const getCaptcha = async (req, res, next) => {
  try {
    // 生成图形验证码配置
    const captcha = svgCaptcha.create({
      size: 4,               // 验证码长度
      ignoreChars: '0o1i',   // 忽略容易混淆的字符
      noise: 2,              // 干扰线数量
      color: true,           // 彩色验证码
      background: '#f0f0f0'  // 背景色
    })

    const captchaCode = captcha.text.toLowerCase()
    const sessionId = req.sessionID
    
    console.log('生成验证码:', { sessionId, captchaCode })

    // 存储验证码到Redis(5分钟过期)
    await captchaRedis.setCaptcha(
      sessionId,
      {
        code: captchaCode,
        sessionId,
        createdAt: new Date().toISOString()
      },
      60 * 5
    )

    // 返回验证码图片和UUID
    res.success({
      captchaEnabled: true,
      img: captcha.data,
      uuid: sessionId
    })
  } catch (error) {
    next(createError(500, error.message))
  }
}

/**
 * 用户登录
 */
const login = async (req, res, next) => {
  try {
    const { username: userName, tenantId } = req.body

    // 更新用户登录信息
    const updateUser = await UsersModel.findOneAndUpdate(
      { userName, tenantId },
      {
        loginDate: sd.format(new Date(), 'YYYY-MM-DD HH:mm:ss'),
        loginIp: req.ip === '::1' ? '127.0.0.1' : req.ip
      },
      { new: true }
    )

    // 生成JWT Token
    const token = await createToken(updateUser.toJSON())

    // 返回登录结果
    res.success({
      access_token: token,
      client_id: req.headers.clientid,
      expire_in: JWT_EXPIRES_IN,
      user: {
        userId: updateUser.userId,
        userName: updateUser.userName,
        nickName: updateUser.nickName,
        avatar: updateUser.avatar
      }
    })
  } catch (error) {
    next(createError(500, error.message))
  }
}

/**
 * 用户登出
 */
const logout = async (req, res, next) => {
  try {
    // 从请求头获取Token
    const headers = req.headers
    let token = headers.authorization
    token = token ? token.split('Bearer ')[1] : null

    if (token) {
      // 将Token加入黑名单
      await tokenRedis.addToBlacklist(token, JWT_EXPIRES_IN)
      console.log('Token已加入黑名单:', token.substring(0, 20) + '...')
    }

    // 清除Session
    req.session.destroy((err) => {
      if (err) {
        console.error('清除Session失败:', err)
      }
    })

    res.success({ message: '登出成功' })
  } catch (error) {
    next(createError(500, error.message))
  }
}

module.exports = {
  getCaptcha,
  login,
  logout
}

六、加解密工具

1. 加密工具函数

// utils/crypto.js
/**
 * 加解密工具函数(基于CryptoJS)
 */
const CryptoJS = require('crypto-js')

/**
 * 解密Base64字符串
 * @param {string} str - Base64编码的字符串
 * @returns {CryptoJS.lib.WordArray} 解密后的WordArray
 */
const decryptBase64 = str => {
  return CryptoJS.enc.Base64.parse(str)
}

/**
 * AES解密
 * @param {string} message - AES加密的字符串
 * @param {string} aesKey - AES密钥
 * @returns {string} 解密后的字符串
 */
const decryptWithAes = (message, aesKey) => {
  const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
  })
  return decrypted.toString(CryptoJS.enc.Utf8)
}

module.exports = {
  decryptBase64,
  decryptWithAes
}

2. RSA解密工具

// utils/jsencrypt.js
/**
 * RSA解密工具(基于JSEncrypt)
 */
const JSEncrypt = require('jsencrypt')
const { PRIVATE_KEY } = require('../config')

/**
 * RSA解密
 * @param {string} txt - 加密的文本
 * @returns {string} 解密后的文本
 */
const decrypt = txt => {
  const encrypt = new JSEncrypt()
  encrypt.setPrivateKey(PRIVATE_KEY)
  const result = encrypt.decrypt(txt)
  if (!result) {
    throw new Error('RSA解密失败')
  }
  return result
}

module.exports = {
  decrypt
}

七、中间件开发

1. 请求体解密中间件

// middlewares/index.js
/**
 * 中间件集合
 */
const { decryptWithAes, decryptBase64 } = require('../utils/crypto')
const { decrypt } = require('../utils/jsencrypt')

/**
 * 请求体解密中间件
 * 用于解密前端加密的请求参数
 */
const decryptedBody = (req, res, next) => {
  // 检查是否需要解密
  const isEncrypt = req.headers['isencrypt'] === 'true'
  const encryptKey = req.headers['encrypt-key']

  if (isEncrypt && encryptKey) {
    try {
      // 解密流程:RSA解密key → Base64解码 → AES解密body
      const rsaDecryptedKey = decrypt(encryptKey)
      const aesKey = decryptBase64(rsaDecryptedKey)
      req.body = JSON.parse(decryptWithAes(req.body, aesKey))
    } catch (error) {
      console.error('请求体解密失败:', error)
      return res.status(400).json({
        code: 400,
        msg: '请求参数解密失败'
      })
    }
  }
  next()
}

module.exports = {
  decryptedBody
}

2. 登录验证中间件

// middlewares/validator/login.js
/**
 * 登录参数验证中间件
 */
const HTTP_STATUS = require('../../utils/httpStatus')
const {
  SystemModel: { UsersModel },
  AuthModel: { TenantModel }
} = require('../../models')
const captchaRedis = require('../../redis/captcha')
const { STATUS } = require('../../enums/user')

const validLogin = async (req, res, next) => {
  const { username, password, tenantId, code, uuid } = req.body

  // 1. 验证租户ID
  if (!tenantId) {
    return res.error('租户ID不能为空')
  }

  const tenant = await TenantModel.findOne({ tenantId })
  if (!tenant) {
    return res.error('租户ID不存在', HTTP_STATUS.NOT_FOUND)
  }

  // 2. 验证验证码
  if (!code || !uuid) {
    return res.error('验证码或UUID不能为空')
  }

  try {
    const captchaData = await captchaRedis.getCaptcha(uuid)
    if (!captchaData) {
      return res.error('验证码过期或不存在', HTTP_STATUS.NOT_FOUND)
    }
    if (captchaData.code !== code.toLowerCase()) {
      return res.error('验证码错误')
    }
    // 验证通过后删除验证码
    await captchaRedis.deleteCaptcha(uuid)
  } catch (error) {
    console.error('验证验证码失败:', error)
    return res.error('验证验证码失败')
  }

  // 3. 验证用户名密码
  if (!username || !password) {
    return res.error('用户名或密码不能为空')
  }

  const user = await UsersModel.findOne({ 
    userName: username,
    tenantId 
  }).select('+password') // 显式包含密码字段

  if (!user) {
    return res.error('用户名或密码错误')
  }

  // 4. 检查用户状态
  if (user.status !== STATUS.NORMAL) {
    return res.error('用户已停用')
  }

  // 5. 验证密码(注意:这里密码是明文,实际应用中应该加密对比)
  if (user.password !== password) {
    return res.error('用户名或密码错误')
  }

  // 将用户信息挂载到请求对象,供后续使用
  req.user = user
  next()
}

module.exports = {
  validLogin
}

八、路由配置

// routes/auth/index.js
/**
 * 认证相关路由配置
 */
const express = require('express')
const router = express.Router()
const authController = require('../../controllers/auth')
const { decryptedBody } = require('../../middlewares')
const { validLogin } = require('../../middlewares/validator/login')

// 验证码接口
router.get('/code', authController.getCaptcha)

// 登录接口(解密 → 验证 → 登录)
router.post('/login', decryptedBody, validLogin, authController.login)

// 登出接口
router.post('/logout', authController.logout)

// 商户管理子路由
router.use('/tenant', require('./tenant'))

module.exports = router

九、响应格式示例

1. 验证码响应

{
  "code": 200,
  "data": {
    "captchaEnabled": true,
    "img": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUwIiBoZWlnaHQ9IjUwIj4KICA8dGV4dCB4PSI1MCIgeT0iMzAiIGZvbnQtc2l6ZT0iMjAiPjEyMzQ8L3RleHQ+Cjwvc3ZnPg==",
    "uuid": "session-id-123456"
  },
  "msg": "操作成功"
}

2. 登录成功响应

{
  "code": 200,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "client_id": "web-client",
    "expire_in": 604800,
    "user": {
      "userId": 1,
      "userName": "admin",
      "nickName": "管理员",
      "avatar": "https://example.com/avatar.jpg"
    }
  },
  "msg": "登录成功"
}

3. 错误响应示例

{
  "code": 401,
  "msg": "用户名或密码错误"
}

十、安全注意事项

1. 生产环境配置

  • 将敏感密钥存储在环境变量中
  • 定期更换JWT_SECRET和PRIVATE_KEY
  • 启用HTTPS传输加密
  • 配置合适的Token过期时间

2. 性能优化建议

  • 对高频接口添加缓存
  • 限制验证码请求频率
  • 监控Redis连接状态
  • 实现Token自动续期机制

3. 安全增强措施

  • 密码应该加密存储(使用bcrypt或argon2)
  • 添加登录失败次数限制
  • 实现登录设备管理
  • 记录详细的登录日志

此认证系统设计提供了完整的用户登录、验证码、Token管理和安全防护功能,适用于企业级应用场景。# 用户认证系统设计文档