はじめに
ActiveStorageにデフォルトで用意されているURLヘルパでは、何も認証が掛かっていない永続的なURLが生成されます。
具体的にはバックエンドがs3の場合、以下のような2種類のURLが生成されます。
- https://example.test/rails/active_storage/blobs/redirect/SIGNED_ID/sample.jpg
- 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
そこで今回の記事では比較的簡単な方法で、(デフォルト設定よりも)セキュアに利用する方法を紹介します。
設定方法
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
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
- Allow expires_in for ActiveStorage signed ids by aki77 · Pull Request #42059 · rails/rails
- Add support for ActiveStorage expiring URLs by aki77 · Pull Request #42410 · rails/rails
-
サムネイル未生成の場合、Viewのレンダリング時に同期的にサムネイル生成やアップロード処理が走ることになり、レスポンス速度に悪影響がでます。 ↩