【RuoYi-Eggjs】:限流,为 API 保驾护航

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.7k人参与

【RuoYi-Eggjs】:限流,为 API 保驾护航

前言

在 Web 应用开发中,保护 API 免受恶意攻击和过载是一项重要的安全措施。无论是防止暴力破解、恶意刷接口,还是保护服务器免于过载,限流(Rate Limiting)都是必不可少的防护手段。[ruoyi-eggjs-ratelimiter](https://github.com/undsky/ruoyi-eggjs-ratelimiter) 就是一个为 Egg.js 量身定制的限流插件,基于强大的 rate-limiter-flexible 库,提供开箱即用的请求限流能力。

为什么需要限流?

🛡️ 防止暴力攻击

登录、注册、找回密码等接口最容易成为暴力破解的目标。通过限流,可以有效限制单个 IP 在短时间内的尝试次数:

攻击者 1 分钟内尝试 1000 次登录 ❌ 被限流
正常用户 1 分钟内尝试 3 次登录 ✅ 正常访问

🚦 保护服务器资源

防止单个用户或恶意脚本占用过多服务器资源,确保所有用户都能获得良好的服务质量。

核心特性

🎯 基于 IP 的智能限流

插件自动获取客户端真实 IP 地址,并基于 IP 进行限流统计。支持反向代理场景,可正确识别经过 Nginx 等代理的真实 IP。

💾 双存储模式

1. 内存存储(Memory)

  • 优势:速度快,无需额外服务
  • 劣势:应用重启后限流记录丢失
  • 适用:开发环境、单机部署

2. Redis 存储(推荐)

  • 优势:数据持久化,多实例共享限流状态
  • 劣势:需要 Redis 服务
  • 适用:生产环境、集群部署

📈 标准响应头

插件严格遵循 HTTP 限流标准,返回标准的响应头:

Retry-After: 5.5                    # 多少秒后可以重试
X-RateLimit-Limit: 100              # 时间窗口内的请求限制
X-RateLimit-Remaining: 0            # 剩余可用请求次数
X-RateLimit-Reset: Mon Jan 01...    # 限流重置时间

前端可以通过这些响应头实现友好的用户提示和自动重试机制。

🔍 自动日志记录

当请求被限流时,插件会自动记录详细日志,包括 IP 地址、用户信息、请求内容等,便于追踪和分析恶意行为。

快速上手

安装

npm i ruoyi-eggjs-ratelimiter --save

配置

1. 启用插件

// config/plugin.js
exports.ratelimiter = {
  enable: true,
  package: "ruoyi-eggjs-ratelimiter",
};

2. 基础配置(内存存储)

适合开发环境或单机部署:

// config/config.default.js
config.ratelimiter = {
  points: 1000,      // 每个时间窗口允许 1000 次请求
  duration: 1000,    // 时间窗口为 1000 毫秒(1 秒)
  redis: null,       // 不使用 Redis
};

3. Redis 配置(生产环境推荐)

适合生产环境和集群部署:

// config/config.prod.js
config.ratelimiter = {
  points: 100,       // 每分钟允许 100 次请求
  duration: 60,      // 时间窗口为 60 秒
  redis: {
    port: 6379,
    host: '127.0.0.1',
    password: 'your-password',
    db: 0,
  },
};

配置参数说明

参数类型默认值说明
pointsNumber1000时间窗口内允许的最大请求次数
durationNumber1000时间窗口大小(内存:毫秒;Redis:秒)
redisObjectnullRedis 配置对象,为 null 时使用内存存储

注意duration 的单位根据存储方式不同而不同!使用 Redis 时单位为,使用内存时单位为毫秒

实战场景

场景 1:防止登录暴力破解

登录接口是最容易被攻击的目标,限制每分钟的尝试次数可以有效防止暴力破解:

// config/config.prod.js
config.ratelimiter = {
  points: 5,         // 每分钟最多 5 次尝试
  duration: 60,      // 60 秒
  redis: {
    host: '127.0.0.1',
    port: 6379,
  },
};

效果

  • 正常用户输错密码几次后需要等待 1 分钟
  • 暴力破解程序在 5 次尝试后被阻止
  • 大大降低了账号被破解的风险

场景 2:通用 API 限流

对于开放的 API 接口,限制每秒的请求次数可以防止单个客户端占用过多资源:

// config/config.prod.js
config.ratelimiter = {
  points: 10,        // 每秒最多 10 次请求
  duration: 1,       // 1 秒
  redis: {
    host: '127.0.0.1',
    port: 6379,
  },
};

效果

  • 正常用户每秒 10 次请求足够使用
  • 恶意脚本无法快速刷接口
  • 服务器负载保持在可控范围

场景 3:开发环境宽松限制

开发环境下希望限流不影响调试,可以设置较大的限流值:

// config/config.local.js
config.ratelimiter = {
  points: 1000,      // 每秒 1000 次请求
  duration: 1000,    // 1000 毫秒(1 秒)
  redis: null,       // 使用内存存储,简单快速
};

场景 4:配合 Nginx 获取真实 IP

在使用 Nginx 等反向代理的情况下,需要正确配置才能获取真实 IP:

Nginx 配置

location / {
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_pass http://127.0.0.1:7001;
}

Egg.js 配置

// config/config.default.js
config.proxy = true;  // 启用代理模式,信任 X-Forwarded-For

这样插件就能通过 ctx.request.ip 获取到真实的客户端 IP,而不是 Nginx 的 IP。

测试限流

使用 curl 测试

快速发送多个请求观察限流效果:

# 快速发送 10 个请求
for i in {1..10}; do
  curl -i http://localhost:7001/api/test
done

观察响应变化

前几次请求(正常):

HTTP/1.1 200 OK
Content-Type: application/json

{"code":200,"data":{...}}

超限后的请求

HTTP/1.1 200 OK
Retry-After: 0.5
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: Wed%2C%2027%20Nov%202024%2013%3A30%3A00%20GMT

{"code":429,"message":"Too Many Requests"}

前端处理示例

// 前端请求拦截器
axios.interceptors.response.use(
  response => {
    if (response.data.code === 429) {
      const retryAfter = response.headers['retry-after'];
      message.error(`请求过于频繁,请在 ${retryAfter} 秒后重试`);
    }
    return response;
  }
);

常见问题

Q1: 为什么选择 Redis 还是内存?

存储方式速度持久化分布式适用场景
内存⚡️ 极快❌ 重启丢失❌ 单机开发环境、单机部署
Redis🚀 快✅ 持久化✅ 共享生产环境、集群部署

推荐做法

  • 开发环境使用内存存储,简单快速
  • 生产环境使用 Redis,数据可靠、支持集群

Q2: duration 单位为什么不一致?

这是因为 rate-limiter-flexible 库的不同实现对时间单位的要求不同:

  • RateLimiterRedis:要求 duration
  • RateLimiterMemory:支持 duration毫秒

建议在配置时明确注释单位,避免混淆:

config.ratelimiter = {
  points: 100,
  duration: 60,  // 秒(使用 Redis)
  redis: { ... },
};

Q3: 如何针对不同接口设置不同限流策略?

当前插件作为全局中间件运行,对所有接口使用统一的限流策略。如果需要细粒度控制,可以考虑:

方案 1:在特定 Controller 中自定义限流逻辑

// app/controller/login.js
async login() {
  // 针对登录接口的特殊限流逻辑
  const limiter = new RateLimiter({ points: 5, duration: 60 });
  await limiter.consume(ctx.request.ip);
  // 业务逻辑
}

方案 2:扩展插件,支持路由级别的配置(需要修改插件代码)

Q4: 如何处理代理场景下的 IP 获取?

确保 Nginx 配置正确转发真实 IP:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

并在 Egg.js 中启用代理模式:

config.proxy = true;

性能优化建议

1. 合理设置限流参数

根据业务场景选择合适的 pointsduration

// 登录接口:严格限制
{ points: 5, duration: 60 }

// 查询接口:适度限制
{ points: 100, duration: 60 }

// 静态资源:宽松限制
{ points: 1000, duration: 60 }

2. Redis 连接优化

使用连接池,避免频繁创建连接:

config.ratelimiter = {
  redis: {
    host: '127.0.0.1',
    port: 6379,
    // 连接池配置
    maxRetriesPerRequest: 1,
    enableOfflineQueue: false,
  },
};

3. 监控限流情况

定期查看日志,分析被限流的请求:

# 查看限流日志
grep "RateLimiter" logs/common-error.log

根据日志调整限流策略,平衡安全性和用户体验。

总结

ruoyi-eggjs-ratelimiter 是一个简单但强大的 Egg.js 限流插件,它的优势在于:

  • 开箱即用:一行配置,全局生效
  • 灵活存储:支持 Redis 和内存两种方式
  • 标准化:遵循 HTTP 限流标准,响应头完整
  • 易于调试:自动记录日志,便于追踪
  • 性能优异:基于成熟的 rate-limiter-flexible 库

无论是保护登录接口免受暴力攻击,还是限制 API 调用频率,这个插件都能轻松胜任。如果你正在使用 Egg.js,强烈建议集成这个插件,为你的 API 加上一道安全防线!


<think>我们面对的是一个Maven循环依赖问题。根据错误信息,ruoyi-system模块依赖于ruoyi-common模块,而ruoyi-common模块又反过来依赖于ruoyi-system模块,形成了循环:ruoyi-system->ruoyi-common->ruoyi-system。解决循环依赖的常见方法:1.重构代码:将两个模块中相互依赖的部分提取到一个新的模块中,或者将公共代码下沉到基础模块。2.检查依赖关系:分析为什么ruoyi-common会依赖ruoyi-system?通常,common模块应该是被其他模块依赖的基础模块,不应该反过来依赖上层模块。具体步骤:1.查看ruoyi-common模块的pom.xml,确认它是否直接或间接地依赖了ruoyi-system。2.查看ruoyi-system模块的pom.xml,确认它对ruoyi-common的依赖。根据错误信息,循环路径是:ruoyi-system->ruoyi-common->ruoyi-system。因此,我们需要在ruoyi-common模块中移除对ruoyi-system的依赖。可能的情况:-ruoyi-common模块的pom.xml中,显式地引入了ruoyi-system依赖。这种情况应该删除该依赖。-或者,ruoyi-common模块中的某些类引用了ruoyi-system模块中的类,导致编译时需要ruoyi-system。这种情况需要重构代码,将ruoyi-common中依赖ruoyi-system的类移到ruoyi-system模块中,或者将两个模块中相互依赖的代码提取到第三个模块。操作建议:1.检查代码:在ruoyi-common模块中搜索对ruoyi-system模块中类的引用。如果存在,考虑将这些引用移除或重构。2.如果ruoyi-common确实需要一些原本在ruoyi-system中的功能,可以将这些功能提取到一个新的模块,然后让两个模块都依赖这个新模块。3.如果循环依赖是由于某些工具类或通用类被放置在ruoyi-system模块中,而这些类又被ruoyi-common需要,那么应该将这些类移动到ruoyi-common模块。注意:在Maven项目中,循环依赖是不允许的,因此必须打破这个循环。根据RuoYi项目的常见结构,ruoyi-common模块通常是基础工具类、通用枚举等,而ruoyi-system是系统模块,包含系统相关的功能。所以,ruoyi-common不应该依赖ruoyi-system。实际解决步骤:1.打开ruoyi-common模块的pom.xml,查看<dependencies>部分,如果发现ruoyi-system依赖,则将其删除。2.如果pom中没有直接依赖,则可能是传递依赖导致?但根据错误信息,是直接形成了循环,所以重点检查直接依赖。3.使用Maven命令分析依赖树:在项目根目录下运行`mvndependency:tree`,查看依赖树,定位循环依赖的位置。如果代码中存在类之间的循环依赖,那么需要修改代码。例如:-如果ruoyi-common中的类A使用了ruoyi-system中的类B,那么可以考虑:a.将类B移动到ruoyi-common中(如果类B是通用的,则合理)b.或者,将类A中依赖类B的部分抽离出来,通过接口解耦(依赖倒置),将接口放在ruoyi-common,而实现放在ruoyi-system,这样ruoyi-common只依赖接口,而实现由ruoyi-system提供,但ruoyi-common不再直接依赖ruoyi-system。另一种方法是使用事件或消息机制来解耦两个模块之间的直接依赖。但是,由于这是一个常见问题,我们可以参考RuoYi项目的标准结构。实际上,在标准的RuoYi项目中,ruoyi-common模块是不应该依赖ruoyi-system模块的。因此,可能是开发过程中不小心引入了反向依赖。总结:先检查pom文件,再检查代码引用。优先确保ruoyi-common的pom中没有引入ruoyi-system依赖。然后检查代码,将ruoyi-common中引用ruoyi-system的类进行调整,要么移动类的位置,要么重构代码消除依赖。如果问题仍然存在,请提供更多信息,例如两个模块中相互引用的具体类名和代码片段,以便更精确地解决问题。</think>### 解决Maven循环依赖问题 您在构建Maven项目时遇到的错误信息表明存在**模块间循环依赖**。具体错误为: ``` Edge between 'Vertex{label='com.ruoyi:ruoyi-common:3.8.8'}' and 'Vertex{label='com.ruoyi:ruoyi-system:3.8.8'}' introduces a cycle in the graph com.ruoyi:ruoyi-system:3.8.8 --> com.ruoyi:ruoyi-common:3.8.8 --> com.ruoyi:ruoyi-system:3.8.8 ``` #### 问题本质 循环依赖发生在两个模块相互引用时: 1. `ruoyi-system` 模块依赖 `ruoyi-common` 2. `ruoyi-common` 模块又反向依赖 `ruoyi-system` 形成闭环:$$ \text{system} \rightarrow \text{common} \rightarrow \text{system} $$ #### 解决方案(逐步操作) ##### 1. **定位循环依赖点** - 检查 `ruoyi-common/pom.xml` 中是否显式依赖了 `ruoyi-system`: ```xml <!-- 错误示例 --> <dependency> <groupId>com.ruoyi</groupId> <artifactId>ruoyi-system</artifactId> <version>3.8.8</version> </dependency> ``` - 若存在,**直接删除此依赖**(这是最快解决方案) ##### 2. **代码级依赖检查** 如果POM中无显式依赖,检查代码: - 在 `ruoyi-common` 模块中搜索 `ruoyi-system` 中的类引用 - 常见场景: - `ruoyi-common` 导入了 `ruoyi-system` 的Service/Entity类 - `ruoyi-common` 的工具类引用了系统模块的功能 ##### 3. **架构重构(核心方案)** ```mermaid graph LR A[原结构] --> B[ruoyi-system] --> C[ruoyi-common] C --> B %% 循环点 D[新结构] --> E[ruoyi-system] D --> F[ruoyi-common] D --> G[新建 ruoyi-common-core] F --> G %% 基础层 E --> G %% 共同依赖基础层 ``` - **步骤**: 1. 创建新模块 `ruoyi-common-core` 2. 将 `ruoyi-common` 中被 `ruoyi-system` 依赖的**基础代码**移至 `core` 模块 3. 调整依赖关系: - `ruoyi-common` 依赖 `ruoyi-common-core` - `ruoyi-system` 依赖 `ruoyi-common-core` - 确保 `ruoyi-common-core` **不依赖**其他业务模块 ##### 4. **依赖作用域优化** 若循环依赖无法完全消除,使用 `<scope>` 限制: ```xml <!--ruoyi-common 中 --> <dependency> <groupId>com.ruoyi</groupId> <artifactId>ruoyi-system</artifactId> <version>3.8.8</version> <scope>provided</scope> <!-- 编译时不传递 --> </dependency> ``` #### 验证修复 执行以下命令验证: ```bash mvn clean install -DskipTests ``` 循环依赖错误应消失,构建成功[^1][^2]。 #### 预防措施 1. 遵循 **单向依赖原则**:基础模块(如common)永远不依赖上层模块 2. 定期运行依赖分析: ```bash mvn dependency:tree -Dincludes=com.ruoyi ``` 3. 使用架构守护工具(如ArchUnit)禁止跨模块反向引用 > **关键提示**:在RuoYi等标准框架中,`common` 模块通常是所有模块的基础层,若出现反向依赖,往往是代码结构问题,需优先检查模块职责划分[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值