Files
xiaocai/server/src/middleware/auth.ts
wangxiaogang d170df5c51 fix(iter-v2): T01 QA fixes - race condition, hook regex, deleted user check
- BUG-1: Pre-commit hook allows .env.example (grep -v filter)
- BUG-2: /auth/refresh uses transaction + FOR UPDATE to prevent race condition
- WARN-1: Auth middleware checks deleted_at with 5-min TTL cache
- WARN-2: PUT /admin/users/:id/status filters deleted_at IS NULL
- WARN-3: Install node-cron@^3.0.3
2026-06-11 09:34:27 +08:00

90 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Request, Response, NextFunction } from 'express'
import { createHmac, timingSafeEqual } from 'crypto'
import { TOKEN_SECRET } from '../config/token'
import pool from '../db/connection'
export interface AuthRequest extends Request {
userId?: number
}
const TOKEN_EXPIRY = 2 * 60 * 60 * 1000 // 2 hours
// 不需要认证的路径
const PUBLIC_PATHS = ['/api/auth/login', '/api/auth/demo-login', '/api/auth/refresh', '/api/health']
/** 已删除用户 ID 缓存5 分钟 TTL */
const deletedUserCache = new Map<number, number>() // userId → cachedAt
async function isUserDeleted(userId: number): Promise<boolean> {
const cached = deletedUserCache.get(userId)
if (cached && Date.now() - cached < 5 * 60 * 1000) {
return true // 仍在缓存中,视为已删除
}
const [rows] = await pool.query('SELECT deleted_at FROM users WHERE id = ?', [userId])
if (!(rows as any[]).length || (rows as any[])[0].deleted_at !== null) {
deletedUserCache.set(userId, Date.now())
return true
}
deletedUserCache.delete(userId)
return false
}
export async 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 (constant-time comparison)
const payload = `${userId}:${timestamp}`
const expectedSig = createHmac('sha256', TOKEN_SECRET).update(payload).digest('hex')
const sigBuf = Buffer.from(signature, 'hex')
const expectedBuf = Buffer.from(expectedSig, 'hex')
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
return res.status(401).json({ code: 40100, message: 'token无效' })
}
// 检查用户是否已被软删除
if (await isUserDeleted(userId)) {
return res.status(401).json({ code: 40100, message: '账号已禁用' })
}
req.userId = userId
} catch {
return res.status(401).json({ code: 40100, message: 'token无效' })
}
next()
}