feat: 小菜记账 v1.0 - 完整功能实现
核心功能: - 记账CRUD(支出/收入/分类/备注/日期) - 统计分析(概览/分类占比/每日趋势) - 预算管理(按月设置/进度条/超支提醒) - 数据导出CSV 安全与认证: - HMAC-SHA256签名token认证 - 用户数据隔离 - 输入验证与错误处理 - CORS配置 前端优化: - 骨架屏加载 - 账单按日期分组 - 预算页面重构(快捷预设+Numpad) - SvgIcon组件(H5+微信双端适配) - 下拉刷新 后端优化: - 共享日期工具函数 - 数据库连接池优化 - 健康检查端点 - 优雅关闭处理 技术栈: - 前端:Uni-app (Vue 3 + Pinia) - 后端:Node.js + Express + MySQL
This commit is contained in:
64
server/src/middleware/auth.ts
Normal file
64
server/src/middleware/auth.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import { createHmac } from 'crypto'
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
userId?: number
|
||||
}
|
||||
|
||||
const TOKEN_SECRET = process.env.TOKEN_SECRET || 'xiaocai-token-secret-change-in-production'
|
||||
const TOKEN_EXPIRY = 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||
|
||||
// 不需要认证的路径
|
||||
const PUBLIC_PATHS = ['/api/auth/login', '/api/health']
|
||||
|
||||
export function authMiddleware(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
// 公开路径跳过认证
|
||||
if (PUBLIC_PATHS.some(p => req.path === p)) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// 从 Authorization header 获取 token
|
||||
const authHeader = req.headers.authorization
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ code: 40100, message: '未登录' })
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7)
|
||||
try {
|
||||
const decoded = Buffer.from(token, 'base64').toString('utf-8')
|
||||
const parts = decoded.split(':')
|
||||
if (parts.length !== 3) {
|
||||
return res.status(401).json({ code: 40100, message: 'token无效' })
|
||||
}
|
||||
|
||||
const [userIdStr, timestamp, signature] = parts
|
||||
const userId = parseInt(userIdStr)
|
||||
const ts = parseInt(timestamp)
|
||||
|
||||
if (isNaN(userId) || isNaN(ts)) {
|
||||
return res.status(401).json({ code: 40100, message: 'token无效' })
|
||||
}
|
||||
|
||||
// Check expiry and reject future timestamps
|
||||
const now = Date.now()
|
||||
if (now - ts > TOKEN_EXPIRY) {
|
||||
return res.status(401).json({ code: 40101, message: 'token已过期' })
|
||||
}
|
||||
if (ts > now + 60000) {
|
||||
return res.status(401).json({ code: 40100, message: 'token无效' })
|
||||
}
|
||||
|
||||
// Verify HMAC signature (full length)
|
||||
const payload = `${userId}:${timestamp}`
|
||||
const expectedSig = createHmac('sha256', TOKEN_SECRET).update(payload).digest('hex')
|
||||
if (signature !== expectedSig) {
|
||||
return res.status(401).json({ code: 40100, message: 'token无效' })
|
||||
}
|
||||
|
||||
req.userId = userId
|
||||
} catch {
|
||||
return res.status(401).json({ code: 40100, message: 'token无效' })
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
Reference in New Issue
Block a user