reCAPTCHA v3の実装方法についてはこちら。
はじめに このような鄙のブログの記事にコメントを寄せる奇特な方は滅多におられないものの、ロボットによるスパムコメントは時折やってくる。その対策として、今となっては珍しいreCAPTCHA V1を長年利用してきた。


<script src="https://www.google.com/recaptcha/api.js" async defer></script> <script> function captchaSubmit(data) { document.getElementById("comments-form").submit(); } </script>コメント入力フォーム・テンプレートの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 」の名称は任意だが、上記ヘッダー・テンプレートの「 document.getElementById("comments-form").submit(); 」と揃える必要があることに注意。 コメント入力フォーム・テンプレートの以下の部分を削除またはコメントアウト
<input type="submit" accesskey="v" name="preview" id="comment-preview" value="確認" /> <input type="submit" accesskey="s" name="post" id="comment-submit" value="投稿" />コメント入力フォーム・テンプレートの上記削除部分の代りに以下を追記
<button class="g-recaptcha" data-sitekey="Your Site Key" data-callback="captchaSubmit" data-badge="inline" type="submit" accesskey="s" name="post" id="comment-submit">投稿</button>Movable Type7を利用している場合には、以下の部分も削除またはコメントアウト
<input type="hidden" name="armor" value="1" />なお、Movable Type7のテンプレート構成では、「ヘッダー・テンプレート」は「HTMLヘッダー・テンプレート」、「コメント入力フォーム・テンプレート」は、「コメント・テンプレート」となっている。 CGIの修正 以上でフロントエンド側の追加・修正は完了したので、次にバックエンド側のCGIに手を加える。対象は、「 mt/mt-comment.cgi 」で、これは上記コメント入力フォーム・テンプレートにおいてformのaction属性に指定されている<$MTCGIPath$><$MTCommentScript$>の参照先である。このCGIは、コメント処理の本体プログラムをuseにより読み込むだけの機能しか有していない。 まず、デフォルトのCGIの末尾にあるuse部分(L10、L11)をコメントアウトする。
#!/usr/bin/perl -w # Movable Type (r) (C) 2001-2017 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 MT::Bootstrap App => 'MT::App::Comments';この後ろに、上記の"reCAPTCHA example in Perl"を参考にして作成した以下のコードを追加する。L40、L44において、reCAPTCHAサイトとの接続失敗や認証失敗が発生した場合には、エラーページに遷移するようにしているので、そのURLを適宜修正すること。
use warnings; use CGI; use LWP::UserAgent; use JSON::Parse 'parse_json'; my $secret = "Your Secret Key"; my $gurl = "https://www.google.com/recaptcha/api/siteverify"; my $mtuse = 'use lib $ENV{MT_HOME} ? "$ENV{MT_HOME}/lib" : \'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}) { 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. }Movable Type7を利用している場合には、上記のL19を以下のように修正すること。
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\';';最後にMovable Typeの管理画面で、設定->コミュニケーション->CAPTCHAプロバイダを「なし」に設定する。 適当な記事を再構築して、このページのコメント欄の下部に表示されているInvisible reCAPTCHAのdata-badgeが表示されコメントが投稿できれば成功。 制限事項と解説 現時点のInvisible reCAPTCHAは、一つのform内では単一ボタンにしか紐付けられないようなので、コメント入力フォーム・テンプレートの確認ボタンを削除している(残しておいても認証エラーとなる)。Perlの追加コードを工夫すればなんとかなりそうだが、今のところはこれで良しとする。 追加コードのポイントは、mt-comment.cgi でのuseによるモジュール読み込みを、reCAPTCHA認証後まで遅延させることにある。このMovable Typeのモジュール(MT::Bootstrap)は、読み込み直後に自動実行されるので、認証とは関係なく無条件でコメントを受け付けてしまうからだ。遅延の具体的方法は、L19で変数$mtuseにuse部分を単なる文字列として格納し、L38の eval ($mtuse); により評価・実行するという実にストレートなものである。 これは蛇足だが、追加コードのL37( if($result->{success}) { )内や evalブロック内でuseしても無駄。CGIがロードされた時点で、コードのどの部分にあろうとuseによる読み込みが行われてしまうためである。これを回避する一つの方法としてrequireを使うことも可能だが、その場合、コメント処理の本体プログラムに手を加える必要がありMovable Typeのバージョンアップ時の対応が厄介になると予想される。その点、mt-comment.cgiは、単なる「バッチファイル」なので、切り分けが容易だ。また、基本的にMovable Typeのバージョンにも依存しない。 ところで追加コードのL37「 if($result->{success}) { 」の挙動については、このサーバー環境ではJSONモジュールが使えないので未確認である。Googleから受け取るJSON(下記参照)で「true」が戻る場合は0以外なので真だが、「false」が戻った場合に偽となるのだろうか? Perlには、予約語としての true も false も無いということをどこかで聞いた憶えがあるのだが...。たぶんL36の「 parse_json 」がうまくやってくれるのだろう。 JSONモジュールが使えない場合 このブログのレンタルサーバーでは、JSONモジュールが使えないので、追加コードのL34以降を以下のように変更して対応している。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") { 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. }この場合、L15のJSONモジュールの読み込みもコメントアウトまたは削除すること。そのままだとCGIロード時にエラーとなるので念のため。
use warnings; use CGI; use LWP::UserAgent; # use JSON::Parse 'parse_json';因みにL40で「 ": 」を 「 "; 」に置換しているのは、以下に示すようにJSONの中にタイムスタンプが含まれており、これがキーの末尾のコロンと重複するのを避けるためである。
{ "success": true, "challenge_ts": "2018-01-13T13:10:07Z", "hostname": "www.minimalvideo.com" }おまけ スパムコメントが認証に失敗した場合にGoogleから受け取るJSONは以下の通り。
{ "success": false, "error-codes": [ "missing-input-response" ] }コメント入力フォームから投稿したにもかかわらず上記のエラーとなる場合は、「 g-recaptcha-response 」の値が間違っているか欠落していることが原因の一つだ。別のサイトにInvisible reCAPTCHAを配置している時に気付いた。特に認証部分をサブルーチン化している場合には引数の受け渡しに要注意だ。 実装効果 本改造を実施した後、CGIの中にアクセスログ機能を設置して経過を観察してみた。約一週間で20件ほどのスパムコメントがあったが、総て防御できていた。当ブログの環境下では十分な効果を発揮しているようだ。 (*1)どうしても規約通りにparseしたいという潔癖症の人は、Simple JSON Parser in Perlというのもある(雀を撃つのに大砲を使うようなところもあるが)。
コメントする