はじめに
例えばRaspberry Piなりなんなりで作成した監視カメラでS3に撮影画像を送信するとします。
撮影された画像を見るWeb UIを作りたいわけですがもちろん「誰でも見れる」のは論外です。というわけで、オブジェクトURLを使うことはできません。
「認証を行ったうえでアクセスさせればいいんだよな、Cognitoを使えばできる?」とやってみたのが今回のネタになります。
S3の設定
まずはS3バケットを作成します。パブリックアクセスブロックはもちろんオンです。
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)
- ユーザを作成する(許可したものしかアクセスさせないので、当然自分でサインアップするのは禁止です)
- ユーザープールとアプリクライアントを指定してIDプールを作成する
- 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
に飛ばします。
const cognitoUser = userPool.getCurrentUser();
if (!cognitoUser) {
document.location.href = 'signin.html'
}
手動でCognitoのユーザを作る場合、初回に必ずパスワード変更が必要になります。JavaScript的にはauthenticateUser
メソッドを呼び出すとnewPasswordRequired
コールバックが呼び出されるという流れになります。この場合の対処方法はSDKドキュメントのUse case 23(アンカーになってないので検索してください)に書かれています。
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
メソッドでもonSuccess
とonFailure
のコールバックを受け付けます(書かないとエラーになったような気もする)
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って証明書の更新してるし普通にアクセスできるんじゃね?」と思いやってみました。
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)
})
普通にアクセスできました。
なお本当は日付→各日のファイル一覧のように階層化したかったのですが検証コードなのでそこら辺は手抜きです。
リンクがクリックされたらファイルを取得してimg
に突っ込みます。
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)
})
})
画像を確認したところネコチャーンが集まっていました。これは緊急事態ですね。
※前に散歩してたときに撮った画像です。
証明書等の期限切れが気になるところですがそのうち考えます。
おわりに
以上今回はパブリックアクセスはブロックしつつ認証されたユーザには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
-
後述のクライアントライブラリで指定する方法がないからなのかなんなのか理由は不明ですがともかくシークレットありだと駄目でした。 ↩