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

Rails 7 Active Record记录级加密密钥删除后仍可解密问题咨询

问题排查

你的代码存在4个核心逻辑错误,导致自定义的单记录密钥完全没生效,删除密钥后自然还能解密:

  • encrypts配置的key参数是类加载阶段求值的,你传入的self.instance_encryption_key是类方法,只会在Rails加载User类时执行一次,后续读写单条记录的加密属性时,根本不会读取你给每个实例赋值的p_encryption_key,全程用的是Rails全局默认加密密钥,你自己生成的单条密钥从头到尾没参与加解密流程。
  • 方法名拼写错误:get_user_encryption_key里调用的是不存在的user_key_name方法,你实际定义的缓存key方法叫profile_key_name,就算密钥逻辑走到这一步也会报错。
  • 开启了deterministic: true(确定性加密),该模式为了支持等值查询,强制使用全局固定的确定性加密密钥,本身就不支持单记录自定义密钥。
  • after_initialize回调在每次实例化User对象时都会执行,一旦缓存没命中就会自动生成新密钥,哪怕你删了老密钥,下次加载记录也会生成新key,完全无法匹配历史密文。
正确实现方案

Rails 7原生提供了单记录动态密钥的接口,不需要自己写零散的回调实现,完全满足「删密钥即永久无法解密」的crypto shredding需求:

  • 不要对需要擦除的加密字段开deterministic: true,如果需要按该字段查询,单独存一个确定性加密的哈希字段做索引即可。
  • 不要把密钥只存在缓存里,必须持久化到独立的密钥表,缓存仅做读取加速,避免缓存丢失导致全量数据无法解密。
  • 重写模型实例的encryption_key_for方法,这是Rails加解密单条记录属性时会自动调用的动态密钥入口,直接返回对应记录的专属密钥即可。

实现代码

首先生成密钥表迁移:

class CreateUserEncryptionKeys < ActiveRecord::Migration[7.0]
  def change
    create_table :user_encryption_keys do |t|
      t.references :user, null: false, foreign_key: true, index: { unique: true }
      t.string :key, null: false
      t.timestamps
    end
  end
end

密钥表关联模型:

class UserEncryptionKey < ActiveRecord::Base
  belongs_to :user
end

修正后的User模型:

class User < ActiveRecord::Base
  has_one :encryption_key_record, class_name: 'UserEncryptionKey', dependent: :destroy

  # 需要擦除的字段关闭确定性加密
  encrypts :last_name, deterministic: false
  # 单独存哈希字段用于查询,开启确定性加密
  encrypts :last_name_query_hash, deterministic: true

  before_save :sync_last_name_query_hash, if: :last_name_changed?

  # Rails原生动态密钥入口,每次加解密指定属性时自动调用
  def encryption_key_for(attribute)
    case attribute
    when :last_name
      fetch_or_generate_per_record_key
    else
      super # 其他属性沿用全局加密配置
    end
  end

  # 数据擦除方法:删除密钥后该记录的last_name永久无法解密
  def crypto_shred_last_name
    encryption_key_record&.destroy
    Rails.cache.delete(per_record_key_cache_key)
  end

  private

  def sync_last_name_query_hash
    self.last_name_query_hash = last_name
  end

  def per_record_key_cache_key
    "user_encryption_key/#{id}"
  end

  def fetch_or_generate_per_record_key
    return nil if new_record?
    # 优先读缓存,缓存miss读数据库,数据库无记录则生成新密钥持久化
    Rails.cache.fetch(per_record_key_cache_key) do
      (encryption_key_record || create_encryption_key_record!).key
    end
  end

  def create_encryption_key_record
    random_key = ActiveRecord::Encryption.key_generator.generate_random_hex_key(length: 16)
    create_encryption_key_record!(key: random_key)
  end
end

验证逻辑

创建一条带last_name的User记录后,调用user.crypto_shred_last_name删除密钥,重新从数据库加载该记录再读取last_name时,会直接抛出ActiveRecord::Encryption::Errors::Decryption解密错误,符合预期。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 16:18:20