サーバーレス(HTML on S3)にGoogle認証付きS3アップローダーを作る

  • 7
    Like
  • 0
    Comment

更新(2017/11/18)

  • S3にあげるたびに更新が起こるのを阻止
  • HTML5のvalidationを使う
  • アップロードが完了した場合内容を画面上部に表示

やりたいこと

  • S3上にHTMLとJSファイルをおいて静的webページとして公開
  • webページに入力した内容をcsvにまとめてS3に出力
  • 認証はgoogleのOpenID Connectを使用(roleをつけておく)
  • バケットポリシーでIP制限をかけておく

S3でwebページ公開に関して

以下のサイトを参考にして公開
S3で静的ウェブサイトをホスティングしてみる
バケットポリシーも作っておく

googleのOpen Connect周り

以下のサイトを参考にクライアントID+認証周り作成
Googleの「OpenID Connect」を利用する為の「クライアントID」の取得方法

スクリーンショット 2017-11-13 18.23.27.png
*承認済みのJavaScript生成元を設定せずにしばらくはまっていました...

Googleと連携するroleの作成

AWSのコンソール画面で
IAM > role > roleの作成
と進み、ウェブIDのタブを選択してプロバイダーにGoogleを選択
AudienceにはクライアントIDを入力してpolicyのattachなどを行う
スクリーンショット 2017-11-15 16.49.52.png

例で用いたpolicyはS3の特定のフォルダにのみアクセス可能なものにしました

policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME/output/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME"
            ],
            "Effect": "Allow",
            "Condition": {
                "StringEquals": {
                    "s3:prefix": "output"
                }
            }
        }
    ]
}

いよいよJavascript作成

フォルダ構成

  • BUCKET
    • output
    • HTML
      • index.html
      • favicon.ico
      • logic.js

ログイン周り

以下のURLを参考にJavascriptを作成(ログイン箇所に関しては例をそのまま使用)
ブラウザの JavaScript
Web Federated Identity Examples

index.html
<!DOCTYPE html>
<html>
<head>
    <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
    <title>AWS SDK for JavaScript - Sample Application</title>
    <meta charset="utf-8"/>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</head>

<body>
<div id="results" role="alert"></div>
<nav class="navbar navbar-default">
    <div class="container">
        <!-- 2.ヘッダ情報 -->
        <div class="navbar-header">
            <a class="navbar-brand">S3アップローダー</a>
        </div>
    </div>
</nav>


<div class="container" id="form">
    <form>
        <div class="form-group" id="name">
            <label>名前</label>
            <input type="text" name="name" class="form-control" placeholder="申請 太郎" required>
        </div>
        <div class="form-group" id="account_flg">
            <label>アカウント</label>
            <select name="account_flg" class="form-control">
                <option value="不要">不要</option>
                <option value="必要">必要</option>
            </select>
        </div>
        ...
        <button type="button" onclick="upload_csv();" class="btn btn-primary">申請登録</button>
    </form>
</div>
<span
        id="login"
        class="g-signin"
        data-height="short"
        data-callback="loginToGoogle"
        data-cookiepolicy="single_host_origin"
        data-requestvisibleactions="http://schemas.google.com/AddActivity"
        data-scope="https://www.googleapis.com/auth/plus.login">
</span>
<script>
    //以下ほぼサンプルのコピペ
    //s3 = null;  s3をアップローダーでも使いたいので後々グローバル変数で指定しています
    var clientID = '************.apps.googleusercontent.com'; // Google client ID
    var roleArn = 'arn:aws:iam::************:role/ROLE_NAME';
    document.getElementById('login').setAttribute('data-clientid', clientID);
    function loginToGoogle(response) {
        if (!response.error) {
            AWS.config.credentials = new AWS.WebIdentityCredentials({
                RoleArn: roleArn,
                WebIdentityToken: response.id_token
            });
            s3 = new AWS.S3();
            console.log('You are now logged in.');
        } else {
            console.log('There was a problem logging you in.');
        }
    }
    (function () {
        var po = document.createElement('script');
        po.type = 'text/javascript';
        po.async = true;
        po.src = 'https://apis.google.com/js/client:plusone.js';
        var s = document.getElementsByTagName('script')[0];
        s.parentNode.insertBefore(po, s);
    })();
</script>
<script src="logic.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>

アップローダー周り

logic.js
//ここに必要な項目のIDを書いておく
csv_array = [
    'name',
    'account_flg'
]

//メインのところ(どのcsvを作成するか)
function upload_csv() {
   //連続で投稿する場合に結果画面を一度空欄にする
    document.getElementById('results').innerHTML = "";
    //inputタグを指定してvalidation確認
    var validity_list = document.getElementById("form").getElementsByTagName("div");
    for (var i = 0, l = validity_list.length; i < l; i++) {
        var validity_zone = validity_list[i].getElementsByTagName("input")[0];
        if (!validity_zone) {
            continue;
        }
        if (!validity_zone.checkValidity()) {
            alert('【'+validity_list[i].getElementsByTagName("label")[0].innerHTML + '】 '+ validity_zone.validationMessage);
            return;
        }
    }
    //成功・失敗検知用
    success_flg = true;
    upload('account', make_csv(csv_array));
    //終わった後上部に移動して結果をalertで通知
   $("html,body").animate({scrollTop: 0}, 'fast');
    if (success_flg) {
        alert('Success upload');
    } else {
        alert("Fail upload ...");
    }
    //画面更新を防ぐ
    return false;
}

//配列情報から変数(csv)にヘッダー情報と内容を入れる関数
function make_csv(array) {
    var csv = '';
    for (i in array) {
        var block = document.getElementById(array[i]);
        csv += block.getElementsByTagName("label")[0].innerHTML + ',';
    }
    csv = csv.slice(0,-1);
    csv += '\n';
    for (i in array) {
        var block = document.getElementById(array[i]);
        if (block.getElementsByTagName("select")[0]){
            var num = block.getElementsByTagName("select")[0].selectedIndex;
            csv += block.getElementsByTagName("select")[0].options[num].value + ',';
        } else {
            csv += block.getElementsByTagName("input")[0].value + ',';
        }
    }
    csv = csv.slice(0,-1);
    return csv;
}

//出来上がったcsvファイルをアップロードする関数
function upload(service_name, body) {
    var results = document.getElementById('results');
    var params = {
        Bucket: 'BUCKET_NAME',
        Key: 'output/' + service_name + '_' + timestamp() + '.csv', //名前が被らないようにtimestam付
        ContentType: 'csv',
        Body: body
    };
    s3.putObject(params, function (err, data) {
        if (err) {
            success_flg = false;
        }
        //実行結果を画面上部に表示
        results.setAttribute("class", !err ? 'alert alert-success':'alert alert-danger' );
        if (!err){
            results.innerHTML = results.innerHTML + "<strong>【"+service_name+"】</strong>";
        }else{
            results.innerHTML = results.innerHTML + "<strong>ERROR【"+service_name+"】</strong>";
        }
    });
}

//タイムスタンプ作成用関数
function timestamp() {
    var d = new Date();
    var year = d.getFullYear();
    var month = d.getMonth() + 1;
    var day = d.getDate();
    var hour = ( d.getHours() < 10 ) ? '0' + d.getHours() : d.getHours();
    var min = ( d.getMinutes() < 10 ) ? '0' + d.getMinutes() : d.getMinutes();
    return '' + year + month + day + hour + min;
}

終わりに

IP制限をかけておけば簡単に社内サービスを作成できます
単にローカルファイルを上げるだけであれば、htnl上でファイル取得をしてuploadの関数を使えばできます
サーバーレス素晴らしい