据我了解,这仅用于学习目的,所以这里是如何实现它的小细节。为了在生产中使用它,我绝对建议只使用 Rack Throttle,因为有许多极端情况和选项,我们在这个简单的示例中没有介绍。
这是最小的 Rack 中间件,它只打印出 IP
#!/usr/bin/env rackup
require "rack"
class Limiter
attr_reader :app
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
puts request.ip
app.call(env)
end
end
use Limiter
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, world!\n"]] }
中间件必须是类,因为它们需要有一个初始化器,该初始化器将传递给链中的下一个应用程序。 call 方法获取请求环境,做它需要做的任何事情,然后调用下一个应用程序。我们的中间件现在只是打印出 IP 地址。
https://github.com/rack/rack/blob/master/lib/rack/request.rb#L354
https://thoughtbot.com/upcase/videos/rack
现在我们可以存储每个 ip 地址的请求计数,并在超过限制时阻止请求:
#!/usr/bin/env rackup
require "rack"
class Limiter
attr_reader :app, :options, :counter
def initialize(app, options)
@app = app
@options = options
@counter = {}
end
def call(env)
request = Rack::Request.new(env)
if counter[request.ip].to_i > options[:max].to_i
[200, {'Content-Type' => 'text/plain'}, ["You are blocked, request count: #{counter[request.ip]}\n"]]
else
counter[request.ip] ||= 0
counter[request.ip] += 1
app.call(env)
end
end
end
use Limiter, max: 1
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, world!\n"]] }
如果您执行此应用程序,它将在第一次请求后阻止您。
您可以看到我们现在使用选项max: 1 初始化中间件,我们可以从options 访问中间件内部。我们使用它来确定何时阻止请求。
我们还初始化了一个简单的散列,我们在其中存储每个 IP 地址的请求计数。请注意,这仅适用于您拥有单个 Web 服务器的情况。如果您有更多服务器,则需要在服务器之间同步计数。不错的选择是 Redis 或 Memcache。
总的来说,Rack Throttle 代码可读性很强,因此我建议通读一遍。大多数逻辑都在Limiter class 中实现,大多数其他类都从该Limiter class 继承。
编辑
作者提出的这个问题
通过相同的解决方案,您的意思是这些 gem 具有跟踪 7 天流量的内置功能,如果是,您能解释一下如何做到这一点吗?
AFAIK Rack::Throttle 尚未实现此功能,但通过继承 TimeWindow 可以轻松添加
class Weekly < RackThrottle::TimeWindow
##
# @param [#call] app
# @param [Hash{Symbol => Object}] options
# @option options [Integer] :max (3600)
def initialize(app, options = {})
super
end
##
def max_per_week(request = nil)
@max_per_week ||= options[:max_per_week] || options[:max] || 3_600 * 7
end
alias_method :max_per_window, :max_per_week
protected
##
# @param [Rack::Request] request
# @return [String]
def cache_key(request)
# %U returns the calendar week
[super, Time.now.strftime('%U')].join(':')
end
end
请注意,这将按日历周进行跟踪。如果您想要滚动跟踪器,您需要每天计数,然后获取最近 7 天的数据。