Implementing Rate Limiting in Rails

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 😄