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などのアカウントを要求することも可能です。
identity poolが作成されたら、identity pool idをメモします。
1-2: 自動作成したIAM RoleにS3 upload権限を追加
次に自動作成したIAM Roleにs3:PutObject / s3:PutObjectAcl権限を追加します。
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します。
<!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
- nodemailer (Gmail転送用)
npm install nodemailer
以上をインストールすると以下の通りnode_modulesディレクトリ以下に保存されます。
[~] ls node_modules
aws-sdk nodemailer
後ほど、作成するindex.jsと共にzipにしてLambdaにuploadします。
3-2 Lambda function
以下の感じで作成します
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を設定します。
以上でひと通り完了です。
確認
無事Gmailで受け取れました
参考サイト
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/