Rack::Attack 进阶配置实战:8 个高级防护场景与源码级解析
应用安全后端【免费下载链接】rack-attackRack middleware for blocking throttling项目地址https://gitcode.com/gh_mirrors/ra/rack-attack点击查看免费下载Rack::Attack 提供了开箱即用的限流throttle、封禁blocklist与白名单safelist能力但当业务场景超出常规配置例如需要指数退避限流、动态维护黑名单、按控制器动作匹配请求时就需要用到本文所讲的进阶配置技巧。本文以仓库中的 docs/advanced_configuration.md 为骨架逐例讲解 8 种高级防护方案并结合 lib/rack/attack 下的真实源码与 spec 测试解释每个方案背后的实现原理与适用边界。读完本文你将能够用分层 throttle 模拟指数退避、自定义请求辅助方法、从 ENV 与 Rails.cache 动态维护黑名单、精确重置某个限流计数、用 Allow2Ban 拦截 Basic Auth 爆破以及在 Rails 中按controller#action优雅地匹配路由。写在前面进阶配置的定位与前提本文对应的官方文档是 docs/advanced_configuration.md。官方对这份文档的定位非常明确——进阶Advanced意味着它们不是常规用法文档开头甚至自嘲式地警告Much of this code is untested. Copy-paste at your own risk!大部分代码未经测试复制粘贴风险自负。因此动手使用前请务必先跑通自己的测试用例并在生产环境谨慎评估。如果你还没有基础配置建议先阅读 docs/example_configuration.md它提供了可直接复制到config/initializers/rack_attack.rb的基础防护配置再回头阅读本文。理解进阶配置前需要先明确 Rack::Attack 中间件的判定顺序。在 lib/rack/attack.rb 的call方法中每个请求按如下优先级依次判定命中任意safelist→ 直接放行跳过后续所有检查否则命中任意blocklist→ 返回blocklisted_responder默认 403否则命中任意throttle→ 返回throttled_responder默认 429否则执行所有track检查只记录、不影响请求放行。下面的每个进阶方案本质上都是围绕这四类规则做组合、扩展或数据源替换理解了判定顺序就能理解为什么某个进阶配置要写成 blocklist 而不是 throttle。一、指数退避Exponential Backoff限流思路与代码普通限流是固定周期内固定次数而指数退避要求失败越频繁、等待时间越长。Rack::Attack 本身没有内置退避算法但可以通过叠加多层 throttlelimit 线性增长、period 指数增长来近似模拟。原文档给出的完整代码如下# Allows 20 requests in 8 seconds # 40 requests in 64 seconds # ... # 100 requests in 0.38 days (~250 requests/day) (1..5).each do |level| throttle(logins/ip/#{level}, :limit (20 * level), :period (8 ** level).seconds) do |req| if req.path /login req.post? req.ip end end end参数演算这段代码共注册了 5 层 throttle各层的参数如下levellimit每次周期内允许次数period周期时长换算1208 秒约 9000 次/天24064 秒约 54000 次/天360512 秒≈8.5 分钟—4804096 秒≈68 分钟—510032768 秒≈9.1 小时 ≈ 0.38 天约 250 次/天同一 IP 对/login的 POST 请求会同时计入这 5 层计数器第 21 次请求20 秒内就触发第 1 层第 41 次请求64 秒内触发第 2 层……请求越密集被拦截的门槛越低从而近似实现退避效果。计数语义的源码依据要理解为什么 20 次之内放行、第 21 次拦截需要看 lib/rack/attack/throttle.rb 中matched_by?的判定(count current_limit)即计数器严格大于limit 时才判定为超限。对应测试 spec/rack_attack_throttle_spec.rb 也验证了limit: 1时第 2 个请求即返回 429。另外注意throttle 块的返回值是计数器的discriminator区分键req.ip表示按 IP 分别计数如果返回 falsy如这里非/login的 POST则跳过该层计数lib/rack/attack/throttle.rb。进阶补充动态 limit/period如果你希望退避参数随请求动态变化Rack::Attack 的 throttle 还支持limit 和 period 传 proc详见 README.md 的 Throttling 一节。例如认证通过的用户放宽到 100 次/秒、普通用户收紧为 1 次/60 秒limit_proc proc { |req| req.env[REMOTE_USER] admin ? 100 : 1 } period_proc proc { |req| req.env[REMOTE_USER] admin ? 1 : 60 } Rack::Attack.throttle(request per ip, limit: limit_proc, period: period_proc) do |request| request.ip end源码层面lib/rack/attack/throttle.rb 的period_for/limit_for会优先调用 proc否则使用常量值。这与分层固定值的退避方案互为补充前者适合基于用户状态的差异化限流后者适合基于暴力破解强度的全局退避。二、扩展 Rack::Attack::Request自定义请求辅助方法问题场景在编写 safelist / blocklist / throttle 规则时判断逻辑往往需要在多个规则里复用例如是否来自 localhost是否属于某个子域。与其在每个规则里重复写req.ip 127.0.0.1不如把这些判断封装成请求对象的辅助方法。官方推荐做法Monkey-patch原文档给出的方案是对Rack::Attack::Request做 monkey-patchclass Rack::Attack::Request ::Rack::Request def localhost? ip 127.0.0.1 end end Rack::Attack.safelist(localhost) { |req| req.localhost? }这并非社区野路子而是官方推荐的扩展点。看 lib/rack/attack/request.rb 的源码注释文件开头就明确写道This is a safe place to add custom helper methods to the request object through monkey patching并在注释中给出了与文档完全一致的localhost?示例。原理与调用链Rack::Attack 中间件在 lib/rack/attack.rb 中构造request Rack::Attack::Request.new(env)所有规则块收到的req都是这个对象由于Rack::Attack::Request ::Rack::Requestlib/rack/attack/request.rb它天然具备ip、path、post?、params、env等 Rack::Request 的全部能力。你 patch 上去的辅助方法在任意 safelist / blocklist / throttle / track 块中都能直接调用。注意事项加载时机patch 必须发生在 Rack::Attack 构造中间件实例之前也就是放在config/initializers/rack_attack.rb中、所有规则定义之前即可若放在 Rails 应用的其他 initializer 里需确认其加载顺序早于请求处理。命名空间隔离只 patchRack::Attack::Request不会影响应用中其他使用Rack::Request的地方副作用可控。三、从环境变量ENV构建黑名单问题场景黑名单列表如垃圾 Referer 域名如果硬编码在代码里每次增删都要发版。将其迁移到环境变量可以让运维直接修改部署配置。完整示例原文档给出了一个把 ENV 中逗号分隔的域名列表转成黑名单正则的示例class Rack::Attack # Split on a comma with 0 or more spaces after it. # E.g. ENV[HEROKU_VARIABLE] foo.com, bar.com # spammers [foo.com, bar.com] spammers ENV[HEROKU_VARIABLE].split(/,\s*/) # Turn spammers array into a regexp spammer_regexp Regexp.union(spammers) # /foo\.com|bar\.com/ blocklist(block referer spam) do |request| request.referer ~ spammer_regexp end end关键细节剖析split(/,\s*/)按逗号分割并允许逗号后跟 0 个或多个空白兼容foo.com, bar.com这种带空格写法。Regexp.union把字符串数组编译为单个正则。需要注意Regexp.union会自动转义字符串中的正则元字符所以foo.com变成/foo\.com/不会把.误当作通配符文档注释中已标明转换结果。匹配语义request.referer ~ spammer_regexp只做子串匹配即 Referer 中任意位置包含任一黑名单域名都会命中。若要精确匹配域名避免notfoo.com误伤可在正则两侧补边界Regexp.new((?:#{spammers.join(|)}))或使用Regexp.union(spammers.map { |d| /(?:^|\\.)#{Regexp.escape(d)}/ })这类变体——这部分属于可选的加固思路需自行测试验证。这个方案在 lib/rack/attack/configuration.rb 的blocklist注册机制上运行规则块返回 truthy 即命中黑名单并返回 403。由于spammers与spammer_regexp在class Rack::Attack上下文中求值一次适合部署时确定、运行期不常变的列表如需运行期动态调整请参考本文第五节从Rails.cache读取黑名单。四、精确重置某个限流计数Reset Specific Throttles问题场景用户触发了某个 throttle例如登录限流后可能因为验证了手机号或管理员人工解封而需要立即恢复访问。文档提示这类需求可以通过 monkey-patching 添加Rack::Attack.reset_throttle logins/email, userexample.com这样的辅助方法。需要说明的是当前仓库源码中并没有内置reset_throttle全仓库检索仅见于该文档它需要你自己基于 Cache 的原语能力来实现。计数器的 key 结构实现的基础要实现精确重置必须先理解 Rack::Attack 的计数器 key 结构。在 lib/rack/attack/cache.rb 的key_and_expiry中[#{prefix}:#{(last_epoch_time / period).to_i}:#{unprefixed_key}, expires_in]其中prefix默认是rack::attacklib/rack/attack/cache.rbunprefixed_key即#{throttle_name}:#{discriminator}lib/rack/attack/throttle.rb。因此logins/email对userexample.com的计数器完整 key 形如rack::attack:#{当前时间/周期取整}:logins/email:userexample.com推荐实现直接用官方原语与其自己拼 key更稳妥的做法是利用 Cache 提供的reset_count(unprefixed_key, period)lib/rack/attack/cache.rb它内部会按同样规则算出时间桶并delete对应 keymodule Rack::Attack # 需要知道目标 throttle 的 period此处仅支持固定 period 的 throttle def self.reset_throttle(name, discriminator) throttle throttles[name] period throttle.period.to_i cache.reset_count(#{name}:#{discriminator}, period) end end Rack::Attack.reset_throttle logins/email, userexample.com其中Rack::Attack.throttles通过 Forwardable 委托到配置lib/rack/attack.rb返回以名称为 key 的 throttle 哈希throttle.period是公开的 readerlib/rack/attack/throttle.rb。若 period 是 proc见第一节则需要额外计算当前请求对应的周期值。官方内置的同类能力Fail2Ban.reset如果你的解封需求针对 Fail2Ban/Allow2Ban 的封禁则无需自己实现——lib/rack/attack/fail2ban.rb 内置了Fail2Ban.reset(discriminator, options)它会同时删除计数 key 与 ban 标志 key。对应测试 spec/fail2ban_spec.rb 验证了 reset 后计数归零、封禁解除、请求恢复 200。此外若要在测试套件中全局清空Rack::Attack 状态官方提供Rack::Attack.reset!见 lib/rack/attack.rb 与 spec/rack_attack_reset_spec.rb它通过 cache store 的delete_matched删除所有rack::attack前缀 key——注意这要求 store 支持delete_matched否则抛出Rack::Attack::IncompatibleStoreError。五、从 Rails.cache 动态维护黑名单问题场景前三节的黑名单都是启动时确定的静态数据。如果希望应用运行期间例如用户举报后、后台任务扫描后能随时封禁某个 IP可以把黑名单的判定数据源换成Rails.cache。完整示例原文档给出的实现# Block attacks from IPs in cache # To add an IP: Rails.cache.write(block 1.2.3.4, true, expires_in: 2.days) # To remove an IP: Rails.cache.delete(block 1.2.3.4) Rack::Attack.blocklist(block IP) do |req| Rails.cache.read(block #{req.ip}) end运行期维护方式封禁一个 IPRails.cache.write(block 1.2.3.4, true, expires_in: 2.days)—— 2 天后自动过期天然支持临时封禁解封一个 IPRails.cache.delete(block 1.2.3.4)查询封禁状态Rails.cache.read(block 1.2.3.4)。原理与边界为什么可行blocklist 规则块Blocklist继承自Check见 lib/rack/attack/blocklist.rb只是在每个请求上执行一次块并检查 truthylib/rack/attack/check.rb完全不经过计数器。所以把块内的判断换成读缓存后黑名单就变成了运行期可写的动态数据源。与 throttle 存储无关这一点很重要——blocklist/safelist不使用Rack::Attack.cacheRack::Attack.cache仅服务于 throttle、track、fail2ban、allow2ban参见 README.md 的 Cache store configuration 一节所以这里直接读写Rails.cache即可无需关心Rack::Attack.cache.store配置成什么。多进程一致性只要Rails.cache是 Redis / Memcached 等共享存储所有应用进程都能读到同一份黑名单这正是该方案相比进程内 MemoryStore 的核心优势。六、用 Allow2Ban 拦截 Basic Auth 爆破问题场景Basic Auth 只有用户名 密码两层校验是最容易被爆破的目标之一。文档给出的方案是5 次认证失败1 分钟内后封禁该 IP 1 小时。完整示例# After 5 requests with incorrect auth in 1 minute, # block all requests from that IP for 1 hour. Rack::Attack.blocklist(basic auth crackers) do |req| Rack::Attack::Allow2Ban.filter(req.ip, :maxretry 5, :findtime 1.minute, :bantime 1.hour) do # Return true if the authorization header is incorrect auth Rack::Auth::Basic::Request.new(req.env) auth.credentials ! [my_username, my_password] end endAllow2Ban 的行为语义源码级Allow2Ban继承自Fail2Banlib/rack/attack/allow2ban.rb关键差异在fail!方法Fail2Banfail!返回truelib/rack/attack/fail2ban.rb即第一次失败请求就立即被 blocklist 拦截403同时累计计数达到maxretry后写入 ban 标志Allow2Banfail!只累计计数、返回falselib/rack/attack/allow2ban.rb源码注释写得很直白we may not block them this time, but theyre banned for next time——在达到maxretry之前请求照常放行交给应用返回认证失败达到后 IP 被 ban后续所有请求无论认证是否成功一律 403。对应测试 spec/allow2ban_spec.rb 验证了完整状态机未达 maxretry 时失败请求返回 200 且计数递增、不写入 ban key达到 maxretry 后写入allow2ban:ban:key此后即使正常请求也返回 403且不再递增计数。fail2ban 的行为差异可对照 spec/fail2ban_spec.rb失败请求在未达 maxretry 时就已返回 403。参数说明与注意点参数含义本例取值maxretryfindtime 内允许的最大失败次数超过即封禁5findtime计数观察窗口1.minutebantime封禁时长ban key 的过期时间1.hour实现细节上Rack::Auth::Basic::Request.new(req.env)是 Rack 标准库对 Basic Auth 请求头的解析器auth.credentials返回[username, password]数组请求头缺失或格式非法时返回 nilnil ! [my_username, my_password]为 true同样计为一次失败。另注意filter的三个选项都是必填的缺少会直接抛ArgumentErrorlib/rack/attack/fail2ban.rb。多规则区分若应用里配置了多个 Fail2Ban/Allow2Ban 规则务必在 discriminator 中加入作用域前缀如pentest:#{req.ip}否则各规则的计数与 ban 标志会互相串扰详见 README.md 的 Fail2Ban 一节。七、在 Rails 中按控制器动作controller#action匹配问题场景用正则匹配 URL 往往脆弱且难以维护路由带参数、嵌套资源、i18n 路径等而 Rails 应用本身就有精确的路由解析能力。文档建议直接借用Rails.application.routes.recognize_path把 URL 还原成控制器动作再与规则比较。完整示例Rack::Attack.safelist(unlimited requests) do |request| safelist [ controller#action, another_controller#another_action ] route (Rails.application.routes.recognize_path request.url rescue {}) || {} action #{route[:controller]}##{route[:action]} safelist.any? { |safe| action safe } end机制解读recognize_path request.url返回匹配到的路由参数哈希其中包含:controller与:action拼成controller#action字符串后与白名单数组比对。rescue {}的必要性对未注册路由404 场景recognize_path会抛出路由错误兜底为空哈希此时action为#{}不会命中白名单请求继续走正常判定流程。为什么放在 safelistsafelist 优先级最高见导读部分的执行顺序命中的请求无条件放行适合内部接口免限流类需求。同样的技巧也可以放进 blocklist 实现某动作限流/封禁。相关实现佐证Rack::Attack 在进入规则判定前会把请求路径规范化env[PATH_INFO] PathNormalizer.normalize_path(env[PATH_INFO])lib/rack/attack.rb。在 Rails 环境下PathNormalizer使用ActionDispatch::Journey::Router::Utils去除尾部斜杠等见 lib/rack/attack/path_normalizer.rb这与recognize_path的路由语义保持一致避免路径末尾斜杠差异导致正则失配的经典坑。结语用好进阶配置的三条原则回顾本文 8 个方案可以提炼出三条实用原则数据源可替换blocklist/throttle 的判定逻辑本质是块 返回值因此数据源可以灵活替换为 ENV第三节、Rails.cache第五节、Rack::Auth 解析结果第六节或 Rails 路由第七节替换时注意区分哪些走Rack::Attack.cachethrottle/fail2ban/allow2ban哪些不走safelist/blocklist。组合优于造轮子指数退避第一节用多层 throttle组合实现封禁阈值第六节用现成的 Allow2Ban 原语实现重置计数第四节应优先基于Cache#reset_count/Fail2Ban.reset等官方原语而非手工拼 key。进阶配置需自测原文档明确标注这些代码未经测试尤其 monkey-patch第二节、第四节与跨进程缓存第五节方案务必在接入生产前用 spec 目录下的测试风格如 spec/allow2ban_spec.rb、spec/rack_attack_throttle_spec.rb补充你自己的回归用例并确认缓存 store 满足increment/write等接口要求lib/rack/attack/cache.rb。更完整的入门配置、Fail2Ban 参数细节、自定义响应与 RateLimit 响应头等能力可继续查阅 docs/example_configuration.md 与 README.md。赞分享应用安全后端【免费下载链接】rack-attackRack middleware for blocking throttling项目地址https://gitcode.com/gh_mirrors/ra/rack-attack点击查看免费下载相关推荐Rack::Attack 高级配置应对复杂攻击场景的10个策略Rack::Attack 是 Ruby 和 Rails 应用中用于阻止和限制恶意请求的强大中间件。在当今复杂的网络安全环境中简单的防护策略往往不足以应对各种攻应用安全后端学之思开源考试系统XZS二次开发指南零基础扩展新题型和功能模块的完整教程学之思开源考试系统XZS二次开发指南零基础扩展新题型和功能模块的完整教程 学之思开源考试系统XZS是一款功能强大的在线考试平台支持多种题型和灵活的部署方式。教育后端前端小程序Rack::Attack实战构建高效的Web应用防护层Rack::Attack实战构建高效的Web应用防护层 什么是Rack::Attack Rack::Attack是一个基于Rack中间件的Ruby安全防护工具应用安全后端上一篇toyDB与GraphQL构建API的新方式下一篇ts-toolbelt社区贡献指南如何为TypeScript最大类型库提交PR创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考