2026年9月28日

2026年9月28日

WordPressのログインに行動生体(Behavioral Biometrics)判定を連携する方法と注意点

行動生体(Behavioral Biometrics)は、キー入力の間隔やマウス・タッチの動きといった「操作のくせ」から、本人らしさやボットらしさを推定する技術です。WordPressでは、これを単独のログイン手段にするのではなく、パスワードと二要素認証の上に重ねる「リスク判定の材料」として使うのが現実的です。この記事では、外部の判定サービスと連携する構成、authenticate フィルターを使った組み込み方、締め出し事故と個人データの扱いで気をつける点を説明します。

行動生体判定でできること・できないこと

行動生体判定のサービスは、ログイン画面に読み込んだスクリプトで操作の特徴を集め、サーバー側で「いつもの本人と比べてどれくらい違うか」「人間の操作らしいか」をスコアにして返します。金融機関の不正検知などで使われてきた技術で、パスワードが漏れていても、操作の様子が明らかに違えば怪しいと判断できるのが利点です。

一方で、限界もはっきりしています。

  • 判定は確率的で、誤判定があります。けがをした、別の端末を使った、パスワード管理ツールで自動入力した、といった理由で本人でもスコアが変わります。
  • 初回ログインや利用回数の少ないユーザーは比較の基準がなく、精度が出ません。
  • 判定結果だけでログインを許可する(パスワードを省く)使い方は勧められません。

このため、構成の基本は「パスワード+二要素認証を土台にし、行動生体のスコアは追加の拒否や警告に使う」です。まだ二要素認証を入れていないなら、行動生体より先にそちらを整えてください。Two Factor プラグインや Wordfence のログインセキュリティ機能など、保守されている実績のあるプラグインを使うほうが、自作より安全です。

WordPress向けの選択肢

(2026年10月現在)WordPress専用の行動生体プラグインで、広く使われ継続的に保守されていると言えるものは多くありません。現実的な選択肢は次の3つです。

  1. ボット判定の一部として使う:Cloudflare Turnstile や Google reCAPTCHA v3 は、操作や環境の信号をもとに人間らしさを判定します。いわゆる行動生体の「本人照合」ではありませんが、ログイン画面への自動攻撃を減らす目的なら、これが最も手軽です。対応プラグインも複数あります。
  2. 外部の行動生体サービスとAPI連携する:本人照合まで行いたい場合は、行動生体の判定APIを提供する事業者と契約し、ログイン処理に組み込みます。提供機能、SDKの仕様、データの保存場所は事業者ごとに違うので、契約前に資料と規約を確認してください。
  3. WAFやホスティング側の機能に任せる:一部のWAFやCDNは、通信の振る舞いからボットを判定する機能を持っています。WordPress側の改修が不要なのが利点です。

以下では2の構成を例に、WordPressのどこに組み込むかを説明します。事業者のAPIは架空のもの(risk.example.com)として書くので、実際の項目名は契約先のドキュメントに読み替えてください。

構成:どこで信号を集め、どこで判定するか

流れは次のとおりです。

  1. login_enqueue_scripts でログイン画面に事業者のSDKを読み込む。SDKは操作の特徴を集め、結果を表す一時的なトークンを作る。
  2. login_form でフォームに隠しフィールドを足し、送信時にトークンを一緒に送る。
  3. authenticate フィルターで、パスワード確認が済んだ後にトークンを事業者APIへ送り、スコアを受け取る。
  4. スコアが高い(危険)ならエラーを返して止める。中程度なら記録だけ残し、管理者が後で確認できるようにする。

ポイントは、操作の生データ(キー入力の時刻列など)をWordPressのデータベースに保存しないことです。WordPressが扱うのはトークンとスコアだけにすると、漏えい時の影響と、個人データとしての管理範囲を小さくできます。

実装例:mu-plugin で authenticate に組み込む

作業前にサイト全体のバックアップを取り、ステージング環境で試してください。APIキーはコードに直接書かず、wp-config.php に定数として置きます(下の値はダミーです)。

// wp-config.php(ダミー値。実際のキーをリポジトリに含めない)
define( 'WPDR_RISK_API_KEY', 'your-api-key' );

wp-content/mu-plugins/wpdr-login-risk.php として次のファイルを置きます。

<?php
/**
 * Plugin Name: WPDR Login Risk Signal (example)
 * Description: ログイン時に外部の判定APIへトークンを送り、高リスクなら止める例。
 */

// 1. ログイン画面に事業者SDKを読み込む(URLは架空)
add_action( 'login_enqueue_scripts', function () {
    wp_enqueue_script( 'wpdr-risk-sdk', 'https://risk.example.com/sdk.js', array(), null, array( 'strategy' => 'defer' ) );
} );

// 2. フォームに隠しフィールドと nonce を追加
add_action( 'login_form', function () {
    wp_nonce_field( 'wpdr_login_signal', 'wpdr_signal_nonce' );
    echo '<input type="hidden" name="wpdr_signal_token" id="wpdr_signal_token" value="">';
} );

// 3. パスワード確認(優先度20)の後で判定する
add_filter( 'authenticate', function ( $user, $username, $password ) {
    // パスワード認証に失敗している、またはログイン画面以外(XML-RPC等)なら何もしない
    if ( ! ( $user instanceof WP_User ) || ! did_action( 'login_init' ) ) {
        return $user;
    }
    if ( ! defined( 'WPDR_RISK_API_KEY' ) ) {
        return $user;
    }

    $token = '';
    $nonce = isset( $_POST['wpdr_signal_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['wpdr_signal_nonce'] ) ) : '';
    if ( wp_verify_nonce( $nonce, 'wpdr_login_signal' ) && isset( $_POST['wpdr_signal_token'] ) ) {
        $token = sanitize_text_field( wp_unslash( $_POST['wpdr_signal_token'] ) );
    }

    $response = wp_remote_post( 'https://risk.example.com/v1/evaluate', array(
        'timeout' => 3,
        'headers' => array(
            'Authorization' => 'Bearer ' . WPDR_RISK_API_KEY,
            'Content-Type'  => 'application/json',
        ),
        // パスワードは送らない。ユーザーIDとトークンだけ
        'body'    => wp_json_encode( array( 'token' => $token, 'user' => (string) $user->ID ) ),
    ) );

    // 判定サービスが落ちている・遅いときは通常の認証に任せ、締め出さない
    if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
        return $user;
    }

    $data  = json_decode( wp_remote_retrieve_body( $response ), true );
    $score = isset( $data['risk'] ) ? (float) $data['risk'] : 0.0;

    if ( $score >= 0.9 ) {
        // 理由を詳しく出さない(攻撃者に手がかりを与えない)
        return new WP_Error( 'wpdr_login_blocked', 'ログインを確認できませんでした。時間をおいて再度お試しください。' );
    }
    if ( $score >= 0.6 ) {
        update_user_meta( $user->ID, 'wpdr_last_login_risk', array( 'score' => $score, 'time' => time() ) );
    }
    return $user;
}, 30, 3 );

コードの要点を補足します。

  • WordPressのパスワード確認は authenticate フィルターの優先度20で行われます。優先度を30にしているのは、パスワードが正しいと分かった後だけ外部APIを呼ぶためです。パスワードが違う試行ごとにAPIを呼ぶと、総当たり攻撃のたびに外部通信が発生し、従量課金や遅延の原因になります。
  • did_action( 'login_init' ) で、wp-login.php からのログインに限定しています。authenticate はXML-RPCやアプリケーションパスワードによるREST APIの認証でも通るため、トークンを送れない経路まで止めてしまわないようにしています。その代わり、使っていないXML-RPCは無効化し、アプリケーションパスワードは必要なユーザーだけに許可するなど、別の経路は別に絞ってください。
  • 外部サービスの障害時は「通す」側に倒しています。止める側に倒すと、事業者の障害がそのまま全員のログイン不能になるためです。二要素認証が土台にあるので、通しても安全性は保たれます。
  • しきい値(0.9、0.6)は例です。事業者ごとにスコアの意味が違うので、最初は記録だけにして誤判定の頻度を見てから、止める基準を決めてください。

運用で外せない3つの備え

締め出し対策:mu-plugin は管理画面から無効化できません。誤判定で管理者がログインできなくなったときは、FTPやSSHで wpdr-login-risk.php の名前を変えるか移動すれば止まります。この手順を、サーバーの接続情報と一緒に手元の手順書に残しておいてください。

試行回数の制限:行動生体判定は総当たり攻撃の対策の代わりになりません。ログイン試行回数の制限(プラグイン、WAF、サーバー設定のいずれか)を別に入れておきます。

ログに秘密を残さない:デバッグのために $_POST をまるごと error_log() に出すと、パスワードが平文でログに残ります。記録するのはユーザーID、スコア、時刻程度にとどめてください。

個人データとプライバシーポリシー

操作のくせは、特定の個人と結び付けて使う以上、個人に関する情報として扱う前提で設計するのが安全です。日本の個人情報保護法の考え方に加え、EU圏の利用者がいる場合、GDPRでは個人を一意に識別する目的で処理する生体データが特別な種類の個人データにあたる可能性があります。該当するかどうかは処理の方法によるため、導入前に法務の確認を受けてください。

最低限、次の点を決めてプライバシーポリシーに書きます。

  • どの事業者に、何の目的で、どんなデータを送るか(国外の事業者なら、その旨)
  • 事業者側とWordPress側の保存期間と削除方法
  • 判定に使っていることの告知と、問い合わせ先

保存期間の考え方は、データ保持ポリシーを決める手順と同じです。WordPressの「プライバシー」設定にある個人データのエクスポート・消去の仕組みに、自分で追加したユーザーメタ(上の例では wpdr_last_login_risk)も含めるかどうかも検討してください。

まとめ

  • 行動生体は操作のくせから本人らしさ・ボットらしさを推定する技術で、誤判定があるため単独の認証手段にはしません。
  • 土台はパスワード+二要素認証。行動生体のスコアは、追加の拒否や警告に使います。
  • 自動攻撃を減らしたいだけなら、Turnstile などのボット判定で足りることが多いです。
  • 組み込むなら authenticate フィルターの優先度20より後で判定し、外部障害時は締め出さない設計にします。
  • mu-plugin の止め方、試行回数の制限、ログに秘密を残さないことを事前に決めておきます。
  • 送るデータ、保存期間、事業者をプライバシーポリシーに明記します。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲