13.2 鉴权中间件
最常用的中间件之一:检查用户是否登录。
实现思路
- 从请求中取出 token(从 header、cookie 或 query 里)
- 用 token 查找对应的用户/会话
- 找到就把用户信息挂到 request 上,继续执行
- 找不到就返回未登录错误
代码实现
src/middlewares/auth.ts:
import type { RequestHandler } from 'express';
import { sendError } from '@/utils/response.js';
import type { SessionModel } from '@/models/session/session-model.js';
import type { UserModel } from '@/models/user/user-model.js';
// 白名单:不需要登录的接口
const WHITELIST = [
'POST /api/v1/auth/login',
'POST /api/v1/auth/register',
'GET /api/v1/health',
];
export function requireAuth(sessions: SessionModel, users: UserModel): RequestHandler {
return (req, res, next) => {
// 白名单直接放行
const key = `${req.method} ${req.path}`;
if (WHITELIST.includes(key)) {
return next();
}
// 从 header 中取 token
const token = req.headers['x-token'] as string | undefined;
if (!token) {
return sendError(res, { status: 4, msg: '未登录', httpStatus: 401 });
}
// 查找会话
const session = sessions.findByToken(token);
if (!session) {
return sendError(res, { status: 4, msg: '登录已过期', httpStatus: 401 });
}
// 查找用户
const user = users.findById(session.userId);
if (!user) {
return sendError(res, { status: 4, msg: '用户不存在', httpStatus: 401 });
}
// 把用户信息挂到 request 上,后面的中间件和路由都能用
(req as any).currentUser = user;
(req as any).sessionToken = token;
next();
};
}
类型扩展
用 TypeScript 的话,最好扩展一下 Request 类型,而不是用 as any。
src/types/express.d.ts:
import type { User } from '@/models/user/user.js';
declare global {
namespace Express {
interface Request {
currentUser?: User;
sessionToken?: string;
}
}
}
export {};
这样后面在 Controller 里就能直接用 req.currentUser,还有类型提示。
使用方式
// app.ts 里注册
app.use('/api', requireAuth(models.sessions, models.users));
或者只给部分路由加:
// 路由文件里
const router = Router();
// 不需要登录的
router.post('/login', authController.login);
// 需要登录的
router.use(requireAuth(sessions, users));
router.get('/profile', userController.profile);
13.3 权限校验中间件
鉴权是"有没有登录",权限是"有没有权限做这件事"。
基于角色的权限控制(RBAC)
// src/middlewares/require-role.ts
import type { RequestHandler } from 'express';
import { sendError } from '@/utils/response.js';
export function requireRole(...roles: string[]): RequestHandler {
return (req, res, next) => {
const user = req.currentUser;
if (!user) {
return sendError(res, { status: 4, msg: '未登录', httpStatus: 401 });
}
if (!roles.includes(user.role)) {
return sendError(res, { status: 3, msg: '无权限', httpStatus: 403 });
}
next();
};
}
使用:
// 只有管理员能删除项目
router.delete('/:id', requireRole('admin'), projectController.remove);
13.4 日志中间件
记录每个请求的方法、URL、状态码、耗时。
src/middlewares/logger.ts:
import type { RequestHandler } from 'express';
export const requestLogger: RequestHandler = (req, res, next) => {
const start = Date.now();
const { method, url, ip } = req;
res.on('finish', () => {
const duration = Date.now() - start;
const { statusCode } = res;
const log = `[${method}] ${url} ${statusCode} - ${duration}ms - ${ip}`;
// 根据状态码决定日志级别
if (statusCode >= 500) {
console.error(log);
} else if (statusCode >= 400) {
console.warn(log);
} else {
console.log(log);
}
});
next();
};
生产环境可以把日志写入文件或上报到日志服务。常用的库:
- morgan:成熟的访问日志中间件
- winston / pino:通用日志库,支持多级别、多输出
13.5 限流中间件
防止接口被刷,限制某个 IP 在一段时间内的请求次数。
import type { RequestHandler } from 'express';
import { sendError } from '@/utils/response.js';
// 简单的内存限流(不适合分布式部署)
export function rateLimit(options: { max: number; windowMs: number }): RequestHandler {
const hits = new Map<string, { count: number; resetAt: number }>();
return (req, res, next) => {
const key = req.ip!;
const now = Date.now();
const record = hits.get(key);
// 窗口过期,重置
if (!record || record.resetAt < now) {
hits.set(key, { count: 1, resetAt: now + options.windowMs });
return next();
}
// 超限
if (record.count >= options.max) {
return sendError(res, {
status: 429,
msg: '请求过于频繁,请稍后再试',
httpStatus: 429,
});
}
record.count++;
next();
};
}
使用:
// 登录接口限流:1 分钟最多 5 次
router.post('/login', rateLimit({ max: 5, windowMs: 60_000 }), authController.login);
生产环境推荐用 express-rate-limit 库,功能更完善。分布式部署需要用 Redis 存储。
13.6 CORS 中间件
跨域资源共享,前端后端不同域名时需要配置。
最简单的方式:用 cors 库。
npm install cors
npm install -D @types/cors
import cors from 'cors';
// 允许所有来源(开发环境用)
app.use(cors());
// 或者更严格的配置
app.use(
cors({
origin: ['https://example.com'], // 允许的前端域名
credentials: true, // 允许带 Cookie
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}),
);
13.7 错误处理中间件
放在所有路由之后,专门捕获错误。
import type { ErrorRequestHandler } from 'express';
import { AppError } from '@/errors/app-error.js';
import { sendError } from '@/utils/response.js';
export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
// 业务错误
if (error instanceof AppError) {
return sendError(res, {
status: error.status,
msg: error.message,
});
}
// 参数校验错误(如 zod 抛出的)
if (error.name === 'ZodError') {
return sendError(res, {
status: 2,
msg: '参数错误',
data: error.errors,
});
}
// 未知错误
console.error('[Server Error]', error);
return sendError(res, {
status: 500,
msg: '服务器内部错误',
httpStatus: 500,
});
};
注意:错误处理中间件必须是 4 个参数 (error, req, res, next),Express 才能识别。
13.8 中间件的执行顺序
一张图总结:
请求进来
↓
1. CORS / Helmet(安全头)
↓
2. express.json()(解析请求体)
↓
3. Cookie / Session 解析
↓
4. 请求日志
↓
5. 限流
↓
6. 鉴权(登录检查)
↓
7. 权限校验
↓
8. 业务路由
↓
9. 404
↓
10. 错误处理
前面的中间件可以决定是否继续往下走。比如鉴权失败就直接返回,不会到业务路由。
13.9 小结
- 鉴权中间件:检查登录状态,把用户信息挂到 request 上
- 权限中间件:检查是否有操作权限
- 日志中间件:记录请求信息和耗时
- 限流中间件:防止接口被恶意刷
- CORS 中间件:处理跨域
- 错误处理中间件:统一捕获和返回错误
- 中间件的执行顺序很重要,按代码顺序依次执行