Google FormにreCAPTCHA的なモノを実装する【GAS】

GoogleにはreCAPTCHAと呼ばれる、外部向けのフォーム等に於いてボットなどによるスパムを防止する為の仕組みが用意されています。通常のHTMLのフォームに組み込んで利用する事ができる大変便利なものですが、Google Formにはこのような機能が搭載されていません。

内部で使う分には必要のない機能ですが、どうしてもお問い合わせフォームにボットが悪さをしてゴミメールを沢山送ってくるのは困ります。そこで今回、お手製のスパム防止用のreCAPTCHA的仕組みをつけて見ました

今回使用するファイル

今回は2つのファイルを利用します。GoogleスライドはプレゼンではなくreCAPTCHA的画像生成の為の土台として使います。フォームには画像生成、フォームのパーツ入れ替えの大きく2つの機能を持たせてあります。

事前準備と仕組み

事前準備

セットアップする箇所は4箇所。

  1. スパム防止用フォームを開くと上にパズルのアイコンが出るので、セットアップを実行
  2. 初期化を実行すると、このフォームのIDがスクリプトプロパティに格納されます。
  3. トリガー設置で、画像生成とフォームパーツの入れ替え用関数がスクリプトトリガーに1時間毎でセットされます。
  4. スパム防止用フォームのスクリプトエディタを開く
  5. slides.gsにあるfileidは画像生成用スライドのファイルIDを入れます。
  6. slides.gsにあるfolderidは画像を生成するフォルダのIDを入れます。

これで利用可能になります。フォームのreCAPTCHAパーツと「上記画像の文字を入力してください」は変更してはいけません。

仕組み

Google Apps Scriptにはテキストを画像化するような素敵なメソッドは用意されていません。また、SlidesAppにも直接スライドを画像化するようなメソッドは用意されていません。

そこで今回は

  1. UrlfetchAppにてスライドをPDFならぬPNG形式に変換する仕組みを使って画像を生成
  2. スライドなので色々カスタマイズが可能
  3. Formの回答の検証では、スクリプトで生成し画像と同じ文字が入っていないと送信できないようにしてあります。
  4. 今回はランダムな文字列で作っていますが、例えば計算式を画像で生成し、答えをValidation側にセットしておくことで、文字入力ではなく計算結果でreCAPTCHAが作れます。
  5. フォントを色々変更してみるのもスパム対策になるでしょう。
  6. 本来のGoogle reCAPTCHAはCloud ConsoleでAPIを有効にし、キーを取得してJavaScriptでHTMLにコードを記述する面倒な手法ですが、こちらの手法はFormだけでなく通常のHTMLでも利用可能です。WordPressに魔改造で貼り付けたものにも、組み込む事はおそらく可能です。

図:テキスト画像に表示されてるものと一致しないと送信できない

ソースコード

今回はgetPropsetPropについてはそのままスクリプトプロパティの値の出し入れの為に直接関数を用意しています。slides.gsが画像生成用のスクリプト、makeForm.gsがフォームのパーツ差し替え用スクリプトになります。

slides.gs

//ファイルID
var fileid = "ここにスライドのファイルIDを入れる";

//生成先フォルダID
var folderid = "ここに画像生成先フォルダのIDを入れる";

//画像のファイル名
var filename = "captcha.jpg";

//1枚目のスライドのID(1枚目は必ずSlideidがpとなる)
var slideId = "p";

//スライドをjpg画像へ変換して格納する
function slide2jpg() {
  //フォルダ内のcaptcha.jpgを削除する
  try{
    var old = getProp("pictid");
    DriveApp.getFileById(old).setTrashed(true);
  }catch(e){
    //エラーでもスルーする
  }

  //このプレゼンテーションをまず取得する
  var slide = SlidesApp.openById(fileid);
  
  //テキストボックスの文字を置き換える
  var pre = slide.getSlides()[0];
  var shape = pre.getShapes()[0];
  var setword = generateCaptcha();
  shape.getText().setText(setword);

  //セーブして保存する
  slide.saveAndClose();
  
  //画像変換実行
  var ret = changepict(setword);
  
  //formの項目変更
  var ret2 = changeForm();
  
  //値を返す
  return ret2;
}

//画像変換
function changepict(setword){
  try{
    //イメージ変換
    //変換用URL
    var url = 'https://docs.google.com/presentation/d/' + fileid + '/export/png?id=' + fileid + '&pageid=' + slideId; 
  
    //オプション指定
    var options = {
      headers: {
        Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
      }
    };
  
    //PNG画像へ変換
    var response = UrlFetchApp.fetch(url, options);
    var image = response.getAs(MimeType.PNG);
  
    //Driveへ生成
    var pictman = DriveApp.getFolderById(folderid).createFile(image);
    var pictid = pictman.getId();
    var pictsetname = pictman.setName(filename);
  
    //画像のIDと生成時間をプロパティに格納する
    setProp("pictid", pictid);
    setProp("genetime", new Date());
    setProp("captcha", setword)
    
    //値を返す
    return true;
    
  }catch(e){
    return false;
  }
}

//ワード生成
function generateCaptcha() {
  //使用する文字
  var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  
  //桁数
  var len = 8;
  
  //ランダムな文字列の生成
  var result = "";
  for(var i=0;i<len;i++){
    result += str.charAt(Math.floor(Math.random() * str.length));
  }

  return result;
}
  • 今回はSlidesAppというスライド操作用のクラスを利用します。
  • 1枚しか入っていないスライドに文字のテキストボックスのみです。この文字を変更し、UrlfetchAppにて画像(PNG形式)へ変換します。変換するコードはPDFを変換するコードと非常に似ています。
  • Spreadsheetとは異なり、getTextしてからsetTextをするという奇妙な方法でテキストボックスの値の書き換えをします。
  • 書き換えする文字は半角英数字でgenerateCaptcha関数で行っています。正規表現でValidationを行うので記号などは入れてはいけません。
  • changepict関数でスライドを画像化し、指定のフォルダに格納しています。
  • changepict関数に続けて、changeForm関数を実行し、フォームのパーツを入れ替えています。
  • スクリプトプロパティには画像のID, 生成時間, captchaで生成したワードを入れています。
  • このslide2jpg関数Triggerにて毎日1時間毎に実行し、画像を生成&入れ替えを自動で行わせることになります。

makeForm.gs

function changeForm() {
  //アクティブフォームを取得する
  var fid = getProp("formid");
  var form = FormApp.openById(fid);
  
  //プロパティの値を取得しておく
  var pid = getProp("pictid");
  var pword = String(getProp("captcha"));

  // 質問項目が画像のものだけを取得
  var items = form.getItems(FormApp.ItemType.IMAGE);
  
  //captcha画像を差し替える
  items.forEach(function(item){
    //対象のグリッドパーツを見つける
    if(item.getTitle().match(/reCaptcha.*$/)){
      //質問セクションを取得する
      var question = item.asImageItem();
      
      //画像ファイルを取得
      var blob = DriveApp.getFileById(pid).getBlob();
 
      //画像を差し替える(300pxに調整)
      question.setImage(blob).setWidth(300);
    }
  });

  //回答の検証を作成
  var Validation = FormApp.createTextValidation()
    .requireTextMatchesPattern(pword)
    .setHelpText("入力文字が画像の文字と一致しません")
    .build();
  
  //質問事項がテキストのものだけを取得
  items = form.getItems(FormApp.ItemType.TEXT);

  //captcha入力欄を差し替える
  items.forEach(function(item){
    //対象のグリッドパーツを見つける
    if(item.getTitle().match(/上記画像の文字を入力してください.*$/)){
      //セットする
      item.asTextItem().setValidation(Validation);
      item.asTextItem().setRequired(true);
    }
  });
}
  • Formには画像パーツreCAPTCHAの文字を入力するテキストボックスを用意してあります。
  • FormのImageを取得し、reCAPTCHAと名前のついているものだけを対象としています。生成済みの画像をBlobで取得してsetImageで差し替え、300pxに縮小しています。
  • Validationでは回答の検証をコードで入れてありますTextValidationBuilderクラスを使って作るのですが、このクラス通常のテキストの「含む」というものが生成するメソッドがありません。今回はrequireTextMatchesPatternを利用して、正規表現で一致するものという条件をセットしています。
  • ValidationにはreCAPTCHAで生成したワードを検証用ワードとしてセットしています。
  • setValidationでセットし、必ずsetRequiredをtrueにします(そうしないとスパム対策の意味がない)
  • changeFormを手動実行すると、フォームのパーツが最新の画像とreCAPTCHAの文字による検証に入れ替わります。実際にはこれをTriggerで自動で行わせています。
  • あまりTriggerで短い間隔でセットすると、1日のTrigger実行とUrlfetchAppのQuotaに引っかかってしまうので、ほどほどに。

図:reCAPTCHAらしい画像と入力欄の出来上がり

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)