Rails

【Rails+Carrierwave】S3の誰でも見れる画像と管理者でしか見れない画像を分けて表示

2021/01/22

問題

ユーザが利用する画面では, Carrierwaveを利用してS3に配置している画像のURLをそのままimgタグで指定し表示する実装をしていた.
そのため, URLさえ分かっていればどこにでも表示することができる.
意図しない形で画像を他サイトで利用されたりは困るが, 画像自体が他の人に見られて困るものではない.

しかし, 本人確認書類の画像などは他の人に閲覧されては困る部類である.
だが, CSなど開発者以外の人が管理者画面などで閲覧したいため, ただただプライベートにすれば良いというものではない.

調べたときにいい感じの記事がなかったので残しておく.

解決策

僕の構成の場合は以下のような対応をした.

  • S3のアクセス権限を全て公開にし, 特定のディレクトリのみ特定のIAMユーザがアクセス可能
  • ユーザ画面では, Carrierwaveが作成してくれたURLをimgタグで指定して表示
  • 管理者画面では, Rails側でS3の画像をBase64に変換して, imgタグで指定して表示

S3の設定

とりあえず, Block public access は全てOFFにする.

そしてBucketポリシーを以下のように設定.

1つ目のStatementでは, Bucketの中身の全てを匿名ユーザでも閲覧できるように設定.

2つ目のStatementでは, 特定のディレクトリへのアクセスを特定のユーザ以外アクセスできないように設定.

【Bucket】  
  ┗ UUID(ユーザ識別子)  
    ┣ ユーザのプロフィール画像を保持するディレクトリ  
    ┣ ユーザの投稿した画像を保持するディレクトリ  
    ┣ ユーザの投稿した動画を保持するディレクトリ  
    ┗ private ※このprivateディレクトリ配下はpublicにしたくない  
      ┗ 本人確認書類の画像を保持するディレクトリ  
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowPublicRead",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::【Bucket名】/*"
        },
        {
            "Sid": "BlockReadPrivateDirectory",
            "Effect": "Deny",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::【Bucket名】/*/private/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:username": "【IAMユーザ名】"
                }
            }
        }
    ]
}

軽くハマった点

バケットポリシーは上から順番に評価され, 上書きされると思っていたため,

{
  "Sid": "AllowUserRead",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::【IAMユーザの数字】:user/【IAMユーザ名】"
  },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::【Bucket名】/*/private/*"
}

みたいなことをやっていた...
うまくいかなくて調べたら, 初めて評価優先度があることを知った...

以下の記事はとても参考になりました.
【AWS S3】S3 bucket policy を使ったアクセス制限方法 Effectの評価優先度を考える
S3バケットへのアクセスを特定IAMユーザにだけ許可して他は弾く

Rails

誰でも見ても良い画像に関しては, Carrierwaveが作成してくれたURLを利用する.

問題は匿名ユーザでは閲覧できないように設定した画像.
こちらは今まで通りURLを渡して表示とはいかない.

軽く調べても, ほとんどの記事がURLを利用して表示させているため, 他の表示手段を残しておこうと思う.

S3を扱うGemを指定

gem 'aws-sdk-s3'

S3への接続情報を記述

Aws.config.update(
  {
    credentials: Aws::Credentials.new(S3に接続できるIAMユーザのアクセスキーID,S3に接続できるIAMユーザのシークレットアクセスキー】
    ),
    region: 【接続したいS3があるリージョン】
  }
)

画像を取得する

※わかりやすく直接文字列で記載しているところがあるが, 実際はオブジェクトから取得したり, 環境変数から取得する部分があるので, 適時解釈してください.

# Carrierwaveから取得した, S3に配置している画像へのURL
url = "https://hoge.s3-ap-northeast-1.amazonaws.com/hogehoge.png"

# S3の画像の配置場所を取得
s3_object_key = url.delete("https://【バケット名】.s3-ap-northeast-1.amazonaws.com/")

# S3のオブジェクト取得
s3_object = Aws::S3::Client.new.get_object(bucket:S3のバケット名】, key: s3_object_key)

# Base64に変換
base64_image = Base64.encode64(s3_object.body.read)

上記で作成した画像情報をAPIで返したり, そのまま表示する場合は以下のようにすることで表示することができる.

<img src="data:image/png;base64,<%= base64_image %>" class="rounded float-left" width="200" height="200" />

他の言語だとこういった記事はよく見かけた記憶があるが, Railsだと見当たらなかったので記事にしてみた.

Twitterフォロー待ってます!