Help us understand the problem. What is going on with this article?

【Laravel】CloudFront+S3で署名付きURLを使いプライベートコンテンツを配信する

※3/22(日) バケットポリシー、AWS_URL、オブジェクトの公開等に関する記述が抜けていたため、修正しました。

環境

  • PHP 7.4.1
  • Composer 1.9.3
  • Laravel Framework 6.14.0
  • league/flysystem-aws-s3-v3 1.0.24
  • aws/aws-sdk-php 3.133.40
  • aws/aws-sdk-php-laravel 3.5.0

手順

長いのでだいぶ雑です。

1. S3アップロード実装

まず、AWSアカウントの作成, S3バケットの作成, IAMポリシーの作成, IAMユーザの作成/IAMポリシー適用などが必要になります。
これらは、ググれば良い資料が沢山出てくるのでそちらを参照してください。
S3のバケット作成後、バケットポリシーには、IAMユーザからのアクションを許可する設定を書いておいてください。
ポリシージェネレーターを使うか、以下の例を書き換えて使用するのがオススメです。
本番環境がEC2の場合は、セキュリティリスクの観点から、IAMユーザではなくIAMロールの利用を検討してみてください。

バケットポリシー例(全アクションを許可)

{
  "Id": "Policy1584851110472",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1581137583020",
      "Action": "s3:*",
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::example-bucket",
      "Principal": {
        "AWS": [
          "arn:aws:iam::000000000000:user/hoge_user"
        ]
      }
    }
  ]
}

公式ドキュメントに則り、Flysystemを使ってS3に画像をアップロードするところまで進めます。

composer require league/flysystem-aws-s3-v3

config/filesystems.phpには既にS3用の記述が存在するため、編集する必要はありません。
.envファイルは自身の環境に合わせて書き換えましょう。

config/filesystems.php(編集不要)
        's3' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
            'url' => env('AWS_URL'),
        ],
/.env
AWS_ACCESS_KEY_ID=hoge
AWS_SECRET_ACCESS_KEY=fuga
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=hoga
AWS_URL=https://example-bucket.s3-ap-northeast-1.amazonaws.com

画像アップロード用のHTMLとコントローラ、ルーティングを作成します。

routes/web.php
Route::get('/', function () {
    return view('index');
});
Route::Post('/', 'TestController@create');
resources/views/index.blade.php
<form method="POST" action="/" enctype="multipart/form-data">
    {{ csrf_field() }}
    <input type="file" name="media" accept="image/*">
    <input type="submit">
</form>
app/Http/Controllers/TestController.php
use Illuminate\Http\Request;
use Storage;

    public function create(Request $request) {
        // S3アップロード
        $media = $request->file('media');
        $path = Storage::disk('s3')->putFile('/', $media);
        $url = Storage::disk('s3')->url($path);
        dd($url);
    }

アップロードがうまくいかない場合、IAMポリシーやアクセスキー、バケットポリシーのチェックを行い、php artisan cache:clearphp artisan config:clearを叩いてみましょう。

この状態では、URLを叩くとエラーになるはずです。
画像をブラウザで確認する場合、S3バケットのパブリックアクセスをオンにした上でオブジェクトを公開状態にする必要があります。
S3バケットの「パブリックアクセスをすべてブロック」をオフにして、putFile()の第三引数に'public'を追記してアップロードすると、見れるようになるはずです。
今回のゴールは、オブジェクトを公開せずに見れるようにすることです。確認が終わったらパブリックアクセスをオフに戻しておきましょう。

2. CloudFrontとの連携

ルートユーザでAWSマネジメントコンソールにアクセスし、「マイセキュリティ資格情報」からCloudFrontキーペアを生成します。
キーペアをダウンロードし、秘密鍵をアプリケーションディレクトリ直下に保存します。

/pk-hogefuga.pem
-----BEGIN RSA PRIVATE KEY-----
aaaaaaaaaaaaa
bbbbbbbbbbbbb
ccccccccccccc
...
-----END RSA PRIVATE KEY-----

次に、CloudFrontのページで「Create Distribution」→Webの「Get Started」と進み、以下の情報を入力して設定を完了します。StatusがDeployedに変わるまで待ちましょう。
この操作が完了すると、S3のバケットポリシーが更新され、CloudFront経由でしかアクセスできないようになります。

Origin Domain Name: S3のバケットを選択
Restrict Bucket Access: Yes
Origin Access Identity: Create a New Identity
Grant Read Permissions on Bucket: Yes, Update Bucket Policy
Restrict Viewer Access(Use Signed URLs or Signed Cookies): Yes
Trusted Signers: Self

次に、以下のコマンドでAWS SDKを導入します。
コマンド入力後は、 https://github.com/aws/aws-sdk-php-laravel に記載のある通り、providersとaliasesを更新します。

composer require aws/aws-sdk-php-laravel

ここまでできたら、後は署名付きURLを取得するだけです。
コントローラと.envファイルを以下のように書き換えます。

app/Http/Controllers/TestController.php
use Illuminate\Http\Request;
use Storage;
use AWS;

    public function create(Request $request) {
        // S3アップロード
        $media = $request->file('media');
        $path = Storage::disk('s3')->putFile('/', $media);
        $url = Storage::disk('s3')->url($path);

        // 署名付きURLを取得
        $client = AWS::createClient('cloudfront');
        $signedUrl = $client->getSignedUrl([
            'url' => $url,
            'expires' => time() + 30, // 30秒間有効
            'private_key' => base_path('pk-hogefuga.pem'),
            'key_pair_id' => env('AWS_CLOUDFRONT_KEY_PAIR_ID')
        ]);
        dd($signedUrl);
    }
/.env
AWS_URL=https://xxx.cloudfront.net # CloudFrontのページにある「Domain Name」
AWS_CLOUDFRONT_KEY_PAIR_ID=hogefuga # 鍵ファイル名のハイフン以降の文字列

実行後、以下のようなURLが取得できたらうまくいっています。

https://xxx.cloudfront.net/jA7g461GuNZDqaBwI0VKAm8Ia2KjXp6E7DMTQVe8.png?Expires=1584705653&Signature=C97kAeYc3Lvr-2mJ1hKe9SSAVQJJuS0wuCmyecIXJMIES~ITw10gJkn0UE1xuMUfpwAW8Oka~dIP61FdPfshrfU4pYzHYkeqMqZzvuZjen3NwNiSqHF1h5raLuVpG9yrhnuEPJAgaMdVsKcQFG0Ej0qHMtSi9zehrPvUh-TMmeAUHhU6dBXsPHEvlPQoPzJC-sVMh2ZVgTbVgAgaTl6CK5Am3WaVsNfSjhD1jBasdC5qTLPuEFOnuVXcMBFyh2svQLtX92DDUnfAa0jx0SUQ3DAgGNuj1I-NjV4J6w30vqvlbdGBIOHTCcBveeHxohAcp4sDxwznul2bZ2IY3Rrx6Q__&Key-Pair-Id=DFNVRFXGY12IDEBGZGPL

CloudFront経由でしかアクセスできない(S3に直接アクセスが禁止されている)こと、GETパラメータ(署名)を外すとアクセスできないこと、有効期限を過ぎるとアクセスできないことを確認してみてください。

余談

以下のコードでもS3にアップロードできます。
個人的にはこちらのほうが直感的で好きです。

$path = $request->file('media')->store('/', 's3');
_hiro_dev
ツイ消しツール「ツイ消し職人 ( https://twikeshi.net )」の開発者 / 25歳フリーランス / 応用情報技術者 / CCNA / Laravel / Vue.js / SQL / 声優オタク / 温泉オタク / サウナー / 技術で世の中をもっと便利にする
https://hir0.dev/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした