商户列表、登录、登出、验证码接口设计文档
一、系统架构概述
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管理和安全防护功能,适用于企业级应用场景。# 用户认证系统设计文档