S3 + Lambda + Cognitoを使って、簡単お問い合わせシステム構築

  • 359
    Like
  • 3
    Comment

S3とLambdaとCognitoを使って、簡単でセキュアなお問い合わせフォームシステムを作ってみます。

記事執筆当時、Cognitoがバージニア/アイルランドのみ利用可能だったため、今回はS3/Lambda/Congito全部バージニア(us-east-1)に揃えています(S3とLambdaはリージョン揃える必要があります)。2015年9月以降、東京リージョンでもCognitoは利用可能となっています。

全体像

S3上にホスティングしているお問い合わせフォームから投稿すると、S3上にJSON形式で内容がuploadされます。ファイルがuploadされると、Lambdaがイベントフックして内容をGmailに送信する構造です。
お問い合わせ数の少ないサイトでは、これで十分でしょう。

SQLインジェクションとは無縁ですし、負荷も気にしなくて良いので、選択肢としてはありかなと思います。

0 準備

0-1 : S3バケットの作成

ホスティング用のS3バケットを作成します。仮にバケット名をxxx.example.comとし、Static Web HostingをONにします。

1 Cognito

フォーム投稿ボタンを押した時に、データをS3に時限でupload可能なIAM Roleを発行してもらうため、AWS CognitoのSecurity Token Serviceを使います。

1-1: Cognitoで identity pool作成

CognitoトップからCreate identity poolをします。

Identity Pool Name: 適当なプール名を入れてください
Enable Access to Unauthenticated Identities: 認証なしで、時限IAM Roleを発行します。Amazon/Facebook/Twitterなどのアカウントを要求することも可能です。

cognito0

cognito1

identity poolが作成されたら、identity pool idをメモします。

cognitox

1-2: 自動作成したIAM RoleにS3 upload権限を追加

次に自動作成したIAM Roleにs3:PutObject / s3:PutObjectAcl権限を追加します。

cognito2

cognito3

1-3: S3バケットにCORS許可

JavaScriptでS3にアクセスするため、S3にCORS(Cross-Origin Resource Sharing)を許可します。

S3バケット(xxx.example.com) => Permissions => Edit CORS Configuration でCORSを設定します。

  • Allowedmethod: PUT(upload)のみ許可します
  • AllowedOrigin: お問い合わせフォームのあるドメインを指定(*にしてしまうと、どこからでもcognitoで発行したSecurity TokenでS3 putができるようになります。S3 put攻撃されたくないので、ここは制限します)
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>xxx.example.com.s3-website-us-east-1.amazonaws.com</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

以上で、お問い合わせフォーム(xxx.example.com)からのみ、時限でs3にファイルをputできるIAM Roleが発行可能になりました。

2 お問い合わせフォーム本体

次にお問い合わせフォーム本体をs3にuploadしましょう。
今回は、「問い合わせタイトル」「返信メールアドレス」「本文」の3項目を投稿させます。
重要なのは、IdentityPoolIdの欄に、先ほどメモったIDをコピペすることです。

aws-sdk.min.jsと以下のindex.htmlをS3バケットにuploadします。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="import.css">
    <script src="aws-sdk.min.js"></script>
    <title>投書画面</title>
    <script>
        var $id = function(id) { return document.getElementById(id); };
        AWS.config.region = "us-east-1";
        AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "Cognitoで作成したIdentityPoolId"});
        AWS.config.credentials.get(function(err) {
            if (!err) {
                console.log("Cognito Identify Id: " + AWS.config.credentials.identityId);
            }
        });

        function uploadFile() {
            AWS.config.region = 'us-east-1';
            var s3BucketName = "xxx.example.com";
            var now = new Date();
            var obj = {"title":$id("title").value, "mail":$id("mail").value ,"contents":$id("contents").value, "date": now.toLocaleString()};
            var s3 = new AWS.S3({params: {Bucket: s3BucketName}});
            var blob = new Blob([JSON.stringify(obj, null, 2)], {type:'text/plain'});
            s3.putObject({Key: "uploads/" +now.getTime()+".txt", ContentType: "text/plain", Body: blob, ACL: "public-read"},
            function(err, data){
                if(data !== null){
                    alert("お問い合わせ完了致しました");
                }
                else{
                    alert("Upload Failed" + err.message);
                }
            });
        }
    </script>
</head>
<body>
    <div class="wrapper">
        <div id="postform">
            <form>
                <table>
                    <tr>
                        <th>件名</th>
                        <td>
                            <input id="title" type="text" name="title" class="titletext fontchange" maxlength="40" value="" />
                        </td>
                    </tr>
                    <tr>
                        <th>メールアドレス</th>
                        <td>
                            <input id="mail" type="email" name="mail" size="30" maxlength="50" />
                        </td>
                    </tr>
                    <tr>
                        <th>お問い合わせ内容</th>
                        <td>
                            <TEXTAREA id="contents" cols="40" rows="6" name="contents" class="fontchange"></TEXTAREA>
                        </td>
                    </tr>
                </table>
                <div id="button_area" class="clearfix">
                    <input onClick="uploadFile();" type="button" value="投稿" id="css_button" class="button_right" />
                </div>
            </form>
        </div>
        <! --loginform-->
    </div>
    <! --wrapper -->
</body>
</html>

3 Lambda

3-1 準備

以下2つのモジュールをDLします。

npm install aws-sdk
npm install nodemailer

以上をインストールすると以下の通りnode_modulesディレクトリ以下に保存されます。

[~] ls node_modules
aws-sdk nodemailer

後ほど、作成するindex.jsと共にzipにしてLambdaにuploadします。

3-2 Lambda function

以下の感じで作成します

index.js
console.log("Loading event")
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var mailer = require('nodemailer');

var settings = {
    service: 'Gmail',
    auth: {
        user: '送信元Gmailアドレス',
        pass: 'Gmailパスワード',
        port: 25
    }
};

var smtp = mailer.createTransport(settings);

exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));
    var bucket = event.Records[0].s3.bucket.name;
    var key = event.Records[0].s3.object.key;
    s3.getObject({Bucket: bucket, Key: key},
        function(err, data) {
            if (err){
                context.done('error', 'error getting file' + err);
            } else {
                var message = JSON.parse(data.Body);
                var options = {
                    to : '送信先Gmailアドレス',
                    replyTo : message.mail,
                    subject: message.title,
                    text: ' ' + message.contents
                };
                smtp.sendMail(options, function(error, info){
                    if (error){
                        console.log('error:' + error);
                    } else {
                        console.log('Message sent:' + info.response);
                    }
                })
            }
        }
    );
};

以上をzipで固めてLambda functionにupします

zip -r s3_form.zip index.js node_modules

function名はS3mail_semdとします。(タイポ...)

3-3: 対象S3バケットのイベントフック作成

最後に対象S3バケットで、下図の通りEvent Notificationsを設定します。

hook

以上でひと通り完了です。

確認

こんな感じで投稿すると
mail

無事Gmailで受け取れました

reply

参考サイト

https://www.system-i-enter.com/blog/blog/2015/02/03/s3/
https://www.system-i-enter.com/blog/blog/2015/02/10/aws-lambda/
http://dev.classmethod.jp/cloud/cors-cross-origin-resource-sharing-cross-domain/
http://dev.classmethod.jp/etc/about-cors/