LoginSignup
23
14

More than 1 year has passed since last update.

Active StorageのURLを期限付きにすることでセキュアに利用する

Posted at

はじめに

ActiveStorageにデフォルトで用意されているURLヘルパでは、何も認証が掛かっていない永続的なURLが生成されます。
具体的にはバックエンドがs3の場合、以下のような2種類のURLが生成されます。

  1. https://example.test/rails/active_storage/blobs/redirect/SIGNED_ID/sample.jpg
  2. https://BUCKET.s3.ap-northeast-1.amazonaws.com/KEY?QUERY

この場合、1のrailsのURLは認証なし(期限なしのSigned Id)の永続的なURLで、そこからリダイレクトされる2のs3のURLが期限付きという挙動になります。
そのため万が一、1の永続的URL漏洩時に備えて何かしらの認証処理を挟もうとすると一手間必要となります。

ログインユーザーなら誰でも全てのファイルを閲覧可能みたいな簡易的な制限で良ければ比較的簡単に対応可能ですが、現実的にはそれで済むケースは少ないのではないかと思います。
また「ActiveStorage 認証」といったワードで検索すると以下のようなコードがよく出てきます。

class ActiveStorage::BlobsController < ActiveStorage::BaseController
  include ActiveStorage::SetBlob

  def show
    if access_allowed?
      expires_in ActiveStorage.service_urls_expire_in
      redirect_to @blob.service_url(disposition: params[:disposition])
    else
      head :forbidden
    end
  end
end

しかしこのコードではBlobの種類が増えれば増えるほど、access_allowed?の中身が苦しいことになっていくのが目に見えています。
またRails公式のスタンスとしても、権限チェックを挟みたい場合は各モデルに対応したコントローラーを用意することが推奨されています。

しかしモデルの数が多かったり、CarrierWaveからActiveStorageに移行するとなった場合、各モデル毎に認証用のコントローラーを用意するというのはなかなか大変かと思います。
またActiveStorage::Blob#service_urlを直接利用するといった方法を見かけることもありますが、その場合はサムネイル表示のケースで問題が出てきます。1
そこで今回の記事では比較的簡単な方法で、(デフォルト設定よりも)セキュアに利用する方法を紹介します。

設定方法

config/routes.rb
Rails.application.routes.draw do
  # https://github.com/rails/rails/blob/v6.1.3.2/activestorage/config/routes.rb#L60-L81
  # を元に`expires_in`オプションを受け付けるように定義しなおしています
  direct :app_blob do |model, options|
    # 1.hourはデフォルトの有効期限
    expires_in = options.delete(:expires_in) { 1.hour }

    if model.respond_to?(:signed_id)
      route_for(
        :rails_service_blob,
        model.signed_id(expires_in: expires_in),
        model.filename,
        options
      )
    else
      signed_blob_id = model.blob.signed_id(expires_in: expires_in)
      variation_key  = model.variation.key
      filename       = model.blob.filename

      route_for(
        :rails_blob_representation,
        signed_blob_id,
        variation_key,
        filename,
        options
      )
    end
  end
end
config/initializers/activestorage.rb
Rails.application.config.active_storage.resolve_model_to_route = :app_blob

if Rails::VERSION::MAJOR < 7
  ActiveSupport.on_load(:active_storage_blob) do
    module ActiveStorageBlobPatched
      def signed_id(**options)
        self.class.superclass.superclass.instance_method(:signed_id).bind(self).call(purpose: :blob_id, **options)
      end
    end

    ActiveStorage::Blob.prepend(ActiveStorageBlobPatched)
  end
end

設定内容の解説です。
まずActiveStorage::Blob#signed_id:expires_inキーワード引数を受け付けるようにモンキーパッチを当てています。
またurl_for(blob)といった指定があったときに解決されるルーティングをデフォルトのrails_blobからapp_blobに変更しています。
そしてapp_blobのルーティング定義ではデフォルトで定義されている rails_storage_redirect のルーティングをベースに、expires_inオプションを受け付けるように定義しています。
受け取ったexpires_inオプションを先に書き換えたActiveStorage::Blob#signed_idに渡すことでsigned_blob_idが期限付きとなり、指定期間以降にアクセスしてもファイルが閲覧出来なくなるといった挙動になります。

利用例

以下のようにapp_blob_pathヘルパを呼ぶことでファイル毎に期限を指定可能となります。

<%= image_tag app_blob_path(user.avatar.variant(resize: "100x100"), expires_in: 30.minutes) %>

生成されるURLのフォーマットは一見以前と同じなのですが、Signed Idが毎回固定ではなく期限付きとなっています。
https://example.test/rails/active_storage/blobs/redirect/SIGNED_ID/sample.jpg

Rails7では

今回紹介した期限付きURL生成のための変更箇所については、Rails本体にもプルリクエストを送ってあり無事マージされています。
何も問題がなければRails7以降は標準機能で期限付きURLが簡単に生成できるようになる予定です。🎉

またRails7では以下のような設定を追加することで、アプリケーション全体でActive Storageのデフォルト挙動が認証付きURLを生成するようになります。

config.active_storage.urls_expire_in = 30.minutes

  1. サムネイル未生成の場合、Viewのレンダリング時に同期的にサムネイル生成やアップロード処理が走ることになり、レスポンス速度に悪影響がでます。 

23
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
14