Rails Active Storage 的正确使用姿势

huacnlee · 2018年11月01日 · 最后由 SpiderEvgn 回复于 2020年08月24日 · 8930 次阅读

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 上去,这个还是蛮影响系统的,不知道有没有解决办法?

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 ActiveStorage 的 Log 好长啊,看着好难受 提及了此话题。 01月14日 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 楼 已删除

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

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

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

需要 登录 后方可回复, 如果你还没有账号请 注册新账号