AWS Lambdaを使って、ブラウザ側とサーバー側で同じバリデーションをするということをやった。

AWS Lambdaを使って、ブラウザ側とサーバー側で同じバリデーションをするということをやった。

日記です。今日の日記コードはこれ。

github.com

qiitaにはさすがにもうAWS Lambdaでフォームなんていう記事は山盛りあったのでこっちで。

日記

Node.jsはサーバーとして動かせるので、ブラウザ側とサーバー側で同じスクリプトを用いることが可能です。(アイソモーフィック?)

しかし動かすとなるとサーバーを用意して〜、など色々と難関がありました。その難関を軽く解消してくれたのがAWS Lambdaであることはみなさんご存知のとおりですね。

バリデーションの準備

まずバリデーターライブラリを用意します。

これにはvalidatorを使いました。

github.com

これをラップしたRecordというクラスをつくり、以下のようなコンフィグ(config.jsに入っています)をあたえ、validateメソッドで確認できるようにしました。

exports.config = {
  attributes: ['name', 'email', 'age', 'gender'],
  validation: {
    name: {
      isLength: {min: 1, max: 20, message: '1-20文字で入力してください'}
    },
    email: {
      isEmail: {message: 'メールアドレスを入力してください'}
    },
    age: {
      isNumeric: {message: '数字を入力してください'}
    },
    gender: {
      isIn: ['female', 'male', 'other']
    }
  }
};

ブラウザ側バリデーション

あとはこのバリデーターを、ブラウザ側で使えるようにします。

ライブラリやコンフィグファイルが別ファイルになっているので、毎度おなじみのbrowserifyで連結します。

const Record = require('../lib/src/record').Record;
const config = require('../lib/src/config').config;
const record = new Record(config);

const Validator = window.Validator = {};

Validator.validate = (data) => {
  if (record.assign(data).validate()) {
    return {result: 'success', data: record.parameters};
  } else {
    return {result: 'failure', errors: record.errors};
  }
};

あとはこのバリデーションをサーバーに送信する前に行い、validであれば送信します。

サーバー側バリデーション

送信されてくるデータは全く安全でないのは常識ですから、サーバー側でもバリデーションする必要があります。

ブラウザ側とほぼ同じコードですが、AWS LambdaではNode.jsが動いていますから、browserifyは必要ありません。

今回はバリデーションの成否で終了していますが、そこらへんにdynamoDBやメール送信の処理を書けばいいんじゃないでしょうか。

"use strict";

const Record = require('./src/record').Record;
const config = require('./src/config').config;

exports.handler = (event, context, callback) => {
  let record = new Record(config);

  if (record.assign(event).validate()) {
    context.succeed({result: 'success', data: record.parameters});
  } else {
    context.fail(JSON.stringify({result: 'failure', errors: record.errors}));
  }
};

エラー時の戻り値がブラウザ側と違ってしまうのが気になるところですが(failの引数は{"errorMessage": 引数}JSONとして飛んでいく)、これはAWS Api Gateway側でfailureをキャッチして、同じ形にします。

.*"result":"failure".*で失敗ステータスコードに振り、本文マッピングテンプレートでエラー部を取り出したものをレスポンスとします。

$input.path('$.errorMessage')

ブラウザ側送信時処理

送信せずとも同じバリデーションが行えるわけですから、送信前にバリデーションし、invalidであればdeployErrorsというメソッドが、各項目のinputの上や下にエラーを表示します。

もし何らかの都合でinvalidなデータが送信されても、サーバー側でinvalid判定されて、同じエラーが返ってきてdeployErrorsというメソッドが、各項目のinputの上や下にエラーを表示します。

    function send($form, $thanks) {
      var data = traceValue($form);

      var validated = Validator.validate(data);
      if(validated.result == 'failure'){
        deployErrors($form, validated.errors);
        return;
      }

      disablizeButton($form, true);
      superagent
              .post(endPoint)
              .set('Accept', 'application/json')
              .send(data)
              .end(function (err, res) {
                if (err) {
                  deployErrors($form, res.body.errors);
                  disablizeButton($form, false);
                } else {
                  $form.hide();
                  $thanks.show();
                }
              });
    }

わりと長年の夢であった、サーバーとクライアントで同じコードでバリデーションするというのが、比較的簡単にできてしまいました。しかも課金の心配が結構軽い。

フォーム作成というこまごまとした仕事は、前職前前職ともによくあって、cgiの、メンテされてるんだがされてないんだかわからないスクリプトを使用していました。

Rails大好きなわたしもさすがに一個の問い合わせフォームにRailsというわけにもいかなくて難儀していましたが、これならヤバイ脆弱性も自分の責任のうちで作成できるのでいいかもしれませんね。