- 后端
- 前端
- 金融科技
- 数据可视化
【免费下载链接】ghostfolio
Open Source Wealth Management Software. Angular + NestJS + Prisma + Nx + TypeScript 🤍
导读
本篇技术指南围绕 Ghostfolio(基于 Angular + NestJS + Prisma + Nx + TypeScript 的开源财富管理软件)API 端的限流实现展开,核心素材来自仓库内.agents/skills/nestjs-best-practices/rules/security-rate-limiting.md安全最佳实践文档。文章将讲解如何用@nestjs/throttler按客户端限制请求速率、为认证等敏感端点设置差异化阈值、在多实例/集群部署下用 Redis 做分布式限流,并结合 Ghostfolio 的真实源码(apps/api/src/app/app.module.ts、apps/api/src/guards/custom-throttler.guard.ts、apps/api/src/app/user/user.controller.ts等)展示这一套规则在生产项目中的落地方式。读完你将掌握限流模块的配置、端点级覆盖、跳过规则、自定义守卫,以及 Ghostfolio 的ENABLE_FEATURE_RATE_LIMITING开关与默认阈值的完整用法。
一、为什么需要速率限制:守护认证端点与公共资源
速率限制(Rate Limiting)是 API 安全的基线能力,它解决两个问题:防滥用(abuse)与保证公平的资源使用(fair resource usage)。具体到财富管理这类含敏感金融数据的系统,其价值尤其突出:
- 防暴力破解:
/auth/login这类端点若不限流,攻击者可以无限次尝试密码,直至命中; - 防邮件轰炸:
/auth/forgot-password会被用来向任意邮箱批量发送重置邮件; - 防资源耗尽:公开数据接口可能被高频爬取,拖垮数据库或上游数据源;
- 保护计费/写路径:支付、下单等操作端点需要比只读端点更严格的配额。
原文档(security-rate-limiting.md)给出的核心主张是:用@nestjs/throttler为每个客户端限制请求速率;不同端点应用不同阈值——认证端点更严格,读操作更宽松;集群部署下考虑用 Redis 做分布式限流。Ghostfolio 正是沿着这条路径落地的。
二、全局配置:ThrottlerModule.forRoot 与多档限流
2.1 基础多档配置(原文档核心示例)
原文档推荐在根模块同时注册多个"档位"(named throttler),每个档位是一组独立的ttl(时间窗口,毫秒)与limit(窗口内最大请求数):
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; @Module({ imports: [ ThrottlerModule.forRoot([ { name: 'short', ttl: 1000, // 1 秒 limit: 3, // 每秒 3 次 }, { name: 'medium', ttl: 10000, // 10 秒 limit: 20, // 每 10 秒 20 次 }, { name: 'long', ttl: 60000, // 1 分钟 limit: 100, // 每分钟 100 次 }, ]), ], providers: [ { provide: APP_GUARD, useClass: ThrottlerGuard, }, ], }) export class AppModule {}要点解读:
- 每个档位独立计数,请求会同时受所有档位约束,任一档位超限即触发
ThrottlerException(默认对应 HTTP 429 Too Many Requests); - 通过
APP_GUARD注册为全局守卫后,所有路由默认套用全部档位,随后可用装饰器按端点精确调参; ttl与limit均为毫秒/次数,便于表达"每秒""每 10 秒""每分钟"等窗口语义。
2.2 Ghostfolio 的全局配置实现
Ghostfolio 没有照搬静态配置,而是用forRootAsync从环境变量动态注入,见 apps/api/src/app/app.module.ts:
ThrottlerModule.forRootAsync({ imports: [ConfigurationModule], inject: [ConfigurationService], useFactory: (configurationService: ConfigurationService) => { const isRateLimitingEnabled = configurationService.get( 'ENABLE_FEATURE_RATE_LIMITING' ); return { errorMessage: getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), skipIf: () => { return !isRateLimitingEnabled; }, storage: isRateLimitingEnabled ? new ThrottlerStorageRedisService({ ...getRedisConnectionOptions(configurationService), // Reject commands immediately while Redis is unavailable enableOfflineQueue: false, maxRetriesPerRequest: 1 }) : undefined, throttlers: [ { limit: THROTTLE_DEFAULT_LIMIT, ttl: THROTTLE_DEFAULT_TTL } ] }; } })这段代码浓缩了生产级限流的关键设计:
skipIf特性开关:ENABLE_FEATURE_RATE_LIMITING默认关闭(见 apps/api/src/services/configuration/configuration.service.ts,bool({ default: false })),未开启时守卫直接放行,方便本地开发与私有部署;- 错误文案对齐 HTTP 语义:
errorMessage使用getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),即标准 "Too Many Requests"; - Redis 分布式存储:启用限流时使用
@nest-lab/throttler-storage-redis的ThrottlerStorageRedisService,并显式设置enableOfflineQueue: false、maxRetriesPerRequest: 1——Redis 不可用时立即拒绝命令而非排队重试,避免限流存储故障拖垮整个 API。
默认档位来自 libs/common/src/lib/config.ts:
export const THROTTLE_DAILY_KEY = 'daily'; export const THROTTLE_DAILY_TTL = ms('1 day'); export const THROTTLE_DEFAULT_LIMIT = 10; export const THROTTLE_DEFAULT_TTL = ms('1 minute'); export const THROTTLE_SIGNUP_LIMIT = 5; export const THROTTLE_SIGNUP_TTL = ms('1 hour');即 Ghostfolio 的全局默认配额为每分钟 10 次请求(THROTTLE_DEFAULT_LIMIT = 10,THROTTLE_DEFAULT_TTL = 1 minute),并预留了"每日"(THROTTLE_DAILY_*)与"注册"(THROTTLE_SIGNUP_*)两档语义常量,可在此统一调整。
三、端点级差异化:认证端点严格、读操作宽松
原文档强调"不同端点不同阈值",给出的端点级覆盖示例为:
// Override limits per endpoint @Controller('auth') export class AuthController { @Post('login') @Throttle({ short: { limit: 5, ttl: 60000 } }) // 每分钟 5 次尝试 async login(@Body() dto: LoginDto): Promise<TokenResponse> { return this.authService.login(dto); } @Post('forgot-password') @Throttle({ short: { limit: 3, ttl: 3600000 } }) // 每小时 3 次 async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> { return this.authService.sendResetEmail(dto.email); } }注:
@Throttle的 key 需要与forRoot中注册的档位name对应;若只注册了匿名默认档位,则使用default作为 key(Ghostfolio 即如此)。
Ghostfolio 中的实际落地
注册端点(严格限流):在 apps/api/src/app/user/user.controller.ts,用户注册(POST /)显式覆盖为更严的阈值:
@Post() @Throttle({ default: { limit: THROTTLE_SIGNUP_LIMIT, // 5 ttl: THROTTLE_SIGNUP_TTL // 1 小时 } }) @UseGuards(CustomThrottlerGuard) public async signupUser(@Body() data: CreateUserDto): Promise<UserItem> { // ... }即每个客户端每小时最多注册 5 个账号,有效遏制批量注册滥用;限流守卫与AuthGuard('jwt')、HasPermissionGuard等业务守卫组合使用,互不冲突。
认证端点(敏感操作):在 apps/api/src/app/auth/auth.controller.ts 中,POST /auth/anonymous(匿名令牌换取 authToken)、POST /auth/webauthn/generate-authentication-options、POST /auth/webauthn/verify-authentication等登录/认证相关端点均挂载了CustomThrottlerGuard(见第 43、124、146 行),确保凭据尝试与 WebAuthn 验证流程天然受限。
计费/写路径:订阅管理端点在 apps/api/src/app/subscription/subscription.controller.ts 同样组合了CustomThrottlerGuard,对涉及支付权益变更的操作做速率约束。
四、跳过限流:健康检查等内部路由
原文档用@SkipThrottle()说明如何对特定路由免除限流:
// Skip throttling for certain routes @Controller('health') export class HealthController { @Get() @SkipThrottle() check(): string { return 'OK'; } }适用场景包括:负载均衡器探活、监控探针(如 Kubernetes liveness/readiness)、CDN 回源等高频但无风险的内部调用。Ghostfolio 的HealthModule即属于此类基础设施路由;同时app.module.ts中ServeStaticModule.forRoot通过exclude将/api/*wildcard、/sitemap.xml等从静态资源匹配中排除,避免与限流中间件产生路径歧义。需要提醒的是:跳过限流应仅限于无副作用、无敏感信息的端点,绝不能应用到登录、注册或支付路径。
五、自定义守卫:按用户类型与身份差异化限流
5.1 原文档的自定义守卫模板
原文档给出基于ThrottlerGuard子类化实现的按用户类型限流方案:
// Custom throttle per user type @Injectable() export class CustomThrottlerGuard extends ThrottlerGuard { protected async getTracker(req: Request): Promise<string> { // Use user ID if authenticated, IP otherwise return req.user?.id || req.ip; } protected async getLimit(context: ExecutionContext): Promise<number> { const request = context.switchToHttp().getRequest(); // Higher limits for authenticated users if (request.user) { return request.user.isPremium ? 1000 : 200; } return 50; // Anonymous users } }- 重写
getTracker可改变"限流对象":认证用户按用户 ID 计数,匿名用户按 IP 计数,避免共享 IP 下多用户互相误伤; - 重写
getLimit可按请求上下文动态返回阈值(如付费用户 1000、普通用户 200、匿名 50),实现按用户等级配额。
5.2 Ghostfolio 的 CustomThrottlerGuard
Ghostfolio 的实现在 apps/api/src/guards/custom-throttler.guard.ts:
import { ExecutionContext, Injectable, Logger } from '@nestjs/common'; import { ThrottlerException, ThrottlerGuard } from '@nestjs/throttler'; @Injectable() export class CustomThrottlerGuard extends ThrottlerGuard { private readonly logger = new Logger(CustomThrottlerGuard.name); public override async canActivate( context: ExecutionContext ): Promise<boolean> { try { return await super.canActivate(context); } catch (error) { if (error instanceof ThrottlerException) { throw error; } this.logger.error(error); return true; } } }它的设计思想是失败降级(fail-open with isolation):
- 只有
ThrottlerException(真正的限流命中)会被原样抛出,客户端收到 429; - 其他任何异常(如 Redis 存储临时故障)只会记录
logger.error并返回true放行,不让限流组件自身的故障阻断正常业务请求——这与app.module.ts中enableOfflineQueue: false、maxRetriesPerRequest: 1的"快速失败"策略配合,形成"限流可用则严格限流、限流不可用则优雅降级"的稳健语义。
六、集群部署:Redis 分布式限流
6.1 为什么要用 Redis
默认的ThrottlerStorage是进程内内存存储,只对单实例有效。当应用水平扩展为多实例(或如 Ghostfolio 般拆分为多进程)时,每个实例各自计数,攻击者可将请求分散到不同实例绕过总配额。此时需要共享存储,即 Redis。
原文档明确建议:"Consider using Redis for distributed rate limiting in clustered deployments."(集群部署中考虑使用 Redis 做分布式限流)。
6.2 Ghostfolio 的 Redis 集成
Ghostfolio 在app.module.ts中通过@nest-lab/throttler-storage-redis接入 Redis,连接参数复用getRedisConnectionOptions(configurationService)(与 BullMQ 队列、Redis 缓存共用同一套连接配置,见 apps/api/src/app/app.module.ts 的BullModule.forRootAsync)。关键细节:
storage: isRateLimitingEnabled ? new ThrottlerStorageRedisService({ ...getRedisConnectionOptions(configurationService), enableOfflineQueue: false, // Redis 不可用时立即拒绝命令 maxRetriesPerRequest: 1 // 单次重试上限 }) : undefined适用前提:分布式限流依赖 Redis 实例的可用性,部署时必须保证 Redis 高可用,否则需权衡"限流降级放行"(CustomThrottlerGuard 的 fail-open 行为)带来的安全缺口。
6.3 反向代理与 TRUST_PROXY 的联动
Ghostfolio 在 apps/api/src/main.ts 处理了反向代理场景的关键坑:
const trustProxy = configurationService.get('TRUST_PROXY'); if (trustProxy) { app.set('trust proxy', trustProxy); } if ( configurationService.get('ENABLE_FEATURE_RATE_LIMITING') && trustProxy === '' ) { logger.warn( 'Rate limiting is enabled, but TRUST_PROXY is not set. If the Ghostfolio application runs behind a reverse proxy, the rate limits are shared across all clients.' ); }原因在于:NestJS 的ThrottlerGuard默认用req.ip作为限流 tracker,若应用位于 Nginx/Caddy 等反向代理之后却未配置TRUST_PROXY,所有客户端 IP 都会解析为代理 IP,导致所有用户共享同一份配额(表现为"一人超限、全员 429")。因此:
- 部署在反向代理后必须设置
TRUST_PROXY(如1表示信任一跳代理),让 Express 正确解析X-Forwarded-For; - 未设置时会输出上述警告日志,提醒运维检查。
七、配置开关与部署清单
7.1 环境变量总览
| 配置项 | 默认值 | 作用 |
|---|---|---|
ENABLE_FEATURE_RATE_LIMITING | false | 全局开关,开启后启用 Redis 存储限流 |
TRUST_PROXY | 空 | 反向代理信任设置,影响req.ip解析 |
THROTTLE_DEFAULT_LIMIT | 10 | 默认档位窗口内最大请求数(libs/common/src/lib/config.ts) |
THROTTLE_DEFAULT_TTL | 1 minute | 默认档位时间窗口(同文件 L413) |
THROTTLE_SIGNUP_LIMIT | 5 | 注册端点每窗口上限(L414) |
THROTTLE_SIGNUP_TTL | 1 hour | 注册端点时间窗口(L415) |
THROTTLE_DAILY_LIMIT/TTL | 1 day | 预留的每日档位常量(L410-L411) |
7.2 上线前检查清单
- 确认
ENABLE_FEATURE_RATE_LIMITING=true且 Redis 可达(@nest-lab/throttler-storage-redis依赖REDIS_*连接配置); - 若位于反向代理后,设置
TRUST_PROXY,并观察 main.ts 的告警日志是否消失; - 用登录、注册、webauthn 验证等敏感端点做压测,确认 429 在期望阈值处触发且
errorMessage文案正确; - 核对
CustomThrottlerGuard的 fail-open 语义是否符合你的安全基线(Redis 故障时放行 vs 拒绝); - 业务上豁免限流的内部路由(健康检查等)确认无敏感数据暴露。
八、小结
结合security-rate-limiting.md最佳实践与 Ghostfolio 源码,可以提炼出这套可复用的限流方法论:
- 全局注册多档限流:
ThrottlerModule.forRoot([...])或forRootAsync动态注入,通过APP_GUARD全局生效; - 端点级覆盖:登录(每分钟 5 次)、注册(每小时 5 次)、忘记密码(每小时 3 次)等敏感操作用
@Throttle收紧,读操作保持宽松; - 明确豁免边界:
@SkipThrottle()仅用于健康检查等内部路由; - 自定义守卫实现差异化:重写
getTracker(按用户 ID 而非 IP 计数)与getLimit(按用户等级给配额),并用 fail-open 兜底组件故障; - Redis 支撑集群:分布式部署用
ThrottlerStorageRedisService共享计数,同时配合TRUST_PROXY避免反向代理导致 IP 归一化。
Ghostfolio 的落地方案(app.module.ts+custom-throttler.guard.ts+config.ts)证明:这套模式可以做到"默认关闭、按需开启、端到端可调、组件故障不拖垮业务",值得在需要保护认证与计费路径的 NestJS 项目中直接借鉴。
- 后端
- 前端
- 金融科技
- 数据可视化
【免费下载链接】ghostfolio
Open Source Wealth Management Software. Angular + NestJS + Prisma + Nx + TypeScript 🤍
相关推荐
financial高级用法:IRR与NPV函数助力投资决策分析
financial高级用法:IRR与NPV函数助力投资决策分析 在投资决策中,准确评估项目的盈利能力是至关重要的。 financial 作为一款零依赖的Type
rsschool-app 的 NestJS 限流实践:基于 @nestjs/throttler 的接口级速率限制指南
rsschool app 的 NestJS 限流实践:基于 @nestjs/throttler 的接口级速率限制指南 本指南以 .agents/skills/n
教育后端前端ioredis限流实现:基于Redis的速率限制算法
ioredis限流实现:基于Redis的速率限制算法 概述 在现代分布式系统中,速率限制(Rate Limiting)是保护服务免受异常访问和资源滥用的关键技术
后端缓存
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考