Files
competition-website/app/middleware/auth.js
WorkBuddy a303cf2d53 chore: 后端首次基线提交
运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。
已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。
2026-08-17 12:25:42 +08:00

117 lines
3.7 KiB
JavaScript
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.
'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;
}
};
};