AWS SDK for PHP を使って Laravel で Amazon S3 の署名付き URL を取得する
はじめに
PHP の SDK を使って Laravel で S3 の署名付き URL を取得する処理を実装しました。今回は、その過程で学んだことやその実装方法についてまとめます。
署名付き URL とは
Amazon S3 では、バケットあるいはそのバケットのオブジェクトに対して、アクセスポリシーを定義したり IAM 単位で制御したりしてアクセスを制御できます。
署名付き URL とは、一言で言うと 有効期限内に一時的にアクセスできる URL を発行する機能です。署名付き URL を発行するには、該当の S3 バケットあるいはオブジェクトへの権限を持つユーザあるいは IAM ロールをアタッチされているリソースが任意の有効期限を指定して作成する必要があります。
方法としては、REST API, AWS CLI, AWS SDK の 3 パターンで実現できます。余談ですが、AWS のほとんどのリソースはこのように複数のインターフェースより抵抗なく操作できる点が魅力だと感じています。
今回は、3 つ目の AWS SDK for PHP を使って Laravel で構築したアプリケーション上に REST API として実装することにしました。
実装
ルーティング
まず、API のエンドポイントをルーティングに定義します。RESTful API の思想で構築しているため、このエンドポイントの命名にすごく悩みました。何かいいネーミングがあれば、是非教えていただけると嬉しいです。
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
+ Route::get('s3/presignedurl', 'S3ClientController@getPresignedUrl')->name('s3.getPresignedUrl');
コントローラ
次に、ルーティングに定義したコントローラにアクションを作成していきます。最初に全体を。
use App\Http\Requests\GetPresignedUrlRequest;
use Aws\S3\S3Client;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
/**
* @param \App\Http\Requests\GetPresignedUrlRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function getPresignedUrl(GetPresignedUrlRequest $request): JsonResponse
{
$s3Client = new S3Client([
'region' => config('filesystems.disks.s3.region'),
'version' => config('filesystems.disks.s3.aws_sdk_version'),
'endpoint' => config('filesystems.disks.s3.client_url')
]);
$filename = $this->makeUniqueFilename(
pathinfo($request->filename, PATHINFO_EXTENSION)
);
$cmd = $s3Client->getCommand($request->method), [
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => $filename
]);
$presignedRequest = $s3Client->createPresignedRequest(
$cmd,
config('filesystems.disks.s3.aws_sdk.pre_signed_url.expired_time')
);
return response()->json([
'filename' => $filename,
'pre_signed_url' => (string) $presignedRequest->getUri()
]);
}
/**
* @param string $extension
* @return string
*/
public function makeUniqueFilename(string $extenstion): string
{
return (string) Str::uuid() . '.' . $extension;
}
順序立てて説明します。
getPresignedUrl
と言うメソッドでは GetPresignedUrlRequest
というフォームリクエストを引数として受け取ります。そうすることでコントローラ内にバリデーションを実装することなく処理を簡潔に書くことができます。詳細については次で触れます。
フォームリクエスト
フォームリクエストとは、HTTP リクエストをバリデーションする便利な機能です。下記の記事にわかりやすく解説してあったので是非参考にしてみてください。
下記では、 filename
, method
というパラメータに対してのルールを指定しました。
public function rules()
{
return [
'filename' => 'bail|string|required', // 文字列、必須ルール
'method' => 'string|required|in:get,put' // 'get' or 'put' の文字列、必須ルール
];
}
config
次に、 config()
と何箇所か書いているところについて軽く触れておきます。
Laravel では、コントローラなどの処理に直接秘密情報である .env の情報を書くことは御法度とされています。だったらどうするかと言うと config/*.php
内で .env の値を取得し、コントローラでは config から読み取るという手法を取っています。
理由については、下記らしいです。癖でいつもこのように書いていて深くは気にすることはなかったので大変勉強になりました。
.env ファイルの読み込みは, php artisan config:cache していない場合にしか行われません! キャッシュを有効にしてある場合, .env に書いてあるだけでシェルから起動する時点で定義されていない環境変数はすべて未定義になってしまうので注意しましょう。
続いて S3 の環境変数を取得できるよう config と .env に定義します。
return [
'disks' => [
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-west-2'),
'bucket' => env('AWS_BUCKET', 'test-bucket'),
'aws_sdk' => [
'version' => 'latest',
'pre_signed_url' => [
'expired_time' => '+5 minutes' // 署名付き URL の有効期限のデフォルト値
]
]
]
]
];
AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=YYYYYYYYYY
AWS_DEFAULT_REGION=us-west-2
AWS_BUCKET=test-bucket
最後に、makeUniqueFilename
メソッドについてです。
use Illuminate\Support\Str;
/**
* @param string $extension
* @return string
*/
public function makeUniqueFilename(string $extenstion): string
{
return (string) Str::uuid() . '.' . $extension;
}
これはおまけみたいなものですが、uuid にファイル名を整形する処理を挟んでいます。理由としては S3 バケット内のオブジェクトを一意に識別できるため、このようにしました。
確認
実装したエンドポイントに対してリクエストを送って確認してみます。次のようなレスポンスが返って来たら OK です。
アップロードしたい場合の例
$ curl "http://localhost:3000/s3/presignedurl?filename=sample.jpeg&method=put" | jq .
{
"filename": "cb12afe2-4e70-1234-5678-c248f4d1213b.jpeg",
"pre_signed_url": "https://test-bucket.s3.us-west-2.amazonaws.com/cb12afe2-4e70-1234-5678-c248f4d1213b.jpeg?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA1AB234ABCDE5FGHIJ%2F20200719%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20200722T103113Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Signature=123456789abc10ef203a46c75ff0e89c5678fd5f69a12345ee6d608123456cbd"
}
ここまで確認できれば、filename
のファイル名で pre_signed_url
のエンドポイントに対してリクエストを送るとアップロードできます。これで非公開に指定あるバケットに対して、認証情報を持たないフロントエンドからでも直接アップロードができるようになります。
まとめ
Laravel で S3 の署名付き URL を取得する API を実装してみました。
これまでは、サーバサイド側でアップロードの処理を実装していましが、署名付き URL を取得することでフロントエンドからでもアップロードが可能になりました。より実装の自由度が高くなったことで、アーキテクチャを疎結合化したり、 UX を向上を目指していきたいです。
参考にさせていただいた記事
Discussion