ユーザーがアップロードした画像をAWS S3に保存して、ユーザー本人だけアクセスできるようにする


そのようなニーズ、よくありますよね。
やっていきましょう。

あ、ちなみに別に画像じゃなくても、他のファイル(PDFとかtxtとか)でももちろん行けます。

:::note warn
本記事で出てくるS3 URLなどは、執筆のために一時的に作っただけなので実際にはアクセスできません。
:::

# S3バケットを作る

とりあえず動きを再現するだけなら、デフォルトで大体OKです。
気を付けるところは↓です。

![s3_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/f642646e-efa8-eebb-423d-f23b70370de2.png)

# CloudFrontを作る

## その前に、公開鍵と秘密鍵ファイルを作る

これはopensslコマンドが存在する環境なら何で実施してもよいです。
Mac, Linux, Windows内のWSL上Linuxなど、どこでも。

```bash
$ openssl genrsa -out private_key.pem
$ openssl rsa -pubout -in private_key.pem -out public_key.pem
```

## 公開鍵をクリップボードにコピーしておく

public_key.pemの中身をテキストで表示すると、こんな感じの文字列だと思うので。

```bash
$ cat public_key.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA58Mo/UyzKZ59RtVMFtXr
N0W3ysaHkTUWdgrDDbKLbryXB9Dq7YgiveBOu3zXndhHiKtdAN8e/szz1zuOCo6y
uO5BpeDMohdpESlS803jim8jXIh7vZX0amyzBsQSO4jnHtBH8lFjSr0CmcyETyJr
4xu3Xa5iDyvQRvcO1i4I0VfFRzd/Hy9XXCvUFC4uKUNAH1StksklF2E1X0cbNnx5
zH8XudWFrt1vWGHFJquyDvGp7EW/JzghL8oE54WNawhddRISTnchiPxaxxCrJaOj
HzIEQ6S4stzzYHTA+ArGRzPfcsbZP92bALSrO3GTjt3uzhZnx0VWQ98B1ZxQixXF
ZQIDAQAB
-----END PUBLIC KEY-----
```

↓この部分をクリップボードコピーしておきます。

```bash
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA58Mo/UyzKZ59RtVMFtXr
N0W3ysaHkTUWdgrDDbKLbryXB9Dq7YgiveBOu3zXndhHiKtdAN8e/szz1zuOCo6y
uO5BpeDMohdpESlS803jim8jXIh7vZX0amyzBsQSO4jnHtBH8lFjSr0CmcyETyJr
4xu3Xa5iDyvQRvcO1i4I0VfFRzd/Hy9XXCvUFC4uKUNAH1StksklF2E1X0cbNnx5
zH8XudWFrt1vWGHFJquyDvGp7EW/JzghL8oE54WNawhddRISTnchiPxaxxCrJaOj
HzIEQ6S4stzzYHTA+ArGRzPfcsbZP92bALSrO3GTjt3uzhZnx0VWQ98B1ZxQixXF
ZQIDAQAB
-----END PUBLIC KEY-----
```

## CloudFrontのパブリックキーを作成しておく

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/88c56ffd-7d5b-b092-26b6-25490ab74beb.png)

「キー」には、先ほどコピーした値をコピペします。

![cloudfront_pubkey.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/33b9e6d4-29dc-e99f-1247-0f4a3296fe9d.png)

## CloudFrontのキーグループを作成しておく

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/bd89fe08-a57e-2e89-ee36-8e41c0e8e759.png)

「パブリックキー」に、つい先ほど作成したものを指定します。

![cloudfront_keygroup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/576a187a-e3ac-8228-9414-9421d0e8c41e.png)

## CloudFrontディストリビューションを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/54bed3ab-9242-77b1-046b-1829fb5598ee.png)

基本とりあえずデフォルトで、要点は以下です。

### ビューワーのアクセスを制限する

![cf4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/e38698de-8e65-5415-73ee-42368248550f.png)

### OAIを作って選択する

![cf1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/b26f7f00-6277-c537-b134-6401e7eff62d.png)

![cf2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/15f269b4-eae2-09f2-44a9-52c30e7fdc09.png)

![cf3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/3342e2cb-af2a-f899-5d6d-a7f6b2130606.png)

:::note warn
OAIが出来上がるのに少々タイムラグがあるらしく、数分待たないと選択肢に出現しませんでした。焦らないでね。
:::


### キャッシュキーとオリジンリクエスト

”CachingOptimized” にすると、画像をアップロードし直した時に即座に反映されません。
アップロードファイルパスが同じまま、ユーザーが頻繁に画像をアップロードし直すケースがある時は注意です。

![cf5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/756c6878-eb5c-8e2a-b7b2-ac4625d7519e.png)

### その他、とりあえず

![cf6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/5925e078-e0b1-0a00-dbbb-bba3c627175c.png)

これで一旦CloudFront作成。

# サンプル画像をアップロード

![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/94832a53-77e3-cf82-65ac-e22988432eab.png)

フォルダを切ることももちろん可能ですが、今回は面倒なのでルートにアップロードします。

![upload.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/18610a20-de66-3628-2ce3-081fbf3af8f9.png)

ちなみにアップロードは、後述の各種言語用ライブラリでできますが、今はとりあえずということで、AWS S3コンソールからポチっとアップロードしちゃいます。

# サンプル画像にダイレクトアクセスしてみる

本例なら、
https://yagrush.s3.ap-northeast-1.amazonaws.com/sample.png

## 結果

![access_denied.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/4191c6f5-f3af-c2a2-1780-0956771c079e.png)

単なるURLを他人に知られてもアクセスできないですね。
とりあえずここまではOK。

# ここで一旦、CloudFrontのデプロイが完了しているか確認します

日付が入って完了していればOK。

![deployed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/fa00f4bc-b266-7ecd-4e24-4638376bda76.png)

# S3のバケットポリシーを編集する

ちょっとS3に戻って「バケットポリシー」を編集します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/af6a7d5a-3cf0-13a1-3d11-9e7ed6236ab4.png)

Identityの値は、CloudFrontの「オリジンアクセスアイデンティティ」の「ID」値を取ってきて埋めてください。

```xml
{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
        	"Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity XXXXXXXXXXXXX"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::yagrush/*"
        }
    ]
}
```

![buchet_policy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/e879cbce-11ea-37db-ee18-977e95cd1b8c.png)

# 何らかのプログラミング言語で、セキュアアクセスURLを発行する

今回はPythonで書いていますが、色々な言語向けにライブラリが公開されています。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/PrivateCFSignatureCodeAndExamples.html

Pythonには boto というライブラリ群があるので、それを使います。

https://github.com/boto

https://aws.amazon.com/jp/sdk-for-python/

```python
from datetime import datetime, timedelta

from botocore.signers import CloudFrontSigner
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


def rsa_signer(message):
    with open("./private_key.pem", "rb") as pk_file:
        private_key = serialization.load_pem_private_key(pk_file.read(), password=None, backend=default_backend())
    return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())


if __name__ == "__main__":
    # セキュアアクセスURLの期限は30分とする
    expire_date = datetime.utcnow() + timedelta(minutes=30)

    cloudfront_signer = CloudFrontSigner("パブリックキーID", rsa_signer)
    signed_url = cloudfront_signer.generate_presigned_url(
        # ベースとなるURLは、ユーザーIDで動的に生成するか、DBに保存しておくか、など。
        "https://d1nlzlzgqyj95p.cloudfront.net/sample.png",
        date_less_than=expire_date,
    )
    print(signed_url)
```

> https://d1nlzlzgqyj95p.cloudfront.net/sample.png?Expires=1648564338&Signature=BPGE4FutRnkgtfh9BHXsVDOaysE6kqKwQl4rODxUZxr8ukJFgaSfRRrdV8zYKShQbbiPwzYiwvuR0DEhVtFYbzUwL6yVkAjEBHm9aF2~SEOmgsL4aMzLYYjRaMNMG1EuD63~X0JK44mg8XLV-LXWoBj0HJAEegcBnjhbQeZD1KVIjR7oLLS1z-XEBwTvPOCVoTZiJnq7lLPNlcwpFFZhZpmGoqL94D1fy052Ouk0tL8sU8Zhh5-aIl6-ka7NQw9R7NcLnRfzFRiEsKDhqboFHrjToHIVLwv04K2kEucXaqbd30y8yWdt5ozF6Jt5v9dkzZDhdCn1B-Ohn5MiyfnZLg__&Key-Pair-Id=K22C7RPRRNQZNS

ちなみに `パブリックキーID` は、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/0cfcb389-100e-c31d-ab05-80199b63a196.png)

です。

print()されたURLをブラウザで試しに叩いてみましょう。
（ただし本例では、30分以内に、ね。）

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553715/8a30f567-49d6-7cef-8888-e4906e2b9e2c.png)

https://yagrush.s3.ap-northeast-1.amazonaws.com/sample.png じゃDeniedだったのに、画像が表示できました！

このような、署名URL
> https://d1nlzlzgqyj95p.cloudfront.net/sample.png?Expires=1648564338&Signature=BPGE4FutRnkgtfh9BHXsVDOaysE6kqKwQl4rODxUZxr8ukJFgaSfRRrdV8zYKShQbbiPwzYiwvuR0DEhVtFYbzUwL6yVkAjEBHm9aF2~SEOmgsL4aMzLYYjRaMNMG1EuD63~X0JK44mg8XLV-LXWoBj0HJAEegcBnjhbQeZD1KVIjR7oLLS1z-XEBwTvPOCVoTZiJnq7lLPNlcwpFFZhZpmGoqL94D1fy052Ouk0tL8sU8Zhh5-aIl6-ka7NQw9R7NcLnRfzFRiEsKDhqboFHrjToHIVLwv04K2kEucXaqbd30y8yWdt5ozF6Jt5v9dkzZDhdCn1B-Ohn5MiyfnZLg__&Key-Pair-Id=K22C7RPRRNQZNS

がネットワーク経路上の例外的な仕掛けで傍受されない限りは大分安全ですし、傍受されたとしても、expire_date が短く設定されていれば、悪人がアクセスするころにはURLは期限切れ、という寸法ですね。
ホホホウ。