Rails Active Storage 的正确使用姿势

huacnlee · November 01, 2018 · Last by SpiderEvgn replied at August 24, 2020 · 8927 hits

Active Storage 已经发布很久了,都在跃跃欲试吧?我们是否能在我们的系统里面提代替掉 Carrierwave?


最近在一个实际的产品场景里面用上了 Active Storage,实际感受不太好,吃了一鼻子灰。

原因是因为 Active Storage 的设计考虑较多,感觉是针对 签名 URL 的私密存储 (Private ACL) 的场景设计的,自带的功能以及 Rails Guides 里面的指导 带出来的结果最终生成的还是偏向于签名 URL 生成。

然而我们往往在 Rails 项目里面用的最多的反而是公开存储的场景,例如:用户头像公司 Logo新闻附件图片等,这些不需要那么复杂的鉴权签名机制,同时新闻图片这类场景我们需要一个固定 URL 地址,以便于插入到 Rich HTML 里面去,否则像 Acitve Storage 默认的那样,URL 有有效期,这可没法玩。


我分析发现默认情况下,Active Storage 生成的 URL 地址包含两部分(Blob key 的签名段,缩略图规则的签名段),这两段签名包含有效期,同时基于 secret_key_base 来签名。

也就是说假如有一天,我们的 secret_key_base 改变了(这个是很有可能发生的),那么就算图片 URL 没有效期,插入到 HTML 里面的图片地址依然会失效。然后这个问题可能还会在你的系统跑了一段时间以后出现。

于是我今天发了几条 Tweet 说了一下这个事情:

https://twitter.com/huacnlee/status/1057844908654977024

关于上面的问题,Rails Issues 里面也有人反馈,但看起来也没给出正确的解决方案(我怀疑他们可能都不用 :disk 存储)。

https://github.com/rails/rails/issues/31419


于是乎,翻了一些资料,以及阅读 Active Storage 的实现,我找到了一个稳妥的解决方案。

这里以 :disk 的存储场景距离,使用 Aliyun 的可以跳过,直接用我弄好的 activestorage-aliyun 即可,用 service_url 生成的 URL 不会有上面说的问题。

解决方式大致是新增一个 uploads_controller 用来代替 Active Storage 自带的 Controller:

class UploadsController < ApplicationController
  before_action :require_disk_service!
  before_action :set_blob

  # GET /upload/:id
  # GET /upload/:id?s=large
  def show
    if params[:s]
      # generate thumb
      variation_key = Blob.variation(params[:s])
      send_file_by_disk_key @blob.representation(variation_key).processed.key, content_type: @blob.content_type
    else
      # return raw file
      send_file_by_disk_key @blob.key, content_type: @blob.content_type, disposition: params[:disposition], filename: @blob.filename
    end
  end

  private

    def send_file_by_disk_key(key, content_type:, disposition: :inline, filename: nil)
      opts = { type: content_type, disposition: disposition, filename: filename }
      send_file Blob.path_for(key), opts
    end

    def set_blob
      @blob = ActiveStorage::Blob.find_by(key: params[:id])
      head :not_found if @blob.blank?
    end

    def require_disk_service!
      head :not_found unless Blob.disk_service?
    end
end

config/routes.rb 新增一行

resources :uploads

需要一个 app/model/blob.rb,用来自定义缩略图名称,以及对于的 ImageMagick 转换规则,比如我这个:

class Blob
  class << self
    IMAGE_SIZES = { tiny: 32, small: 64, medium: 96, xlarge: 2400 }

    def disk_service?
      ActiveStorage::Blob.service.send(:service_name) == "Disk"
    end

    def variation(style)
      ActiveStorage::Variation.new(combine_options: combine_options(style))
    end

    def combine_options(style)
      style = style.to_sym
      size = IMAGE_SIZES[style] || IMAGE_SIZES[:small]

      if style == :xlarge
        { resize: "#{size}>" }
      else
        { thumbnail: "#{size}x#{size}^", gravity: "center", extent: "#{size}x#{size}" }
      end
    end

    def path_for(key)
      ActiveStorage::Blob.service.send(:path_for, key)
    end
  end
end

最后,需要用到图片、附件固定 URL 的场景,改用 upload_path 代替:

<%= image_tag upload_path(@user.avatar.blob.key, s: :small) %>

最后可以得到这样的结果:

我整理了一个例子项目,有需要的可以拉下来,跑起来看看:

https://github.com/huacnlee/rails-activestorage-example

用 activestorage 如果后面接入的是 cdn 服务,就会出一个问题,就是访问图片要先请求 rails,然后再跳转到 cdn 上去,这个还是蛮影响系统的,不知道有没有解决办法?

Reply to hging

你可以看看 activestorage-aliyun 的实现,README 上有说明,用 service_url  的方式生成,可以直接是 CDN 地址

S3 Storage (1038.9ms) Checked if file exists at key: variants/Ef3kRjSHxoqNw86TTNDPz8DC/10d4e6731e902108be27818bfbb7b760bac6df85d9578ddb35096d2102c0bd89 (yes)

缩略图的使用,如果直接用 self.avatar.representation(variation_key).processed.service_url 这种机制,会在每次使用的时候有网络 IO

貌似代码块在通知这里的显示有些 bug

huacnlee in ActiveStorage 的 Log 好长啊,看着好难受 mention this topic. 14 Jan 11:20

也就是说假如有一天,我们的 secret_key_base 改变了(这个是很有可能发生的),那么就算图片 URL 没有效期,插入到 HTML 里面的图片地址依然会失效。然后这个问题可能还会在你的系统跑了一段时间以后出现。

这个好办,可以在 config/application.rb 里添加这样一段,让 ActiveStorage 使用不同的 secret 来做签名

initializer "app.active_storage.verifier", after: "active_storage.verifier" do
  config.after_initialize do |app|
    storage_key_base =
      if Rails.env.development? || Rails.env.test?
        app.secrets.secret_key_base
      else
        validate_secret_key_base(
          ENV["STORAGE_KEY_BASE"] || app.credentials.storage_key_base || app.secrets.storage_key_base
        )
      end
    key_generator = ActiveSupport::KeyGenerator.new(storage_key_base, iterations: 1000)
    secret = key_generator.generate_key("ActiveStorage")
    ActiveStorage.verifier = ActiveSupport::MessageVerifier.new(secret)
  end
end
7 Floor has deleted

看起来 除非 master.key , credentials.yml.enc , secret_key_base,永远不变。 但是,似乎,难免会遇到需要修改 secret_key_base 的时候,就像是密码,怎么可能永久不变。这个时候,rich html 里面的 SGID 可能就坏了... 所有文章的图片附档,不就通通失效.....

感觉,如果一定要 Rich Html 用途,Carrierwave 似乎比较省事? 安全要求较高的,且不将附档放在文章内,再使用 Active Storage

上传到 aliyun 的时候该如何自动设置文件的 header 呢?

You need to Sign in before reply, if you don't have an account, please Sign up first.