Implementing Rate Limiting in Rails
Bài đăng này đã không được cập nhật trong 9 năm
Giới hạn tần suất truy cập của người dùng đến một dịch vụ web được đưa ra nhằm ngăn chặn sự xâm chiếm tài nguyên bằng một thiết lập phụ với người dùng trong hệ thống. Bằng cách thông báo meassage báo lỗi khi họ vượt quá số lượt truy cập trong một khoảng thời gian nhất định. Thêm vào thông báo lỗi bao gồm thông tin như khi giới hạn được thiết lập lại và người dùng có thể tiếp tục thực hiện truy cập hệ thống sau khi reset.
Sau đây chúng ta sẽ đi tìm hiểu làm thế nào để thiết lập giới hạn lượt cho người dùng trong hệ thống.
1. Ứng dụng test
Chúng ta sẽ tạo một ứng dụng đơn giản với API đơn giản foo.json
Ta sẽ cần làm như sau:
file routes.rb
> # config/routes.rb
> RailsThrottle::Application.routes.draw do
> get 'foo.json' => 'foo#index'
> end
Tạo file controller foo_controller.rb
> # app/controllers/foo_controller.rb
> class FooController < ApplicationController
> def index
> render json: {foo: :bar}
> end
> end
2. Lưu trữ dữ liệu số lượt
Chúng ta cần một nơi để lưu trữ mỗi địa chỉ IP của người dùng và số yêu cầu nó đưa ra. Tăng count cho mỗi một yêu cầu và thiết lập lại về 0 sau một khoảng thời gian nào đó định trước. Xem xét những yêu cầu, Redis rất phù hợp để luư trữ dữ liệu này. Redis lưu trữ các cặp khoá - giá trị và thời hạn cho phép được quy định cho mỗi mục. Redis cũng đi kèm với lệnh INCR
để đảm bảo rằng hành động tăng lên là tự động. Điều này sẽ giúp ích cho chúng ta nếu chúng ta chạy nhiều trường hợp của ứng dụng sau một lần load.
Để cài đặt ứng dụng sử dụng Redis, chúng ta sẽ cần cài đặt gem redis
. Sau khi có gem rồi chúng ta sẽ thêm một initializer
mới đặt tên là throttle.rb
:
> # config/initializers/throttle.rb
> require "redis"
> redis_conf = YAML.load(File.join(Rails.root, "config", "redis.yml"))
> REDIS = Redis.new(:host => redis_conf["host"], :port => redis_conf["port"])
Nó sẽ load port và host của server Redis từ cấu hình trong file nằm trong config/redis.yml
:
> # config/redis.yml
> host: localhost
> port: 6379
**3. Sử dụng before_filter cho giới hạn lượt **
Bước đầu tiên là ghi lại số yêu cầu mỗi khách hàng đặt ra. Điều này có thể dễ dàng đạt được với before_filter
. Ta thêm filter vào ApplicationController
.
> # app/controllers/application_controller.rb
> class ApplicationController < ActionController::Base
> ...
> before_filter :throttle
>
> def throttle
> client_ip = request.env["REMOTE_ADDR"]
> key = "count:#{client_ip}"
> count = REDIS.get(key)
>
> unless count
> REDIS.set(key, 0)
> end
> REDIS.incr(key)
> true
> end
> ...
> end
Khi đó before_filter
thuộc ApplicationController
, nó sẽ được áp dụng cho tất cả các yêu cầu, trừ khi một controller cụ thể chọn bỏ qua nó. Vì vậy, trước mọi yêu cầu được xử lý, lọc lấy IP của khách và kiểm tra xem count trong Redis cho IP đó. Nếu không có count của key thì nó sẽ khởi tạo cho nó. Cuối cùng nó tăng count lên.
Lúc này, mới chỉ lọc ra và ghi lại các yêu cầu mà chưa giới hạn nó. Bây giờ ta sẽ đi thực hiện việc giới hạn. Chúng ta cần xác định khung thời gian để giới hạn lượt và bao nhiêu yêu cầu nên được phép trong thời gian đó. Chúng ta sẽ cho phép một khách hàng tối đa được 60
lượt trong 15
phút. Ta sẽ định nghĩa hằng số trong throttle.rb
> THROTTLE_TIME_WINDOW = 15 * 60
> THROTTLE_MAX_REQUESTS = 60
Bộ lọc cần phải được thay đổi tương ứng với các thông báo lỗi kbi lượt truy cập vượt quá giới hạn.
> # app/controllers/application_controller.rb
>
> class ApplicationController < ActionController::Base
> ...
>
> before_filter :throttle
>
> def throttle
> client_ip = request.env["REMOTE_ADDR"]
> key = "count:#{client_ip}"
> count = REDIS.get(key)
>
> unless count
> REDIS.set(key, 0)
> REDIS.expire(key, THROTTLE_TIME_WINDOW)
> return true
> end
>
> if count.to_i >= THROTTLE_MAX_REQUESTS
> message = "You have fired too many requests. Please wait for some time."
> render :status => 429, :json => {:message => message}
> return
> end
> REDIS.incr(key)
> true
> end
>
> ...
> end
Khi đạt đến giới hạn, yêu cầu tiếp theo sẽ được gửi lại một thông báo lỗi và mã code là 429
. Mã code 429
chỉ ra rằng người dùng đã gửi quá nhiều yêu cầu trong một khoảng thời gian nhất định.
Hãy thử kiểm tra điều này:
bash$ for i in {1..100}
do
curl -i http://localhost:3000/foo.json >> /dev/null
done
bash$ less log/development.log | grep "200 OK" | wc -l
60
bash$ less log/development.log | grep "429 Too Many Requests" | wc -l
40
4. Cải tiến
Trong khi ta thực hiện giới hạn các yêu cầu, nó không cung cấp cấp cho khách hàng đầy đủ thông tin như phải chờ bao lâu trước khi thực hiện lại yêu cầu tiếp theo. Nó cũng sẽ hữu ích hơn nếu máy chủ thông báo trên mỗi yêu cầu là tổng số bao nhiêu requests nó cho phép được thực hiện trong một cửa sổ và bao nhiêu requests có thể thực hiện trước khi hạn chế.
Để báo cho khách hàng về các thông số giới hạn, cơ chế cần để có thể thiết lập headers trên response. before_filter
hữu ích trong việc giới hạn các yêu cầu, nhưng nó không thể thay đổi response từ một yêu cầu hợp lệ. Ta có thể sử dụng after_filter
để đạt được điều này.
Chúng ta sẽ comment before_filter
lại. Sau đó chúng ta sẽ định nghĩa một middleware rỗng. Quy ước định nghĩa middleware trong thư mục app/middleware
.
> # app/middleware/rate_limit.rb
>
> class RateLimit
> def initialize(app)
> @app = app
> end
>
> def call(env)
> @app.env
> end
> end
middleware được sử dụng như sau:
> # config/application.rb
>
> class Application < Rails::Application
> ...
> config.middleware.use "RateLimit"
> end
5. Giới hạn lượt cơ bản
Thực hiện lại những việc đã làm ở trên thay thế sử dụng middleware.
> def call(env)
> client_ip = env["REMOTE_ADDR"]
> key = "count:#{client_ip}"
> count = REDIS.get(key)
> unless count
> REDIS.set(key, 0)
> REDIS.expire(key, THROTTLE_TIME_WINDOW)
> end
>
> if count.to_i >= THROTTLE_MAX_REQUESTS
> [
> 429,
> {},
> [message]
> ]
> else
> REDIS.incr(key)
> @app.call(env)
> end
> end
>
> private
> def message
> {
> :message => "You have fired too many requests. Please wait for some time."
> }.to_json
> end
6. Trạng thái giới hạn
Các headers biểu diễn các trạng thái giới hạn số lượt:
X-RateLimit-Limit
: số lượng tối đa các yêu cầu mà khách hàng được phép thực hiện tron g thời gian cho phép.X-RateLimit-Remaining
: số lượng các yêu cầu còn lại hiện thời trong khoảng thời gian đó.X-RateLimit-Reset
: khoảng thời gian mà giới hạn lượt hiện thời sẽ được thiết lập lại.
middleware sẽ thiết lập headers cho tất cả các yêu cầu với thay đổi:
> def call(env)
> client_ip = env["REMOTE_ADDR"]
> key = "count:#{client_ip}"
> count = REDIS.get(key)
> unless count
> REDIS.set(key, 0)
> REDIS.expire(key, THROTTLE_TIME_WINDOW)
> end
>
> if count.to_i >= THROTTLE_MAX_REQUESTS
> [
> 429,
> rate_limit_headers(count, key),
> [message]
> ]
> else
> REDIS.incr(key)
> status, headers, body = @app.call(env)
> [
> status,
> headers.merge(rate_limit_headers(count.to_i + 1, key)),
> body
> ]
> end
> end
>
> private
> def message
> {
> :message => "You have fired too many requests. Please wait for some time."
> }.to_json
> end
>
> def rate_limit_headers(count, key)
> ttl = REDIS.ttl(key)
> time = Time.now.to_i
> time_till_reset = (time + ttl.to_i).to_s
> {
> "X-Rate-Limit-Limit" => "60",
> "X-Rate-Limit-Remaining" => (60 - count.to_i).to_s,
> "X-Rate-Limit-Reset" => time_till_reset
> }
> end
Nó sẽ tính toán thời gian còn lại cho đến khi giới hạn được thiết lập lại, số các yêu cầu còn lại và đặt headers tương ứng.
Kết Luận
Trên đây đã trình bày cách để giới hạn lượt thực hiện yêu cầu của người dùng trên một trang web với ruby on rails. Cảm ơn bạn đã theo dõi bài viết! Mong rằng nó sẽ giúp ích cho bạn
All rights reserved