HN 日本語サマリー

← 一覧へ戻る
Web開発

静的サイトの問い合わせフォームにCloudflare WorkersとreCAPTCHA v3を使用する

Using Cloudflare Workers and reCAPTCHA v3 for a Static Site Contact Form (nooshu.com)

18 pointsby speckx5 コメント

要約

この記事では、Cloudflare Pages Functions、reCAPTCHA v3、Resendを使用して、静的サイト(Eleventy製)にスパム対策を施した問い合わせフォームを構築する方法を解説しています。ユーザーはフォームに入力し、reCAPTCHA v3がバックグラウンドで動作して検証トークンを生成します。Cloudflare Workerはこのトークンを検証し、入力内容をバリデーション後、Resend経由でメールを送信します。代替案としてCloudflare Turnstileも紹介されています。

全文翻訳

静的サイトの問い合わせフォームにCloudflare WorkersとreCAPTCHA v3を使用する

Cloudflare Pages Functions、reCAPTCHA v3、Resend、そして代替としてCloudflare Turnstileを使用して、スパムを削減する安全なEleventy (11ty) の問い合わせフォームを構築します。

はじめに

先日、標準の非圧縮HTMLファイルではなく、Brotli圧縮されたHTMLをエッジで配信するためにCloudflare Workerを使用するブログ記事を書きました。この記事に続き、私の問い合わせページで使用しているCloudflare Workerの設定について記事を書くことにしました。スパムを停止するために、フォーム機能にreCAPTCHA v3も統合しました。フォームは非常にシンプルで、訪問者に氏名、メールアドレス、そして私に送りたいメッセージを尋ねます。その後、Workerはこの情報をまとめて、私の個人メールアドレスを指すカスタムメールアドレスにメールを送信します。これにより、サイトから届いたメッセージを簡単にフィルタリングでき、必要に応じて返信することができます。

この投稿は、Sia Karamalegos氏の2024年のNetlifyからCloudflareへの移行に関する投稿に大きく影響を受けていることを付け加えておきます。私はアムステルダムでPerformance.now() 2023で講演した際にSia氏に会うという素晴らしい機会を得ました。彼女は私の講演のMCを務めてくれ、講演前に(率直に言って)極度の緊張でパニックになっていた私を、彼女は本当にサポートしてくれました!

では、なぜ問い合わせフォームにサーバーレスエンドポイントを使用するのでしょうか?前述の通り、このブログは11tyで構築された静的サイトであり、Cloudflare PagesのProプランでホストされています。メールを送信するために必要なバックエンドが存在しないのです。

これまでに使用したソリューション

このブログの問い合わせフォームの仕組みを変更するのは初めてではありません。長年にわたって5つの代替案を数えました!Gitは履歴管理に素晴らしいですね!私のひどい記憶力では、これらすべてを覚えておくことはできなかったでしょう!ですので、Cloudflareを使用していない場合や、Workerの使用を避けたい場合は、以下に完全に実行可能な代替案のリストがあります。

Netlify Forms
  • URL: Netlify Forms
  • コスト: 無料以上(選択するNetlifyのティアによる)

Netlify Formsは、Netlifyでホストされているサイト向けの組み込みフォーム処理サービスです。バックエンドを構築・維持することなく、静的ウェブサイトからのフォーム送信を収集・管理できます。送信はNetlifyダッシュボードで表示したり、メールやWebhookで転送したりできます。CloudflareがNetlify Formsほど簡単に統合・使用できるフォーム設定を提供していないのは残念ですが、もし提供していれば、このブログ記事を書く必要はなかったでしょう!

Cloudflare Pages Functions + MailChannels

当初、NetlifyからCloudflareにホスティングを移行した際にこの設定を使用しました。しかし、MailChannelsがCloudflare Workersユーザー(私)向けの無料メール送信サービスを終了したため、他のソリューションを探し始めました。詳細は後述します。

Formspree.io

Formspreeは、独自のバックエンドサーバーを構築せずにフォーム送信を収集できるホスト型フォームサービスです。フォームをFormspreeにポイントするだけで、送信をメールアドレスや他のAPIエンドポイントに送信してくれます。組み込みのスパム保護機能や、さまざまなワークフローとの連携機能が備わっています。

Formspark.io (旧 Submit Form)

Formsparkは、独自のバックエンドサーバーなしでフォーム送信を受け付けて管理できる、基本的なホスト型フォームバックエンドです。フォームをFormspreeに送信するだけで、データが保存(または転送)され、メール通知、Webhook、スパムフィルタリングのオプションがあります。

Botpoison
  • URL: https://botpoison.com/
  • コスト: 無料プランあり(必要なボット検証数に応じて月額$4以上)

Botpoisonは、ウェブフォーム向けの目に見えないアンチスパムおよびボット対策サービスで、CAPTCHAや追加の手順をユーザーに要求することなく、自動送信をブロックします。フォームのインタラクションを分析し、プルーフ・オブ・ワークチャレンジやレピュテーションチェックを適用して、人間のトラフィックとボットを区別します。Formspark.ioの設定でまだ問い合わせフォームのスパムを受け取っていた際に使用を開始しました。

Resend

Resendは、開発者向けのメール配信サービスで、アプリケーションやウェブサイトからトランザクションメールやブロードキャストメールを送信するためのシンプルなAPIとSMTPインターフェースを提供します。配信可能性、バウンス追跡、抑制リスト、メール分析などの機能を処理するため、開発者はメールインフラストラクチャを自分で管理する必要がなく、多くのプラットフォームと統合できます。Resendは現在、私のメールワークフローの一部となっています。後述します。

セットアップの概要

現在の私のセットアップは以下の通りです:Cloudflare Pages Functions(Worker)+ Google reCAPTCHA v3 + メール送信のためのResend。

このワークフローの概要は以下の通りです。

  1. ユーザーが連絡先フォームに詳細とメッセージを入力します。
  2. reCAPTCHA v3が実行され、短命の検証トークンが追加されます。
  3. フォームは /api/contact にPOSTされます(リポジトリのルートにある functions/api/contact.js のPages Functionを経由)。
  4. Pages Functionは以下の処理を行います。
    • Googleとトークンを検証します。
    • ユーザー入力を検証します。
    • 検証済みの入力をResendを使用してメールで送信します。
  5. ユーザーを「ありがとう」ページにリダイレクトし、メッセージが送信されたことを確認します。

[画像: 上記リストで説明したワークフローの図]

Google reCAPTCHA v3

この投稿で書かれていることの中で、Google reCAPTCHA v3がどのように機能するかを正確に調査したことが、おそらく最も興味深かったです!ユーザーには完全に不可視であり、複雑(しかし論理的)なスコアリングメカニズムに基づいているという事実に本当に感銘を受けました。人間であることを証明しない、非常にイライラする視覚的なCAPTCHAは不要です。「あれはアメリカ人であることを証明するものです!」(この引用の功績は私のものではありません。おそらくTerence Eden氏のものだと思いますが、少なくとも私が読んだのは彼からだったと記憶しています!)

では、reCAPTCHA v3はどのようにして人間かどうかを判断するのでしょうか?それは、視覚的なチャレンジをユーザーに課すのではなく、ユーザーの行動やコンテキストのシグナルを分析し、そのシグナルに基づいてリスクスコアを作成するようです。最大のシグナルは、ユーザーがページ上でどのように行動するかです。

ユーザーセッションの行動分析

reCAPTCHAは、ユーザーがページと対話する際のさまざまなパターンとメトリクスを観察します。これには以下が含まれます。

  • マウスの移動パターン
  • タイピング速度とリズム
  • スクロール行動
  • クリックのタイミングと配置
  • フィールド間またはタブ間のフォーカス移動

私たち人間は、コンピューターを使用する際に少し不器用です。マウスの動きはわずかに曲線を描き、さまよい、無意識に一時停止し、タイピングには時折の躊躇や不均一なリズムが伴います。

一方、Skynetは完璧すぎる傾向があります。マウスの経路はしばしば直線的で、タイミングは予測可能で、実際の人間が管理できるよりもはるかに速くインタラクションが発生する可能性があります。これらの過度に整然としたパターンは、自動化が機能していることを示す強力な兆候であることがよくあります。

ブラウザとデバイスのフィンガープリント

ブラウザのフィンガープリントというと、広告や企業がインターネット全体でユーザーを追跡することと関連付けることが多いです。正直に言って、これはGoogleの話なので、彼らが実際にこれを行っている可能性は非常に高いです!:sad_face: ブラウザとデバイスのフィンガープリントは、reCAPTCHAスクリプトがリクエストの発信元となる環境とデバイスの情報を調べる方法です。

典型的なシグナルには以下が含まれます。

  • ブラウザのバージョンと機能
  • インストールされているフォントとプラグイン
  • 画面解像度とデバイスのプロパティ
  • WebGLおよびCanvasのフィンガープリントシグナル
  • Cookieの利用可能性
  • JavaScript実行の挙動
  • クライアントのタイムゾーン
  • 言語設定

これらのフィンガープリントがどれほど正確であるかに驚くでしょう。各情報がエンティピ(エントロピー)を少しずつ追加するため、デバイスを区別することが容易になります。これらは非常に正確であるため、多くの金融セクターや政府機関が不正検出に使用していることを知っています。

これらすべてを簡単に行えるFingerprintのようなライブラリを見つけるのに、それほど苦労する必要はありません!このリンクをクリックするだけで、あなたに関するどれだけの情報がキャプチャされたか見てみてください!Cookieトラッキングが悪いと思っていたのに!

注: 他にも多くのフィンガープリントライブラリが利用可能です。Fingerprintを特にターゲットにしているわけではありません。

プライバシーとトラッキングの観点からブラウザとデバイスのフィンガープリントに同意できないとしても、ユーザーが人間でありボットではないことを証明する際には役立ちます。特に、ボットはしばしばヘッドレスブラウザや最小限の環境で実行され、実際のユーザーデバイスとは大きく異なるように見えます。

ネットワークとレピュテーションのシグナル

Googleのグローバルネットワークのおかげで、グローバルなレピュテーションデータにもアクセスできます。以下をキャプチャします。

  • IPのレピュテーションと履歴
  • 既知のボットネットワークまたはプロキシの使用
  • データセンターIP範囲
  • IPまたはセッションに関連付けられた過去の不正パターン

世界中でGoogleのネットワークを流れるトラフィックは膨大であり、疑わしいネットワークトラフィックを驚くほど簡単に検出できます。

アクションコンテキスト

Googleは、ユーザーが行おうとしているアクションの予測スコアも付けることができます。reCAPTCHA v3では、開発者は次のようなアクション名でライブラリを呼び出します。

login
checkout
signup
comment

長年にわたる無数の兆の観測から、Googleはすでに人間の典型的なログイン行動(約3〜10秒かかる)を知っています。一方、ボットは1分間に数百回のログインを送信でき、これは自動化の使用に対する大きな警告信号です。

機械学習によるスコアリング

では、Googleはこれらのさまざまなシグナルをどのように使用してスコアリングするのでしょうか?

それらを機械学習アルゴリズムにフィードしてスコアリングを行い、対話しているのが人間かボットかを決定します。

| スコア |
| --- | --- |
| 0.9~1.0 | 非常に人間らしい |
| 0.5 | 不明 |
| 0.0~0.3 | 非常にボットらしい |

この時点で、スコアをどう扱うかはあなたのバックエンド次第です。例えば:

0.8+  リクエストを許可
0.4–0.8  MFAを要求
<0.4  ブロックまたはチャレンジ

私の場合は、Cloudflare Workerがシンプルなサーバーレスバックエンドであり、人間かボットからのフォーム送信かを判断するために以下のコードが含まれています。

// スコアを検証(reCAPTCHA v3は0.0から1.0の間のスコアを返します)
// 低いスコアはボットのような挙動を示します。0.5が一般的な閾値ですが、0.3はより正当なトラフィックを許可します
if (typeof verifyJson.score === "number" && verifyJson.score < 0.3) {
	return new Response(
		JSON.stringify({
			error: "CAPTCHA verification failed",
			details: `Score too low: ${verifyJson.score}`,
		}),
		{ status: 400, headers: { "content-type": "application/json" } },
	);
}

上記のコードは、Googleから返されたスコアが0.3未満(非常にボットらしい)の場合は拒否することを意味します。フィルタリングに対して攻撃的すぎると感じた場合は、この閾値を簡単に変更することもできます。

技術的なセットアップ

reCAPTCHA v3を使用するには、まずGoogle reCAPTCHA管理コンソールでサイトを登録して、2つのキーを取得する必要があります。

「サイトキー」はクライアントサイドで使用され、ユーザーがアクションを実行したときにブラウザがreCAPTCHA検証トークンをリクエストできるようにします。「シークレットキー」はサーバーサイドに保持する必要があり、そのトークンをGoogleと検証するために使用されます。

これは、シークレットを環境変数として安全に保存する必要があることを意味します。私のセットアップでは、Cloudflare Pagesのビルドダッシュボードを通じて行われます。その後、Cloudflare Workerはビルドプロセス中に環境変数からそれを読み取ります。

すべて設定したら、Google reCAPTCHA管理コンソールを使用して以下を行うこともできます。

  • キーを特定のホスト名に制限する
  • ボット検出の厳格さを制御するためのスコア閾値を設定する

フォームに使用されるHTMLはすべてかなり標準的です。できるだけアクセスしやすいようにすることを忘れないでください!セマンティックマークアップがこれを達成する鍵です!方法がわからない場合は、WebAIM.orgのウェブサイトにあるアクセシブルなフォームの作成という優れた投稿があります。私の問い合わせフォームのHTMLは以下のようになります。

<!--
  Contact form: POSTs to the Cloudflare Pages Function at /api/contact.
  Uses novalidate so we can run custom client-side validation and show
  accessible error messages before and after submit.
-->
<form id="fs-frm" action="/api/contact" method="POST" aria-labelledby="page-title" novalidate>
    <!--
	  Error summary: shown when validation fails (client or server).
	  role="alert" announces to screen readers when content appears.
	  tabindex="-1" allows focus to be moved here after submit for accessibility.
	  hidden by default; JS removes the attribute to reveal and populate it.
	-->
    <div id="form-error-summary" class="form-error-summary" role="alert" tabindex="-1" hidden></div>
    <!--
	  Live region for status updates (e.g. "Sending…", success message).
	  role="status" is polite; screen readers announce changes without interrupting.
	-->
    <div id="form-status" class="form-status" role="status"></div>
    <!--
	  Honeypot (spam trap): invisible to users, attractive to bots.
	  - Wrapped in a div with a neutral class (e.g. "abc") so bots don’t skip it.
	  - aria-hidden="true" keeps it out of the accessibility tree.
	  - tabindex="-1" and autocomplete="off" reduce chance a human touches it.
	  - Obscure name so bots don’t recognise it as a honeypot; server rejects if set.
	-->
    <div class="abc" aria-hidden="true">
        <input type="checkbox" name="_its-a-trap!" tabindex="-1" autocomplete="off" />
    </div>
    <!-- Server/worker uses these hidden fields for redirect and email behaviour -->
    <input type="hidden" name="_redirect" value="/contact/thanks/" />
    <input type="hidden" name="_append" value="false" />
    <input type="hidden" name="_email.subject" value="New Message from Nooshu.com" />
    <fieldset>
        <!-- Legend is required for fieldset; visually-hidden keeps layout clean -->
        <legend class="visually-hidden">Contact form</legend>
        <div class="form-group">
            <label for="name" class="required">Full Name:</label>
            <input type="text" id="name" name="name" autocomplete="name" required />
            <span id="name-error" class="form-error" hidden></span>
        </div>
        <div class="form-group">
            <label for="email" class="required">Email:</label>
            <input type="email" id="email" name="email" autocomplete="email" placeholder="hello@example.com" required />
            <span id="email-error" class="form-error" hidden></span>
        </div>
        <div class="form-group">
            <label for="message" class="required">Message:</label>
            <textarea rows="7" id="message" name="message" required></textarea>
            <span id="message-error" class="form-error" hidden></span>
        </div>
    </fieldset>
    <!--
	  reCAPTCHA v3 token: client-side JS runs grecaptcha.execute() and sets
	  this value before submit. Server verifies the token with Google before
	  sending the email.
	-->
    <input type="hidden" name="g-recaptcha-response" value="" />
    <button type="submit">Send Message</button>
</form>

コメントが少ないバージョンはGitHubのGistで利用可能です(新しいタブで開きます)。

上記で使用している問い合わせフォームには、以下の機能があります。

ハニーポットとreCAPTCHAによるスパムブロック

Google reCAPTCHAの仕組みはすでに説明したので、ここでは繰り返しませんが、ハニーポットは最初の簡単な保護層です。ハニーポットの基本的な理論は、実際のユーザーには隠されているが、ページマークアップには存在するシンプルなフォームフィールドです。ソースコードをスキャンするボットは、この隠されたフィールドを含む、見つけたすべてのフィールドに記入することがよくあります。そのフィールドが記入されている場合、ユーザーインターフェース(UI)では見えないため、ボットからの送信であるという強いシグナルとなり、リクエストを即座に拒否できます。

安全な入力を保証するためのクライアントおよびサーバー検証

フォーム検証は2つのレイヤーで処理されます。

  1. クライアントサイドでは、フォームは novalidate をカスタムJavaScriptと共に使用して、必須フィールドをチェックし、名前、メール、メッセージ入力のメール形式を検証します。
  2. サーバーサイドでは、APIは必須フィールドが存在するかどうかを確認し、各フィールドの最大長制限(名前:200文字、メール:320文字、メッセージ:5000文字)を強制し、有効なreCAPTCHAトークンが送信に含まれていることを確認することで、独自の検証を実行します。
アクセシブルなエラー処理とフォーカス管理

フォームは、aria-labelledby を使用してページタイトルと明確に関連付け、`role=