117 lines
3.7 KiB
JavaScript
117 lines
3.7 KiB
JavaScript
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const jwt = require('jsonwebtoken');
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* JWT 认证中间件
|
|||
|
|
*
|
|||
|
|
* 功能:
|
|||
|
|
* 1. 从请求头 Authorization 中提取 Bearer Token
|
|||
|
|
* 2. 验证 JWT 的有效性
|
|||
|
|
* 3. 将解码后的用户信息挂载到 ctx.state.currentUser
|
|||
|
|
* 4. 支持管理员角色校验(admin 路由专用)
|
|||
|
|
* 5. 验证失败返回统一错误响应
|
|||
|
|
*
|
|||
|
|
* 使用方式:
|
|||
|
|
* - 选手认证:app.middleware.auth()
|
|||
|
|
* - 管理员认证:app.middleware.auth({ adminOnly: true })
|
|||
|
|
*
|
|||
|
|
* ctx.state.currentUser 数据结构:
|
|||
|
|
* {
|
|||
|
|
* uid: 账号 uid,
|
|||
|
|
* id_number: 身份证号,
|
|||
|
|
* role: 'contestant' | 'admin'
|
|||
|
|
* }
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
module.exports = (options = {}, app) => {
|
|||
|
|
return async function authMiddleware(ctx, next) {
|
|||
|
|
// 从 ctx.app.config 中获取 JWT 密钥
|
|||
|
|
const appConfig = app || ctx.app;
|
|||
|
|
const { secret } = appConfig.config.jwt;
|
|||
|
|
|
|||
|
|
// 获取 Authorization 请求头;同时支持 Query ?token= 回退(用于跨设备直链下载,
|
|||
|
|
// 如 iOS Safari 不响应 blob + a.download 时改用浏览器原生导航下载的场景)。
|
|||
|
|
const authorization = ctx.get('Authorization') ||
|
|||
|
|
(ctx.query && ctx.query.token ? `Bearer ${ctx.query.token}` : '');
|
|||
|
|
|
|||
|
|
// 检查 Authorization 头是否存在
|
|||
|
|
if (!authorization) {
|
|||
|
|
ctx.status = 401;
|
|||
|
|
ctx.body = {
|
|||
|
|
code: 401,
|
|||
|
|
message: '未提供认证令牌,请先登录',
|
|||
|
|
};
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 提取 Bearer Token(格式:Bearer <token>)
|
|||
|
|
const tokenParts = authorization.split(' ');
|
|||
|
|
if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer') {
|
|||
|
|
ctx.status = 401;
|
|||
|
|
ctx.body = {
|
|||
|
|
code: 401,
|
|||
|
|
message: '认证令牌格式错误,应为 Bearer <token>',
|
|||
|
|
};
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const token = tokenParts[1];
|
|||
|
|
|
|||
|
|
// 验证 JWT Token
|
|||
|
|
try {
|
|||
|
|
const decoded = jwt.verify(token, secret);
|
|||
|
|
|
|||
|
|
// 将解码后的用户信息挂载到 ctx.state.currentUser
|
|||
|
|
ctx.state.currentUser = {
|
|||
|
|
id: decoded.id || decoded.uid || 0,
|
|||
|
|
uid: decoded.uid || decoded.id || 0,
|
|||
|
|
id_number: decoded.id_number || '',
|
|||
|
|
phone: decoded.phone || '',
|
|||
|
|
role: decoded.role,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 如果启用了管理员模式,校验当前用户是否为管理员
|
|||
|
|
if (options.adminOnly) {
|
|||
|
|
if (ctx.state.currentUser.role !== 'admin') {
|
|||
|
|
ctx.status = 403;
|
|||
|
|
ctx.body = {
|
|||
|
|
code: 403,
|
|||
|
|
message: '无权限访问,需要管理员权限',
|
|||
|
|
};
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 继续执行后续中间件和控制器
|
|||
|
|
await next();
|
|||
|
|
} catch (err) {
|
|||
|
|
// 仅当错误确实是 JWT 校验问题(过期/签名无效)时,才返回 401「认证失败」。
|
|||
|
|
// 注意:try 内部包含 `await next()`,即后续中间件与控制器也在同一 try 中。
|
|||
|
|
// 若下游控制器/服务抛出异常(如文件读取失败、数据库异常等),绝不能被
|
|||
|
|
// 这里误吞并伪装成 401,否则前端会误判「登录过期」把用户踢回登录页。
|
|||
|
|
// 因此:非 JWT 类错误必须重新抛出,交由 Egg 全局错误处理(返回 500 并写日志)。
|
|||
|
|
if (err.name === 'TokenExpiredError') {
|
|||
|
|
// Token 已过期
|
|||
|
|
ctx.status = 401;
|
|||
|
|
ctx.body = {
|
|||
|
|
code: 401,
|
|||
|
|
message: '认证令牌已过期,请重新登录',
|
|||
|
|
};
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (err.name === 'JsonWebTokenError') {
|
|||
|
|
// Token 无效(签名错误、格式错误等)
|
|||
|
|
ctx.status = 401;
|
|||
|
|
ctx.body = {
|
|||
|
|
code: 401,
|
|||
|
|
message: '无效的认证令牌',
|
|||
|
|
};
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
// 其它异常(下游业务逻辑抛出):原样抛出,避免被误报为 401
|
|||
|
|
throw err;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
};
|