如何借助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:
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
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.
- Combine with Global Locking: Keep Devise's default
lockablemodule enabled to enforce global user locks if needed. Just update yourip_locked?method to also checkaccess_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

