106 lines
3.0 KiB
JavaScript
106 lines
3.0 KiB
JavaScript
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 };
|