8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

認証されたユーザのみにS3に置かれたファイルを表示する方法

Posted at

はじめに

例えばRaspberry Piなりなんなりで作成した監視カメラでS3に撮影画像を送信するとします。
撮影された画像を見るWeb UIを作りたいわけですがもちろん「誰でも見れる」のは論外です。というわけで、オブジェクトURLを使うことはできません。
「認証を行ったうえでアクセスさせればいいんだよな、Cognitoを使えばできる?」とやってみたのが今回のネタになります。

S3の設定

まずはS3バケットを作成します。パブリックアクセスブロックはもちろんオンです。

01_s3-block-public-access.jpg

Web(JavaScript)からアクセスする際にはCORSの設定も必要です。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Cognitoの設定

Cognitoは使ったことがなかったので用語を理解するのに少し手間取りました。
なお各リソースの作り方の詳細なステップについては他の方が記事を挙げられているので省略します(手抜き)

  1. ユーザープールを作成する
  2. ユーザープールにアプリクライアントを作成する(「クライアントシークレットを生成」はオフにする1
  3. ユーザを作成する(許可したものしかアクセスさせないので、当然自分でサインアップするのは禁止です)
  4. ユーザープールとアプリクライアントを指定してIDプールを作成する
  5. IDプールと同時に作成されたIAMロールに権限を付加する(後述)

「認証されたロール」にS3バケットへのアクセス権を追加します。
というかこれでアクセスはできるものの書き方が正しいのかよくわからない。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::restricted-s3-junjis0203",
                "arn:aws:s3:::restricted-s3-junjis0203/*"
            ]
        }
    ]
}

JavaScriptクライアント

以上でリソースの設定はできたので「パブリックアクセス禁止」のS3にアクセスするJavaScriptクライアントを作成します。Cognitoの認証を行うにはAWS SDK本体に加えてAmazon Cognito Identity SDKが必要です。今はAmplifyというフレームワークの一部になっているようですが、見たところAmplifyが提供する機能でやりたいことはできなそうなのでSDKを素で使いました。

コードはこちらに置きました。もちろんID系は抜いているので試す場合は必要な情報を埋めてください。
https://github.com/junjis0203/restricted-s3-access-example

Cognitoの認証(パスワード変更付き)

サンプルの基本的な動きは以下のようになります。

index.htmlにアクセスされるとuserPoolから「current user」を取得します。
current userはlocalStorageに保存されていますが初回はもちろんnullです。認証を行うためにsignin.htmlに飛ばします。

index.html抜粋
            const cognitoUser = userPool.getCurrentUser();
            if (!cognitoUser) {
                document.location.href = 'signin.html'
            }

手動でCognitoのユーザを作る場合、初回に必ずパスワード変更が必要になります。JavaScript的にはauthenticateUserメソッドを呼び出すとnewPasswordRequiredコールバックが呼び出されるという流れになります。この場合の対処方法はSDKドキュメントのUse case 23(アンカーになってないので検索してください)に書かれています。

signin.html抜粋
                cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
                cognitoUser.authenticateUser(authenticationDetails, {
                    onSuccess: function(result) {
                        document.location.href = 'index.html'
                    },

                    onFailure: function(err) {
                        console.log(err)
                    },

                    newPasswordRequired: function(userAttributes, requiredAttributes) {
                        delete userAttributes.email_verified
                        sessionUserAttributes = userAttributes

                        changePasswordForm.style = 'display: block;'
                    }
                });

ともかく以下のような適当フォームで新しいパスワードを入力させてパスワード変更を行います。ドキュメントでは書かれていませんがcompleteNewPasswordChallengeメソッドでもonSuccessonFailureのコールバックを受け付けます(書かないとエラーになったような気もする)

02_change_password.jpg

signin.html
                cognitoUser.completeNewPasswordChallenge(newPassword, sessionUserAttributes, {
                    onSuccess: function(result) {
                        document.location.href = 'index.html'
                    },

                    onFailure: function(err) {
                        console.log(err)
                    }
                })

S3へのアクセス

さてこれでともかく認証が行えました。
がこの後どのようにしてS3にアクセスすればいいのかがよくわかりませんでした。

初めに考えたのはLambdaで「(API Gatewayに)アクセスしたユーザの権限でS3からファイルを取得し流せばよい?」というものだったのですが、API Gatewayでアクセスユーザの設定をする方法(オーソライザー?)がよくわからずに試行錯誤しているうちに、
「あれ?よく思うとAmazon Cognito Identity SDKって証明書の更新してるし普通にアクセスできるんじゃね?」と思いやってみました。

index.html抜粋
                s3.listObjects({Bucket: S3_BUCKET}, function(err, data) {
                    if (err) {
                        console.error(err)
                        return
                    }

                    const ul = document.createElement('ul')
                    data.Contents.forEach(function(content) {
                        if (content.Key.endsWith('.jpg')) {
                            const li = document.createElement('li')
                            const a = document.createElement('a')
                            const br = document.createElement('br')
                            const img = document.createElement('img')
                            a.href = '#'
                            a.innerHTML = content.Key
                            a.addEventListener('click', function(e) {
                                // 後述
                            })
                            li.appendChild(a)
                            li.appendChild(br)
                            li.appendChild(img)
                            ul.appendChild(li)
                        }
                    })
                    const app = document.getElementById('app')
                    app.appendChild(ul)
                })

普通にアクセスできました。
なお本当は日付→各日のファイル一覧のように階層化したかったのですが検証コードなのでそこら辺は手抜きです。

03_list_objects.jpg

リンクがクリックされたらファイルを取得してimgに突っ込みます。

index.html抜粋
                            a.addEventListener('click', function(e) {
                                e.preventDefault()

                                s3.getObject({Bucket: S3_BUCKET, Key: content.Key}, function(err, data) {
                                    if (err) {
                                        console.error(err)
                                        return
                                    }

                                    console.log(data)
                                    const blob = new Blob([data.Body], {type: data.ContentType})
                                    img.src = URL.createObjectURL(blob)
                                })
                            })

画像を確認したところネコチャーンが集まっていました。これは緊急事態ですね。
※前に散歩してたときに撮った画像です。

04_get-object.jpg

証明書等の期限切れが気になるところですがそのうち考えます。

おわりに

以上今回はパブリックアクセスはブロックしつつ認証されたユーザにはS3内のファイルアクセスを許可するという方法について説明しました。
普通に需要がありそうなのでもう少し簡単な方法があるのかもしれません。少なくとも軽く探してみた限りでは見当たらなかったですが。

参考

ユーザープールの作り方などJavaScriptでCognitoを使う方法全般
https://qiita.com/tmiki/items/f36edc6c7473f5baa398

CORS設定などを参照しました。
https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/s3-example-photos-view.html

Ajaxで画像ファイルを取得してimgに設定する方法。getObjectで返されるのはUInt8ArrayのためBlobにする必要があります。
https://qiita.com/Yarimizu14/items/f56123c738f12ad1844a
http://var.blog.jp/archives/62330155.html

  1. 後述のクライアントライブラリで指定する方法がないからなのかなんなのか理由は不明ですがともかくシークレットありだと駄目でした。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?