【Laravel】CloudFront+S3で署名付き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ユーザからのアクションを許可する設定を書いておいてください。
[ポリシージェネレーター](https://awspolicygen.s3.amazonaws.com/policygen.html)を使うか、以下の例を書き換えて使用するのがオススメです。
本番環境が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"
        ]
      }
    }
  ]
}
```

[公式ドキュメント](https://readouble.com/laravel/6.x/ja/filesystem.html)に則り、Flysystemを使ってS3に画像をアップロードするところまで進めます。

```shell

composer require league/flysystem-aws-s3-v3
```

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

```php: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とコントローラ、ルーティングを作成します。

```php:routes/web.php
Route::get('/', function () {
    return view('index');
});
Route::Post('/', 'TestController@create');
```

```php: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>
```

```php: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:clear`や`php 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を更新します。

```shell

composer require aws/aws-sdk-php-laravel
```

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

```php: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にアップロードできます。
個人的にはこちらのほうが直感的で好きです。

```php

$path = $request->file('media')->store('/', 's3');
```