Files
api_user/middleware/auth.js
T
2026-09-01 21:49:41 +08:00

106 lines
3.0 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.
import { DBModel } from '../models/index.js';
import ResponseUtil from '../utils/api_response.js';
import { hashToken } from '../utils/crypto.js';
/**
* 从请求中提取 token(优先 Authorization header
*/
function extractToken(ctx) {
// 优先从 Authorization: Bearer xxx 获取
const authHeader = ctx.header?.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.slice(7);
}
// 兼容旧方式:body/query/header 中的 token 字段
return ctx.request.body?.token
|| ctx.request.query?.token
|| ctx.header?.token;
}
/**
* 按 token 查找用户
* DB 中存储的是 token 的 sha256 哈希;兼容旧明文 token,命中后自动迁移为哈希
*/
async function findUserByToken(rawToken) {
const hashed = await hashToken(rawToken);
let user = await DBModel.User.findOne({ 'security.token': hashed });
if (!user) {
// 兼容旧明文 token
user = await DBModel.User.findOne({ 'security.token': rawToken });
if (user) {
user.security.token = hashed;
await user.save();
}
}
return user;
}
/**
* 认证中间件 - 验证 token 并挂载用户信息到 ctx
* 可选参数:
* - required: 是否必须登录(默认 true)
* - roles: 允许的角色列表(可选)
*/
function auth(options = {}) {
const { required = true, roles } = options;
return async (ctx, next) => {
const token = extractToken(ctx);
if (!token) {
if (!required) {
ctx.state.user = null;
return await next();
}
return ResponseUtil.unauthorized(ctx, '缺少认证 token');
}
const user = await findUserByToken(token);
if (!user) {
return ResponseUtil.unauthorized(ctx, '用户未登录或 token 无效');
}
if (user.security.tokenExpiry && new Date() > user.security.tokenExpiry) {
return ResponseUtil.unauthorized(ctx, '登录已过期,请重新登录');
}
if (user.status.account === 'lock') {
return ResponseUtil.forbidden(ctx, '账户已被锁定');
}
// 角色检查
if (roles && roles.length > 0) {
const hasRole = roles.some(role => {
// 检查 app 字段中的角色
return Object.values(user.app || {}).some(appData =>
appData && Array.isArray(appData.role) && appData.role.includes(role)
);
});
if (!hasRole) {
return ResponseUtil.forbidden(ctx, '权限不足');
}
}
ctx.state.user = user;
await next();
};
}
/**
* 返回用户安全对象(去除密码、token 等敏感字段)
* 注意:登录/注册响应中的 token 由 handler 单独附加
*/
function sanitizeUser(user) {
const obj = user.toObject ? user.toObject() : { ...user };
delete obj.security?.passwd;
delete obj.security?.passwdSalt;
delete obj.security?.token;
delete obj.security?.tokenExpiry;
delete obj.security?.passwordResetToken;
delete obj.security?.passwordResetExpiry;
return obj;
}
export { auth, extractToken, sanitizeUser, findUserByToken };