Express框架的中间件错误处理和路由保护,核心就是两件事:一是用统一的错误处理中间件捕获所有未被捕获的异常,防止服务崩溃并返回友好的错误响应;二是在路由层通过身份验证、权限校验、输入验证等手段,确保只有合法请求才能进入业务逻辑。具体做法是在app.js末尾添加一个四参数的错误处理中间件,同时在需要保护的路由前挂载auth中间件、参数校验中间件,并配合helmet、cors、rate-limit等安全中间件形成多层防护体系。下面我把每一个环节拆开讲透。
Express中间件错误处理的本质与标准写法
Express的中间件本质上就是一个函数,签名为(req, res, next)。错误处理中间件和普通中间件唯一的区别是它有四个参数:(err, req, res, next)。Express会自动跳过所有普通中间件,直到找到第一个四参数的错误处理函数。这意味着你必须把错误处理中间件放在所有路由定义之后,否则它永远不会被触发。
// 标准错误处理中间件,放在所有路由之后
app.use((err, req, res, next) => {
// 记录错误日志,生产环境不要把详细堆栈暴露给客户端
console.error(err.stack);
// 判断错误类型
if (err.name === 'ValidationError') {
return res.status(400).json({
success: false,
message: '参数验证失败',
errors: err.errors
});
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({
success: false,
message: '未授权访问'
});
}
// 默认500错误
res.status(err.status || 500).json({
success: false,
message: err.message || '服务器内部错误'
});
});
很多开发者犯的第一个错误就是把错误处理中间件放在路由前面,或者用try-catch包裹每个路由函数却不调用next(err)。正确的做法是:在异步路由中用try-catch捕获,然后手动调用next(err)把错误传递下去。
// 异步路由中的错误处理示范
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
// 主动抛出错误,而不是直接返回404
throw new Error('用户不存在');
}
res.json({ success: true, data: user });
} catch (error) {
// 关键:把错误交给Express的错误处理中间件
next(error);
}
});
自定义错误类与结构化错误体系
在中大型项目中,光靠if-else判断err.name是不够的。你需要建立一套自定义错误类体系,让错误信息结构化、可追溯。建议创建一个errors目录,里面放不同类型的错误类。
// errors/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true; // 区分可预期错误和程序bug
Error.captureStackTrace(this, this.constructor);
}
}
// errors/ValidationError.js
class ValidationError extends AppError {
constructor(message, errors) {
super(message, 400);
this.errors = errors;
}
}
// errors/UnauthorizedError.js
class UnauthorizedError extends AppError {
constructor(message = '请先登录') {
super(message, 401);
}
}
// errors/ForbiddenError.js
class ForbiddenError extends AppError {
constructor(message = '无权执行此操作') {
super(message, 403);
}
}
module.exports = { AppError, ValidationError, UnauthorizedError, ForbiddenError };
这样做的好处是:业务代码里只需要throw new ValidationError('邮箱格式错误', errors),错误处理中间件里通过instanceof判断类型即可,代码可读性和维护性大幅提升。
路由保护的第一层:身份认证中间件
路由保护最基础的一层就是身份认证。通常使用JWT(JSON Web Token)方案。用户登录后签发token,后续请求在Header中携带Authorization: Bearer xxx,中间件验证token有效性后才放行。
// middleware/auth.js
const jwt = require('jsonwebtoken');
const { UnauthorizedError } = require('../errors');
const authMiddleware = (req, res, next) => {
// 从Header获取token
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedError('缺少认证令牌');
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // 把用户信息挂到req上
next();
} catch (err) {
// token过期或伪造
throw new UnauthorizedError('令牌无效或已过期');
}
};
module.exports = authMiddleware;
使用方式非常简单,在需要保护的路由前加上这个中间件即可。
// 保护特定路由
app.get('/api/profile', authMiddleware, (req, res) => {
res.json({ success: true, data: req.user });
});
// 保护一组路由
app.use('/api/admin', authMiddleware, adminRoutes);
路由保护的第二层:权限校验中间件
认证解决的是"你是谁"的问题,权限解决的是"你能干什么"的问题。很多系统有管理员、普通用户、访客等角色,不同角色能访问的资源不同。权限中间件通常接收一个或多个允许的角色作为参数。
// middleware/authorize.js
const { ForbiddenError } = require('../errors');
const authorize = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
throw new UnauthorizedError('请先登录');
}
if (!allowedRoles.includes(req.user.role)) {
throw new ForbiddenError('权限不足');
}
next();
};
};
module.exports = authorize;
实际使用时可以这样组合:
// 只有管理员能删除用户
app.delete('/api/users/:id', authMiddleware, authorize('admin'), async (req, res, next) => {
try {
await User.findByIdAndDelete(req.params.id);
res.json({ success: true, message: '删除成功' });
} catch (err) {
next(err);
}
});
路由保护的第三层:输入验证与参数校验
永远不要信任前端传来的数据。Express本身不做参数校验,你需要借助joi、express-validator或zod等库来验证请求体、查询参数、路由参数的合法性。这一层放在业务逻辑之前,能拦截大量恶意或错误请求。
// 使用express-validator的示例
const { body, param, validationResult } = require('express-validator');
// 创建用户时的验证规则
const createUserValidation = [
body('username')
.trim()
.isLength({ min: 3, max: 20 })
.withMessage('用户名长度必须在3-20个字符之间'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('请输入有效的邮箱地址'),
body('password')
.isLength({ min: 8 })
.matches(/\d/)
.withMessage('密码至少8位且必须包含数字'),
body('role')
.isIn(['user', 'admin'])
.withMessage('角色只能是user或admin')
];
// 验证结果处理中间件
const handleValidation = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const errorMessages = errors.array().map(e => e.msg);
throw new ValidationError('参数验证失败', errorMessages);
}
next();
};
// 路由使用
app.post('/api/users', createUserValidation, handleValidation, async (req, res, next) => {
try {
const user = await User.create(req.body);
res.status(201).json({ success: true, data: user });
} catch (err) {
next(err);
}
});
安全加固中间件:helmet、cors、rate-limit
除了业务层面的错误处理和路由保护,Express还有几个必须配置的安全中间件。helmet帮你设置各种HTTP安全头,防止XSS、点击劫持等攻击;cors控制跨域请求;rate-limit限制单个IP的请求频率,防止暴力破解和DDoS。
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
// 安全头设置
app.use(helmet());
// 跨域配置,生产环境要限制具体域名
app.use(cors({
origin: ['https://yourdomain.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE']
}));
// 限流:每IP每15分钟最多100次请求
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { success: false, message: '请求过于频繁,请稍后再试' }
});
app.use('/api/', limiter);
全局错误处理与异步错误的坑
Express 4.x有一个已知问题:如果异步路由中抛出错误但没有用try-catch包裹,或者用了try-catch但忘记调用next(err),Express不会自动捕获这个错误,进程可能直接崩溃。解决方案有两个:一是使用express-async-errors这个包,它会自动包装异步路由函数;二是在最外层加一个uncaughtException监听,但这只是最后的兜底,不推荐作为主要手段。
// 方案一:使用express-async-errors(推荐)
require('express-async-errors');
// 现在所有async路由函数抛出的错误都会自动传到错误处理中间件
app.get('/api/data', async (req, res) => {
const data = await fetchData(); // 如果这里抛错,自动被捕获
res.json(data);
});
// 方案二:全局兜底(不推荐作为主要方案)
process.on('uncaughtException', (err) => {
console.error('未捕获异常:', err);
// 记录日志、发送告警、优雅关闭
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error('未处理的Promise拒绝:', reason);
process.exit(1);
});
生产环境的错误处理最佳实践
开发环境和生产环境的错误处理策略应该不同。开发环境需要看到详细的错误堆栈方便调试,生产环境则要隐藏内部细节,只返回通用错误信息。可以通过NODE_ENV环境变量来区分。
// 生产环境错误处理中间件(精简版)
app.use((err, req, res, next) => {
// 生产环境不暴露堆栈
if (process.env.NODE_ENV === 'production') {
// 记录到日志系统(如winston、pino)
logger.error({
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip
});
return res.status(err.status || 500).json({
success: false,
message: '服务器繁忙,请稍后重试'
});
}
// 开发环境返回详细信息
res.status(err.status || 500).json({
success: false,
message: err.message,
stack: err.stack,
errors: err.errors
});
});
路由保护的架构建议:分层设计
一个成熟的Express项目,中间件和路由应该按职责分层。建议的目录结构是:中间件放在middleware目录下按功能分类(auth、authorize、validation、errorHandler),路由按模块拆分(userRoutes、orderRoutes),控制器放在controllers目录。这样每个路由文件只负责定义路径和挂载中间件,具体业务逻辑全部放到控制器里,错误处理统一在app.js末尾的全局中间件中完成。
最后强调一点:错误处理不是事后补救,而是架构设计的一部分。从项目第一天起就建立统一的错误类、标准化的错误响应格式、分层的路由保护机制,后面扩展功能时才不会越来越乱。Express本身很轻量,它不帮你做这些,但正因为轻量,你才有完全的控制权去构建一套健壮的错误处理和安全防护体系。
