python3でaws s3のsignatureを作成する備忘録


やったこと

Django(rest framework)とjsのSPAフレームワークを用いたwebアプリにおいてs3に画像をアップロードしなければならない案件を受けるにあたり、signatureを作成することになりました。しかし、python3の記事が出てこなかった気がしたので備忘録として載せておきます。

python2はこちらにあるのですがなかなか手間取ったので、、、

こんなのドキュメント読めば分かるだろ!って多くの人は思うかもしれないですが、僕みたいなコピペファッションエンジニアはこういうのあると助かると思うので載せておきます。

仕組み

この辺はドキュメントとか資料とか多いのですが、一応
普通はhttpのAuthorization headerにシークレットキー、アクセスキーなどから暗号化関数を使って作成した署名文字列を付与して送信しますが、今回はブラウザからアップロードする案件だったのでJSにシークレットキーとか記載するわけにはいきません。
したがって、サーバーサイドで署名文字列を作成し、それをフロント側に返してそれを用います。

今回の流れとしては
アップロード先のパスをフロント側で指定してサーバーサイドに署名をリクエストする

サーバーサイドで暗号化関数を用いて署名文字列を作成し、返す

フロント側で署名文字列を受け取り、formDataオブジェクトで送信する。

s3の設定

独自サービスのフロントからアップロードということでクロスオリジンは設定しない行けません。
反映にも結構時間がかかるので一番最初にこれをやっておくことをオススメします。
セキュリティちゃんとしたいときはAllowedOriginにちゃんとしたドメインを入れてください。
backetを選択して右側のプロパティ→アクセス許可→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>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

実装

urls.pyにはアクセスしたいURLを記載しておいて下さい。

settings.py
S3_ACCESS_KEY = 'AAAAAAAAAAAAAAAA'
S3_SECRET_KEY = 'hogehugapiyo'
S3_BUCKET_NAME = "s3_bucket_name"

このようにsettingsに書いておくと良いでしょう。

view.py
from rest_framework import status
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from django.conf import settings
import base64, hmac, hashlib
from datetime import datetime, timedelta

class View(APIView):

    POLICY = ''\
        '{"expiration": "%s",'\
        '  "conditions": ['\
        '    {"acl": "public-read"},'\
        '    {"bucket": "%s"},'\
        '    {"key": "%s"},'\
        '    ["content-length-range", 0,MAX_LENGTH]'\  #ここは必要に応じて
        '  ]'\
        '}'\

    def get(self, request):
        s = (self.POLICY % ((datetime.now() + timedelta(minutes=3)).strftime("%Y-%m-%dT%H:%M:%S.000Z"), settings.s3_BUCKET_NAME, request.query_params['upload_path'])).replace("\n", "").encode('ascii')
        private_key = settings.S3_SECRET_KEY
        policy = base64.b64encode(s)
        signature = base64.b64encode(hmac.new(private_key.encode('ascii'), policy, hashlib.sha1).digest())
        access_key = settings.S3_ACCESS_KEY
        data = {
            "policy": policy.decode('ascii'),
            "signature": signature.decode('ascii'),
            "access_key": access_key
        }
        return Response(data, status=200)

今回はpathをリクエストから取ってくるということでrequest.query_params['upload_path']にしています。違うパラメータを使いたければご自由に。
また、一応3分でこの署名文字列は失効するように書いています、でももっと短くてもいいかもしれない。。。

チョットハマったところが
asciiにencodeした後にそのままjsonで返却しようとしたところ

typeerror the json object must be str not 'bytes'

で返せなかったのでdecodeして返しています。

あとはjsのformDataオブジェクトでこんな感じで作成

upload.js
何かライブラリーjqueryでもsuperagentでも.get(django_url, {upload_path:"path/path.jpg"}, function(success) {
  formdata = new FormData();
  formdata.append("acl", "public-read");
  formdata.append("key", path);
  formdata.append("AWSAccessKeyId", success.access_key);
  formdata.append("policy", success.policy);
  formdata.append("signature", success.signature);
  formdata.append("file", blob);
  library.post(s3_url ,formdata, function(){
    console.log("わーい\(^o^)/");
  })
});

jsは雑ですが意図は読み取ってもらえると思います。

何かご意見ございましたら、是非お寄せください。

参考
http://dev.classmethod.jp/cloud/aws/s3-cors-upload/
http://funasaki.hatenablog.com/entry/2013/11/06/163558