Devise 到自定义认证:不仅仅是 `has_secure_password`

发布: (2026年2月1日 GMT+8 19:04)
6 min read
原文: Dev.to

Source: Dev.to

Devise 到自定义认证的封面图:这不仅仅是 has_secure_password

想要切换的冲动

Devise 是 Rails 认证领域的 800 磅大猩猩。它可靠、安全,并且能 做所有事
但最终你会碰到“墙”。你可能想要无密码登录流程,厌倦了为了改一个重定向而覆盖数十个控制器方法,或者只是想了解自己的应用是如何登录用户的。

于是你决定:“我要把 Devise 拔掉,改用 Rails 内置的 has_secure_password。”

我已经完成了这次迁移。它让代码库更简洁、更快——但通往那里的道路布满地雷。以下是你将面临的最大挑战。

挑战 1:数据库模式不匹配

Devise 对数据库列有自己的约定。

  • Devise 使用:encrypted_password
  • Rails(has_secure_password)期望:password_digest

你有两种选择,各有利弊。

选项 A:别名(快速且脏)

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password :password, validations: false
  alias_attribute :password_digest, :encrypted_password
end

风险: 这种做法感觉像是 hack。未来的 gem 或工具如果期待标准的 Rails 约定,可能会出问题。

选项 B:迁移(干净但有风险)

# db/migrate/xxxx_rename_encrypted_password.rb
class RenameEncryptedPassword < ActiveRecord::Migration[6.1]
  def change
    rename_column :users, :encrypted_password, :password_digest
  end
end

风险: 确保部署期间零停机时间。回滚可能会很快变得混乱。

挑战 2: “Recoverable” 模块

Devise 为你免费提供了“Forgot Password”。在将其移除后,除非你自行实现,否则任何人都无法重置密码

安全的密码重置流程必须:

  1. 生成唯一的、高熵的令牌。
  2. 存储该令牌的摘要(永不存储原始令牌)。
  3. 设置过期时间(例如,2 小时)。
  4. 处理电子邮件发送。
  5. 关键: 使用后使令牌失效,以防重放攻击。

Devise 为你处理了时序攻击防护和令牌哈希;现在这项安全责任由你承担。

挑战 3:Cookie 安全与 “记住我”

session[:user_id] 适用于基本登录,但 “记住我” 更为复杂。Devise 的 Rememberable 模块负责持久化 cookie、令牌轮换以及安全比较。

如果自行实现:

  • 对 cookie 进行签名/加密。
  • 防范 会话劫持(被盗的 cookie 可授予长期访问权限)。
  • 提供一种 “使所有会话失效” 的方式(例如,当用户更改密码时)。

挑战 4:视图与控制器清理

Devise 的帮助方法遍布各处。你需要在整个代码库中替换它们:

  • authenticate_user! → 你自己的 require_login 过滤器
  • user_signed_in?logged_in?
  • current_user → 在 ApplicationController 中的自定义帮助方法
  • devise_error_messages! → 你自己的错误局部视图

小贴士: 不要立刻删除该 gem。创建一个 shim,将旧的 Devise 方法名映射到你的新逻辑,从而可以逐步迁移。

# app/controllers/concerns/auth_shim.rb
module AuthShim
  extend ActiveSupport::Concern

  included do
    helper_method :current_user, :logged_in?
  end

  def authenticate_user!
    redirect_to login_path unless logged_in?
  end

  def user_signed_in?
    logged_in?
  end

  def logged_in?
    !!current_user
  end

  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end
end

ApplicationController 中包含 AuthShim,并逐步替换调用。

挑战 5:遗留密码兼容性

Devise 和 has_secure_password 默认都使用 BCrypt,因此它们通常可以互操作。然而,较旧的 Rails 应用(例如从 Rails 3/4 迁移而来)可能将 Devise 配置为使用遗留的哈希策略(带盐的 SHA‑1)。has_secure_password 无法验证这些哈希。

解决方案: 实现自定义认证方法,步骤如下:

  1. 检测存储的密码是否为 BCrypt 哈希。
  2. 如果不是,则使用遗留的验证逻辑进行校验。
  3. 在遗留登录成功后,使用 BCrypt 重新对密码进行哈希并更新记录。

判定:值得吗?

如果以下情况,请不要这样做:

  • 为客户构建 MVP(速度很重要)。
  • 严重依赖 OmniAuth(Devise 能简化 OAuth 集成)。
  • 对会话固定或时序攻击等安全概念不熟悉。

如果以下情况,请这样做:

  • 正在构建预计使用 5 年以上的单体应用。
  • 需要专门的流程(仅 OTP、魔法链接、多步骤 onboarding)。
  • 想要降低内存占用和依赖膨胀。

移除 Devise 是在 Rails 中最具教育意义的操作之一。带上手电筒——管道里很暗。

Back to Blog

相关文章

阅读更多 »