完善了安全功能
This commit is contained in:
83
middleware/auth.js
Normal file
83
middleware/auth.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { DBModel } from '../models/index.js';
|
||||
import ResponseUtil from '../utils/api_response.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 并挂载用户信息到 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 DBModel.User.findOne({ 'security.token': 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();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回用户安全对象(去除密码等敏感字段)
|
||||
*/
|
||||
function sanitizeUser(user) {
|
||||
const obj = user.toObject ? user.toObject() : { ...user };
|
||||
delete obj.security?.passwd;
|
||||
delete obj.security?.passwdSalt;
|
||||
delete obj.security?.passwordResetToken;
|
||||
delete obj.security?.passwordResetExpiry;
|
||||
return obj;
|
||||
}
|
||||
|
||||
export { auth, extractToken, sanitizeUser };
|
||||
51
middleware/ratelimit.js
Normal file
51
middleware/ratelimit.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import ResponseUtil from '../utils/api_response.js';
|
||||
|
||||
/**
|
||||
* 内存速率限制
|
||||
* 基于 IP + 接口路径维度,不引入 Redis
|
||||
*/
|
||||
const rateLimitStore = new Map();
|
||||
|
||||
// 每 60 秒清理一次过期记录
|
||||
const cleanupTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, record] of rateLimitStore.entries()) {
|
||||
if (now - record.resetAt > record.windowMs) {
|
||||
rateLimitStore.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60_000);
|
||||
cleanupTimer.unref();
|
||||
|
||||
/**
|
||||
* 创建速率限制中间件
|
||||
* @param {number} max - 窗口内最大请求数
|
||||
* @param {number} windowMs - 窗口时长(毫秒),默认 60 秒
|
||||
*/
|
||||
function rateLimit(max = 10, windowMs = 60_000) {
|
||||
return async (ctx, next) => {
|
||||
const ip = ctx.ip || ctx.request.ip;
|
||||
const key = `${ip}:${ctx.path}`;
|
||||
const now = Date.now();
|
||||
|
||||
let record = rateLimitStore.get(key);
|
||||
if (!record || now - record.resetAt > windowMs) {
|
||||
record = { count: 0, resetAt: now, windowMs };
|
||||
rateLimitStore.set(key, record);
|
||||
}
|
||||
|
||||
record.count++;
|
||||
|
||||
if (record.count > max) {
|
||||
const retryAfter = Math.ceil((record.resetAt + windowMs - now) / 1000);
|
||||
ctx.set('Retry-After', retryAfter);
|
||||
return ResponseUtil.error(ctx, `请求过于频繁,请 ${retryAfter} 秒后重试`, null, 429);
|
||||
}
|
||||
|
||||
ctx.set('X-RateLimit-Limit', max);
|
||||
ctx.set('X-RateLimit-Remaining', Math.max(0, max - record.count));
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
export { rateLimit };
|
||||
Reference in New Issue
Block a user