Site icon image rpine lab Blog

プログラミングや電子工作系の記事を投稿しています。

🤖 Next.jsでCloudflare Turnstileを使ったサイトを実装してみる

Cloudflare TurnstileはCloudflareが提供しているCAPTHCA系のサービスです。

このサービスの最大の特徴は、reCAPTCHAとは異なりユーザがクイズを解くことなく自動的に人間であるかを判定してくれる点にあります。また、最大10個のウィジェットまでは原則無料で利用可能です。

Image in a image block
ページを読み込んだ直後に自動でチャレンジが実行
Image in a image block
自動でチャレンジの成否が判定

今回は、このTurnstileを使ってNext.jsでサイトを実装してみます。

サイト構成

本記事では、以下の技術スタックを使用します。

  • フレームワーク:Next.js 14
  • フロントエンド:Next.js 14 (App Router) ・ React 18
  • バックエンド:Server Actions (Next.js)

Cloudflareダッシュボード上での設定

  1. Cloudflareのダッシュボードにログインして、サイドバーからTurnstileの管理ページに行きます。
  2. 「サイトを追加」を押してサイトを追加します。
    • ドメイン:Turnstileを導入するサイトのドメインを入力します。localhostを追加すると、ローカル開発時(next dev)にも使えます。(本番用とサイトを分けて追加することを推奨)
    • ウィジェットモード:デフォルトの管理対象(Managed)にします。(Cloudflareの推奨設定)
  3. 「作成」をクリックし、表示されるサイト キーとシークレット キーをコピーしておきます。これらは後からでも管理画面で確認できます。

事前設定(環境変数)

ウィジェットの設置に必要なサイトキー(NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY)とシークレットキー(CF_TURNSTILE_SECRET_KEY)を環境変数に設定します。開発環境では、localhostをドメインに追加したサイトのキーを使うか、テスト用のキーが利用できます。

サイトキーはウィジェットのHTMLタグに埋め込んでクライアントに公開するので、頭にNEXT_PUBLIC_を付けた名前にしています。(参考:公式ドキュメント

NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY=ここにサイトキー
CF_TURNSTILE_SECRET_KEY=ここにシークレットキー
.env.development.local(開発環境の場合)

フロントエンド(Next.js App Router)

Turnstileを利用するには、Webページにウィジェットを設置する必要があります。

Reactの場合は以下のライブラリを利用することで簡単に導入できますが、ここでは手動で設置します。

"use client";

import { useEffect } from "react";
import { submit } from "./actions";
import { useFormState } from "react-dom";
import Script from "next/script";

export default function Page() {
  const [state, formAction, pending] = useFormState(submit, null);

  // トークンは再利用できないので送信の度に雑にuseEffectでウィジェットをリセットしておく
  useEffect(() => {
    try {
      // @ts-ignore
      turnstile.reset();
    } catch { }
  }, [state]);

  return (
    <form action={formAction}>
      <Script
        src="https://challenges.cloudflare.com/turnstile/v0/api.js"
        async
        defer
      />
      <div>
        <label htmlFor="email">Email: </label>
        <input id="email" type="text" name="email" />
      </div>
      <div>
        <label htmlFor="password">Password: </label>
        <input id="password" type="password" name="password" />
      </div>
      {/* ↓ これがTurnstileウィジェットになる */}
      <div
        suppressHydrationWarning
        className="cf-turnstile"
        data-sitekey={process.env.NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY}
      />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button type="submit" aria-disabled={pending}>
        {pending ? "送信中..." : "送信"}
      </button>
    </form>
  );
}
src/app/page.tsx

バックエンド(Server Actions)

トークンの検証関数

以下は、公式ドキュメントを参考にしたTurnstileのトークンを検証する関数です。クライアントIPの取得部分はCloudflare Workers / Pagesが前提なので適宜変える必要があります。

import { headers } from "next/headers"

/**
 * Turnstileのトークンを検証する
 * @param formData Turnstileのトークンを含むフォームデータ
 * @returns 検証結果(true: 成功, false: 失敗)
 */
export async function verifyTurnstile(formData: FormData) {
  const token = formData.get('cf-turnstile-response') as string
  const ip = headers().get('CF-Connecting-IP') as string

  const verifyFormData = new FormData()
  verifyFormData.append('secret', process.env.CF_TURNSTILE_SECRET_KEY!)
  verifyFormData.append('response', token)
  verifyFormData.append('remoteip', ip)

  const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
  const result = await fetch(url, {
    body: verifyFormData,
    method: 'POST',
  })

  const outcome = (await result.json()) as {
    success: boolean
    'error-codes': string[]
  }
  if (!outcome.success) {
    console.error('Turnstile validation failed', {
      errorCodes: outcome['error-codes'],
    })

    return false
  }

  return true
}
src/app/turnstile.ts

Server Actions によるフォームの送信処理

フォームの送信処理はNext.js 14のServer Actionsを利用します。最低限結果を返せるようにuseFormStateで使える形にしています。

"use server";

import { verifyTurnstile } from "./turnstile";

type FormState = null | { success: boolean; message?: string };

export async function submit(_prevState: FormState, formData: FormData): Promise<FormState> {
  const verificationResult = await verifyTurnstile(formData)

  if (!verificationResult) {
    return {
      success: false,
      message: '検証に失敗しました。',
    }
  }

  // 送られたデータを使った適当な処理をここに書く
  const email = formData.get('email');
  const password = formData.get('password');

  return { success: true, message: `検証に成功しました。(email: ${email})`};
}
src/app/actions.ts

作ったページ(検証後)

Image in a image block

まとめ

本記事では、Cloudflare Turnstileを利用したBOT対策をNext.jsで実装しました。公式ドキュメントを見ながら進めることで、導入は比較的容易でした。

Turnstileを使うことで、従来のCAPTCHAと比べてユーザー体験を損なわずにいい感じのBOT対策が可能です。また、無料プランでも十分な機能が提供されているので、小規模なプロジェクトや個人開発にも適しています。

広告