以下の流れで実装しました。
- PostObjectV4 のインスタンスを使い、署名付きPOSTに必要な情報を発行
- Vue.jsで、アプリケーションサーバを介さず、クライアント側から直接S3にファイルをアップロード
準備
AWS
S3でバケットを作成してください。
※IAMの設定は省略します
S3 CORSの設定
CORSを設定しておかなければなりません。
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
AWS-SDK-PHPのインストール
$ composer require aws/aws-sdk-php
実装
署名付きPOSTに必要なデータを発行するPHPの処理と、ファイルアップロードするVue.jsの処理を記載します。
実装した処理は以下の通りです。
- 署名付きPOSTに必要なデータ形式のjsonを返すAPI
- Vue.jsから↑のAPIを叩き、レスポンスを取得
- レスポンスを利用し、ファイルアップロード
API(PHP)
<?php
    // laravel apiのcontrollerの処理を一部抜粋
    public function getPresignedUrl(Request $request)
    {
        $s3Client = new \Aws\S3\S3Client([
            'credentials' => [
                'key' => env('AWS_ACCESS_KEY_ID'),
                'secret' => env('AWS_SECRET_ACCESS_KEY'),
            ],
            'region' => env('AWS_DEFAULT_REGION'),
            'version' => 'latest'
        ]);
        $bucket = env('AWS_BUCKET');
        $requestData = $request->all();
        $formInputs = [
            'acl' => 'public-read',
            'key' => 'hoge/' . $requestData['filename'] . '.' . $requestData['fileext'],
        ];
        $options = [
            ['acl' => 'public-read'],
            ['bucket' => $bucket],
            ['starts-with', '$key', 'hoge/'],
        ];
        $expires = '+20 minutes';
        $postObject = new \Aws\S3\PostObjectV4(
            $s3Client,
            $bucket,
            $formInputs,
            $options,
            $expires
        );
        $formAttributes = $postObject->getFormAttributes();
        $formInputs = $postObject->getFormInputs();
        return response()
            ->json([
                'url' => $formAttributes['action'],
                'fields' => $formInputs
            ]);
    }
Client側(Vue.js)
<script>
    export default {
        name: 'AwsS3Upload',
        methods: {
            async upload() { 
                const upload_files = document.getElementById('upload-file');
                const upload_file = upload_files.files[0];
                // 署名付きPOSTのAPI叩く
                let preSignedUrl = await this.getPresignedUrl();
                // S3へアップロード
                let uploadS3Path = await this.uploadS3(preSignedUrl, upload_file);
            },
            async getPresignedUrl() {
                // ↓ここのファイル名は仮置きで適当になってますw
                let filename = 'fuga';
                let filetype = 'image/jpeg'
                let fileext = 'jpg'
                try {
                    const url = '/api/get-presigned-url?filename=' +  filename + '&filetype=' + filetype + '&fileext=' + fileext;
                    let response = await axios.get(url);
                    console.log('S3署名付きURL取得 成功');
                    return response;
                } catch (error) {
                    console.log('S3 署名付きURL取得 失敗');
                }
            },
            async uploadS3(presignedUrl, up_file) {
                let data = presignedUrl.data;
                try {
                    var formdata = new FormData();
                    for (let key in data.fields) {
                        formdata.append(key, data.fields[key]);
                    }
                    formdata.append("file", up_file);
                    const headers = {
                        "content-type": "multipart/form-data",
                    }
                    console.log('S3 アップロード 開始');
                    let response = await axios.post(
                        data.url,
                        formdata,
                        {
                            headers: headers,
                        }
                    );
                    console.log('S3 アップロード 成功');
                    return data.url + '/' + data.fields.key;
                } catch (error) {
                    console.log('S3 アップロード エラー');
                }
            },
        }
    }
</script>
結論
署名付きPOSTで、S3へ直接クライアントサイドからファイルアップロードができました。
また、アプリケーションサーバを介さずにファイルをアップロードできるため、ファイルサイズが大きい場合においてもサーバに負荷をかけずに済みます。 (20191129追記)
参考
【AWS S3】S3 Presigned URLの仕組みを調べてみた
CORS(Cross-Origin Resource Sharing)について整理してみた
PresignedPost.php
ブラウザからS3へのダイレクトアップロード
