You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

178 lines
4.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 身份认证和权限验证中间件
*
* 功能:
* 1. 验证用户是否已登录JWT token验证
* 2. 验证用户是否有足够的权限访问资源
*/
const { verifyToken } = require('../utils/encryption');
const { AuthenticationError, AuthorizationError } = require('./errorHandler');
/**
* 身份认证中间件
* 验证用户是否已登录通过JWT token
*
* 说明:
* - 前端在请求头中携带 token: "Bearer xxxxx"
* - 服务器验证token是否有效
* - 验证通过后,将用户信息存入 req.user
*
* 使用示例:
* router.get('/profile', authenticate, getProfile);
*/
const authenticate = async (req, res, next) => {
try {
// 从请求头获取token
const authHeader = req.headers.authorization;
// 检查是否提供了token
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new AuthenticationError('未提供认证令牌,请先登录');
}
// 提取token去掉"Bearer "前缀)
const token = authHeader.substring(7);
// 验证token
const decoded = verifyToken(token);
// 将解密后的用户信息存入req对象供后续中间件使用
req.user = {
id: decoded.id,
username: decoded.username,
role: decoded.role
};
// 继续执行下一个中间件
next();
} catch (error) {
// 如果是我们定义的错误,直接传递
if (error instanceof AuthenticationError) {
next(error);
} else {
// 其他错误统一处理为认证失败
next(new AuthenticationError(error.message));
}
}
};
/**
* 角色权限验证中间件工厂函数
* 生成一个验证特定角色权限的中间件
*
* @param {array} allowedRoles - 允许访问的角色列表
* @returns {function} Express中间件
*
* 说明:
* - 必须在 authenticate 之后使用
* - 检查用户角色是否在允许的角色列表中
*
* 使用示例:
* // 只允许管理员访问
* router.delete('/users/:id', authenticate, checkRole(['admin']), deleteUser);
*
* // 允许管理员和服务员访问
* router.post('/orders', authenticate, checkRole(['admin', 'waiter']), createOrder);
*/
const checkRole = (allowedRoles) => {
return (req, res, next) => {
try {
// 确保已经通过身份认证
if (!req.user) {
throw new AuthenticationError('请先登录');
}
// 检查用户角色是否在允许列表中
if (!allowedRoles.includes(req.user.role)) {
throw new AuthorizationError(`此操作需要以下角色之一: ${allowedRoles.join(', ')}`);
}
// 权限验证通过,继续执行
next();
} catch (error) {
next(error);
}
};
};
/**
* 可选的身份认证中间件
* 如果提供了token就验证没提供也不报错
*
* 说明:适用于"登录后有额外功能,不登录也能访问"的场景
* 例如:浏览菜单不需要登录,但登录后可以看到会员价格
*
* 使用示例:
* router.get('/menu', optionalAuthenticate, getMenu);
*/
const optionalAuthenticate = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
// 如果没有提供token直接跳过
if (!authHeader || !authHeader.startsWith('Bearer ')) {
req.user = null;
return next();
}
// 如果提供了token验证它
const token = authHeader.substring(7);
const decoded = verifyToken(token);
req.user = {
id: decoded.id,
username: decoded.username,
role: decoded.role
};
next();
} catch (error) {
// token无效也不报错只是不设置用户信息
req.user = null;
next();
}
};
/**
* 检查是否是资源所有者或管理员
* 用于保护用户只能操作自己的资源
*
* @param {string} userIdParam - URL参数中用户ID的字段名
* @returns {function} Express中间件
*
* 使用示例:
* // 用户只能查看自己的订单,或者管理员可以查看所有订单
* router.get('/users/:userId/orders', authenticate, checkOwnerOrAdmin('userId'), getOrders);
*/
const checkOwnerOrAdmin = (userIdParam = 'id') => {
return (req, res, next) => {
try {
if (!req.user) {
throw new AuthenticationError('请先登录');
}
const resourceUserId = parseInt(req.params[userIdParam]);
const currentUserId = req.user.id;
const isAdmin = req.user.role === 'admin';
// 如果是管理员,或者是资源所有者,允许访问
if (isAdmin || currentUserId === resourceUserId) {
next();
} else {
throw new AuthorizationError('您只能访问自己的资源');
}
} catch (error) {
next(error);
}
};
};
// 导出所有中间件
module.exports = {
authenticate,
checkRole,
optionalAuthenticate,
checkOwnerOrAdmin
};