You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何借助Trackable扩展Devise Lockable实现IP维度登录锁定?

Hey there! Great question—extending Devise's lockable functionality to enforce IP-specific login limits (alongside the existing global lock) is totally feasible, especially since you're already using the Trackable module. Let me walk you through two practical approaches I’ve implemented in production:


Approach 1: Database-backed IP Locking (Persistent & Auditable)

This method stores failed attempt data in your database, making it easy to audit, manually unlock, or integrate with admin tools.

Step 1: Create a Failed Login Attempts Model

First, generate a model to track failed attempts per user and IP:

rails generate model FailedLoginAttempt user:references ip_address:string failed_count:integer last_attempt_at:datetime
rails db:migrate

Step 2: Set Up Associations in the User Model

Add a relationship to your User model to link it with failed attempts:

# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :lockable, :trackable # Ensure Trackable is enabled

  has_many :failed_login_attempts, dependent: :destroy

  # Add a method to check if an IP is locked for this user
  def ip_locked?(ip_address)
    attempt = failed_login_attempts.find_by(ip_address: ip_address)
    return false unless attempt.present?

    # Match your Devise lock thresholds (from config/initializers/devise.rb)
    attempt.failed_count >= Devise.maximum_attempts && 
      (Time.current - attempt.last_attempt_at) < Devise.unlock_in
  end
end

Step 3: Track Failed Login Attempts

Use Warden's failure hook to update failed attempt counts when a login fails. Add this to config/initializers/devise.rb:

Warden::Manager.before_failure do |env, opts|
  user = opts[:resource]
  ip_address = env['action_dispatch.remote_ip'].to_s

  if user.present?
    attempt = user.failed_login_attempts.find_or_initialize_by(ip_address: ip_address)
    attempt.failed_count += 1
    attempt.last_attempt_at = Time.current
    attempt.save!
  end
end

Step 4: Block Logins from Locked IPs

Create a custom Devise Sessions Controller to check for IP locks before processing login:

# app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  before_action :check_ip_lock, only: [:create]

  private

  def check_ip_lock
    user = User.find_by(email: params.dig(:user, :email))
    if user&.ip_locked?(request.remote_ip)
      flash[:alert] = "This IP has been locked due to too many failed attempts. Please try again later."
      redirect_to new_user_session_path and return
    end
  end
end

Update your routes to use this custom controller:

# config/routes.rb
devise_for :users, controllers: { sessions: 'users/sessions' }

Step 5: Reset Attempts on Successful Login

Clear the IP's failed attempts when the user logs in successfully. Add this to config/initializers/devise.rb:

Warden::Manager.after_authentication do |user, auth, opts|
  ip_address = auth.request.remote_ip.to_s
  user.failed_login_attempts.find_by(ip_address: ip_address)&.destroy
end

Approach 2: Cache-backed IP Locking (Lightweight & Temporary)

If you don't need persistent audit logs, use Rails cache (e.g., Redis, Memcached) for a lighter implementation:

Step 1: Update the User Model

Add an IP lock check method that uses the cache:

# app/models/user.rb
def ip_locked?(ip_address)
  cache_key = "failed_attempts:#{id}:#{ip_address}"
  failed_count = Rails.cache.read(cache_key) || 0
  failed_count >= Devise.maximum_attempts
end

Step 2: Track Failed Attempts in Cache

Modify the Warden failure hook to use cache instead of the database:

# config/initializers/devise.rb
Warden::Manager.before_failure do |env, opts|
  user = opts[:resource]
  ip_address = env['action_dispatch.remote_ip'].to_s

  if user.present?
    cache_key = "failed_attempts:#{user.id}:#{ip_address}"
    current_count = Rails.cache.read(cache_key) || 0
    new_count = current_count + 1

    # Cache expires after your Devise unlock window
    Rails.cache.write(cache_key, new_count, expires_in: Devise.unlock_in)
  end
end

Step 3: Reset on Successful Login

Clear the cache key when login succeeds:

# config/initializers/devise.rb
Warden::Manager.after_authentication do |user, auth, opts|
  ip_address = auth.request.remote_ip.to_s
  cache_key = "failed_attempts:#{user.id}:#{ip_address}"
  Rails.cache.delete(cache_key)
end

You can reuse the same custom Sessions Controller from Approach 1 to check for IP locks.


Additional Tips
  • Combine with Global Locking: Keep Devise's default lockable module enabled to enforce global user locks if needed. Just update your ip_locked? method to also check access_locked? if you want to prioritize global locks.
  • Custom Error Messages: Tweak the flash messages to be more specific (e.g., distinguish between IP locks and global user locks).
  • Cleanup Tasks: For the database approach, add a rake task or background job to delete expired failed attempts periodically.

内容的提问来源于stack exchange,提问作者user9726962

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:13:08