reCAPTCHA v3 実装(Movable Type 7)

| コメント(0)

reCAPTCHA_v3.jpg

Movable Type 7のコメント投稿にGoogle reCAPTCHA v3を実装してみた。以前に投稿した「Movable TypeとInvisible reCAPTCHA」と同様に、フロントエンド側のテンプレートとバックエンド側のCGIを修正する必要がある。

v3では、チャレンジ画像による認証ではなく、ユーザーのサイト閲覧行動に基づくとされるGoogleの判定スコアを用いた認証方式が採用されている。このためバックエンド側のCGIには、このスコアによる投稿可否判定処理を組み込む必要があった。以下に備忘録的に具体的な実装方法を記しておく。

【警 告】以下の改造では、テンプレートだけではなくMovable TypeのCGIそのものにも手を加えています。当然、Six Apart社のサポート対象外となりますので、改造するに当たっては、あくまで自己責任でお願いします。改造の結果、発生する可能性がある総ての不具合、損失、データの破損、消失等について当方は一切の責任を負わないことを予めご承知おき願います。

テンプレートの修正

フロントエンド側の実装方法については、「Google reCAPTCHA の使い方(v2/v3)」に詳細な説明とサンプルコードが掲載されている。注意すべき点は、トークンの取得タイミングを投稿ボタン押下げ時まで遅延させることである。トークンの有効時間は取得後2分間なので、ページ読込み時点で取得してしまうと投稿時には無効となっている可能性がある。ここでは、上記のサンプルコードをほぼそのまま利用してテンプレートの修正を行った。

なお、reCAPTCHAの認証キーの取得は、GoogleのreCAPTCHAサイトから行う。取得したキーを、以下のコードの「YourSiteKey」と「YourSecretKey」の部分に記入する。

Movable Typeの管理画面から、コメント・テンプレートコメントプレビュー・テンプレートに以下の追記、修正を行う(修正箇所は両方とも同じ)。

1)上記2つのテンプレート内の<form>タグに「 id="comments-form" 」を追記

<form method="post" action="<$MTCGIPath$><$MTCommentScript$>" name="comments_form" id="comments-form" onsubmit="if (this.bakecookie.checked) rememberMe(this)">

idとして用いる「 comments-form 」の名称は任意だが、下記追加スクリプトのL3「 var rc_form = document.getElementById('comments-form');」と揃える必要があることに注意。

2)<form>-</form>内の以下の部分を削除またはコメントアウト

<input type="hidden" name="armor" value="1" />

3)</form>の後に以下を追記

<script src="https://www.google.com/recaptcha/api.js?render=YourSiteKey"></script>
<script>
var rc_form = document.getElementById('comments-form');
//フォーム要素にイベントハンドラを設定
rc_form.onsubmit = function(event) {
    //デフォルトの動作(送信)を停止
    event.preventDefault();  
    //トークンを取得
    grecaptcha.ready(function() {
        grecaptcha.execute('YourSiteKey', {action: 'submit'}).then(function(token) {
            var token_input = document.createElement('input'); //input 要素を生成
            token_input.type = 'hidden';
            token_input.name = 'g-recaptcha-response';
            token_input.value = token; //トークンを値に設定
            rc_form.appendChild(token_input);
            var action_input = document.createElement('input'); //input 要素を生成
            action_input.type = 'hidden';
            action_input.name = 'action';
            action_input.value = 'submit';  //アクション名を値に設定
            rc_form.appendChild(action_input); 
            rc_form.submit();  //フォームを送信
        });
    });
}
</script>


CGIの修正

以上でフロントエンド側の追加・修正は完了したので、次にバックエンド側のCGIに手を加える。対象は、「 mt/mt-comment.cgi 」で、これは上記コメント入力フォーム・テンプレートにおいてformのaction属性に指定されている<$MTCGIPath$><$MTCommentScript$>の参照先である。このCGIは、コメント処理の本体プログラムをuseにより読み込むだけの機能しか有していない。 まず、デフォルトのCGIの末尾にあるuse部分(L10-L14)をコメントアウトする。

#!/usr/bin/env perl

# Movable Type (r) (C) 2001-2020 Six Apart Ltd. All Rights Reserved.
# This code cannot be redistributed without permission from www.sixapart.com.
# For more information, consult your Movable Type license.
#
# $Id$

use strict;
# use lib $ENV{MT_HOME} ? "$ENV{MT_HOME}/lib" : 'lib';
# use lib $ENV{MT_HOME}
#    ? "$ENV{MT_HOME}/plugins/Comments/lib"
#    : 'plugins/Comments/lib';
# use MT::Bootstrap App => 'MT::App::Comments';

この後ろに、"reCAPTCHA example in Perl"を参考にして作成した以下のコードを追加する。L44、L48において、reCAPTCHAサイトとの接続失敗や認証失敗が発生した場合には、エラーページに遷移するようにしているので、そのURLを適宜修正すること。

use warnings;
use CGI;
use LWP::UserAgent;
use JSON::Parse 'parse_json';

my $secret = "YourSecretKey";
my $gurl = "https://www.google.com/recaptcha/api/siteverify";
my $gscore = 0.5;
my $mtuse = 'use lib $ENV{MT_HOME} ? "$ENV{MT_HOME}/lib" : \'lib\'; use lib $ENV{MT_HOME} ? "$ENV{MT_HOME}/plugins/Comments/lib" : \'plugins/Comments/lib\'; use MT::Bootstrap App => \'MT::App::Comments\';';

my $cgi = CGI->new ();
my $ua = LWP::UserAgent->new ();
my $response = $cgi->param ('g-recaptcha-response');
my $remoteip = $ENV{REMOTE_ADDR};
my $greply = $ua->post (
    $gurl,
    {
        remoteip => $remoteip,
        response => $response,
        secret => $secret,
   },
);

if ($greply->is_success ()) {
    my $json = $greply->decoded_content ();
    my $result = parse_json ($json);
    if($result->{success} && $result->{action } eq "submit" && $result->{score} >= $gscore) {
        eval ($mtuse);
    } else {
        print "Location: http://www.example.com/comment-error2.html\n\n"; #jump to your authentication error page.
    }

} else {
    print "Location: http://www.example.com/comment-error1.html\n\n"; #jump to your connection error page.
}

前述のようにreCAPTCHA v3では、Googleの判定スコアに基づいて認証の可否を決める必要がある。上記のCGIでは、L22で閾値の「$gscore」を0.5に設定し、L41においてこの閾値未満の判定スコアの場合にはスパムとして認証を拒否している。この閾値は、サイトごとに異なるので状況をモニタリングしながら調整する必要がある(本サイトにおける非スパムコメントは、総て0.7以上の判定スコアであった)。また、Googleから戻されたアクション名が、送信したもの("submit")と一致していることについても検証している。追加コードの詳細については、「Movable TypeとInvisible reCAPTCHA」を参照のこと。


JSONモジュールが使えない場合
このブログのレンタルサーバーでは、JSONモジュールが使えないので、追加コードのL38以降を以下のように変更して対応している。JSONを単一の文字列にして連想配列を作るという些か荒っぽい方法だが、Googleから受け取るJSONは極めて単純なので実用上はこれで十分だ。(*1)
if ($greply->is_success ()) {
    my $json = $greply->decoded_content ();
    $json =~ s/\{//g;
    $json =~ s/\}//g;
    $json =~ s/\r//g;
    $json =~ s/\n//g;
    $json =~ s/\":/\";/g;
    $json =~ s/\"//g;
    $json =~ s/[\s ]+//g;
    my %result = split(/[;,]/, $json);

    if($result{success} eq "true" && $result{action} eq "submit" && $result{score} >= $gscore) {
        eval ($mtuse);
    } else {
        print "Location: http://www.example.com/comment-error2.html\n\n"; #jump to authentication error page.
    }

} else {
    print "Location: http://www.example.com/comment-error1.html\n\n"; #jump to connection error page.
}

この場合、L18のJSONモジュールの読み込みもコメントアウトまたは削除すること。そのままだとCGIロード時にエラーとなるので念のため。
use warnings;
use CGI;
use LWP::UserAgent;
# use JSON::Parse 'parse_json';

因みにL40で「 ": 」を 「 "; 」に置換しているのは、以下に示すようにJSONの中にタイムスタンプが含まれており、これがキーの末尾のコロンと重複するのを避けるためである。
{
  "success": true,
  "challenge_ts": "2021-01-27T03:29:04Z",
  "hostname": "www.minimalvideo.com",
  "score": 0.9,
  "action": "submit"
}

(*1)どうしても規約通りにparseしたいという潔癖症の人は、Simple JSON Parser in Perlというのもある(雀を撃つのに大砲を使うようなところもあるが)。

コメントする

アーカイブ