Rails 5.2 から導入されたActiveStorageは rails_blob_path を使うことで署名つきURLへのリダイレクトを返してくれます。
# in view
rails_blob_path(object.image)
# => "/rails/active_storage/blobs/<signed_id>/<filename>"
# 上記のURLにアクセスするとサービスに応じた認証URLへリダイレクトしてくれる。
# たとえばGCSなら `https://storage.googleapis.com/<bucket-name>/xxxyyyzzz`
# というようなURLにリダイレクトされる。
認証URLには有効期限がついている(デフォルトで5分)ので、/rails/active_storage/blobs/** にアクセスが来たときに適当に認証処理を挟むとファイルに対して簡易的なアクセス制御が実現できます。これを実現するには上記のエンドポイントを処理する ActiveStorage::BlobsController を上書きすれば良いです。参考のために ActiveStorage::BlobsControllerのデフォルトの実装を見てみましょう。
rails/blobs_controller.rb at bc9fb9cf8b5dbe8ecf399ffd5d48d84bdb96a9db · rails/rails
# frozen_string_literal: true
# Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
# security-through-obscurity factor of the signed blob references, you'll need to implement your own
# authenticated redirection controller.
class ActiveStorage::BlobsController < ActiveStorage::BaseController
  include ActiveStorage::SetBlob
  def show
    expires_in ActiveStorage::Blob.service.url_expires_in
    redirect_to @blob.service_url(disposition: params[:disposition])
  end
end
実はここのコメントにも**「アクセス制御をしたい場合は自前でリダイレクトのためのコントローラーを実装しろ」**と書いてあります。
というわけでapp/controllers/active_storage/blobs_controller.rbファイルを作成し、アクセス制御のコードを挿入します。アクセス制御の内容はサービスによって様々でしょうから適宜書き換えてみてください。
class ActiveStorage::BlobsController < ActiveStorage::BaseController
  include ActiveStorage::SetBlob
  def show
    expires_in ActiveStorage::Blob.service.url_expires_in
    if access_allowed?(@blob)
      redirect_to @blob.service_url(disposition: params[:disposition])
    else
      head :forbidden
    end
  end
  private
  def access_allowed?(blob)
    # サービスに応じたアクセス処理を記述する。
    ...
  end
end
どのレコードにアクセスが来たのか
上記のコントローラーで参照できるインスタンスは基本的に@blobだけで、Blobオブジェクトはサービス固有の情報を一切持っていないのでこのままではアクセス制御は行えません。しかしながら、@blobに一対多の関係で紐づいている@blob.attachmentsが持つrecordという参照を利用することで、元のレコードを(だいたい)特定できます。Attachmentが複数なので結局どのレコードに対するアクセスなのかは判明しませんが、基本的にはそのうちのどれか一つに対するアクセスが許可されるならば、ここでは認証URLを返して良いと思います(つまり blob.attachments.any? { ... } が true ならOK)。
単一のファイル(Blob)を複数のレコードに対して紐づけることも想定されているためこのような実装になっているそうです( @scivola さんありがとうございます!)。そのような場合は単一のBlobに対し複数のレコードと、「関係」を抽象化したAttachmentが存在するわけですが、複数のレコードのうちどれかに閲覧権を持つユーザーならファイル自体の閲覧権も持つでしょう。
ActiveStorage::BlobsController ではどのレコードに対するアクセスなのか特定できないためこのような回りくどいことをしているわけですが、もしかしたらレコードを特定する方法があるかもしれません(し、ないなら導入しても良いんじゃないでしょうか?)。
expires_in だけ変えたい
認証URLの有効期限(expires_in)はコントローラーで直接変えてもいいですが、config/application.rb 等で ActiveStorage::Service.url_expires_in を設定することでも変更できます。
require_relative 'boot'
require 'rails/all'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module HogeApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2
    ActiveStorage::Service.url_expires_in = 1.minute
  end
end