HonoでCloudflare Turnstileのクライアント・サーバー側の処理を実装する

このテキストは、個人的な興味からCloudflare Turnstileの組み込みを試した経験について述べています。Cloudflareエバンジェリスト亀田さんの公開資料が参考になり、TurnstileをHonoで動かすことに挑戦しました。ローカル環境で動かすためにWranglerを使用し、Turnstileのキーを設定します。Honoを使用して認証ページを作成し、Honoでサーバー側の検証処理も行います。Honoの使い方には慣れが必要ですが、ルーティングの部分は書きやすくなりました。HonoとReactを組み合わせるか、Cloudflare PagesのfunctionsとしてHonoをデプロイする方法も考えられます。

広告ここから
広告ここまで

目次

    個人的な興味もあって、Cloudflare Turnstileの組み込みを試していました。Cloudflareエバンジェリスト亀田さんが公開されている資料がとても参考になったのですが、「どうせなら、これをHonoで動かしてみよう」となったので挑戦してみました。

    ローカル環境で動かす準備

    手元で手早く終わらせるため、Wranglerを使った設定を行います。

    キーの設定

    Turnstileのサイトキーとシークレットキーを、.dev.varsファイルに設定します。

    このファイルをプロジェクトのルートに配置することで、wrangler devコマンドを利用した開発環境内で環境変数が使えます。

    SITE_KEY = '0xxxxxxxxx8';
    TURNSTILE_SECRET_KEY = 'xxxx'

    Cloudflare Workers上で利用するには、wrangler secret putを使いましょう。

    % wrangler secret put SITE_KEY
    ------------------
    ? Enter a secret value: › ************************

    認証のためのページを、Hono(とhono/jsx)で作成する

    元の資料では、HTMLファイルを使用していました。今回はあえてhono/jsxを利用してJSXで書いてみましょう。

    import { Hono } from 'hono'
    import { FC } from 'hono/jsx';
    import { html } from 'hono/html';
    
    type Bindings = {
        SITE_KEY: string;
    }
    const app = new Hono<{
        Bindings: Bindings
    }>()
    
    
    const Top:FC<Bindings> = ({SITE_KEY: siteKey}) => {
        
        return (
            <html>
                <head>
                    <title>Turnstile &dash; Dummy Login Demo</title>
                    <style>
                        {html`
                    html,
                    body {
                        height: 100%;
                    }
    
                    body {
                        display: flex;
                        align-items: center;
                        justify-content: center;
                        padding-top: 40px;
                        padding-bottom: 40px;
                        background-color: #fefefe;
                    }
                    form > * {
                        margin-bottom: 20px;
                    }
                        `}
                    </style>
                </head>
                <body>
                    <form id="payment-form" action="/submit" method="POST">
                        <div class="cf-turnstile" data-sitekey={`${siteKey}`}></div>
                        <button class="w-100 btn btn-lg btn-primary" type="submit" disabled>Sign in</button>
                    </form>
                    {html`
                        <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=_turnstileCb" async defer></script>
                        <script>
                            let turnstileToken = '';
                            let submitButon;
                            function _turnstileCb() {
                                turnstile.render('.cf-turnstile', {
                                    callback: function(token) {
                                        turnstileToken = token;
                                        submitButon = document.querySelector("button[type='submit']");
                                        submitButon.removeAttribute('disabled');
                                    },
                                })
                            }
                        </script>
                        `}
                </body>
            </html>
        )
    }
    
    app.get('/', (c) => {
        return c.html(<Top {...c.env}/>)
    })
    
    export default app
    

    app.getで作成したJSXコンポーネントを、読み込んでいます。同時にc.envの値をコンポーネントに渡すことで、環境変数をアプリ側に渡すことも実現できました。

    レスポンスのHTMLは、c.htmlの引数にJSXコンポーネントを渡すことでHono側がSSRしてくれます。

    JSXとはいえ、SSRするhtmlを記述するのがメインになるため、Reactなどを使わない場合は、html``を利用しないとクライアント側で実行するJavaScriptが書けない点に注意です。

    サーバー側の検証処理も、Honoで記述する

    Honoを利用するメリットの1つは、「パスやメソッドごとに処理を記述しやすいこと」です。

    app.post(‘submit’)を追加して、Form送信時の処理を追加しましょう。

    
    import { HTTPException } from 'hono/http-exception';
    
    type TurnstileResult = {
        success: boolean;
        challenge_ts: string;
        hostname: string;
        'error-codes': Array<string>;
        action: string;
        cdata: string;
    }
    app.post('/submit', async c => {
        const body = await c.req.json();
        const ip = c.req.header('CF-Connecting-IP')
    
        const formData = new FormData();
        formData.append('secret', c.env.TURNSTILE_SECRET_KEY);
        formData.append('response', body.turnstileToken);
        formData.append('remoteip', ip || '');
        const turnstileResult = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
            body: formData,
            method: 'POST',
        });
        const outcome = await turnstileResult.json<TurnstileResult>();
        if (!outcome.success) {
            throw new HTTPException(401, {
                message: JSON.stringify(outcome)
            });
        }
    
        return new Response('Turnstile token successfuly validated. \n' + JSON.stringify(outcome));
    });
    

    書いている内容は、ほとんど元のワークショップと同じです。失敗時のエラーレスポンスを、hono/http-exceptionthrowする形になっているくらいでしょうか。

    curlで直接APIを呼び出すと、Turnstileのエラー結果が取得できます。

    $ curl http://127.0.0.1:8787/submit -XPOST -H 'Content-Type: application/json' -d '{"test": true}' | jq .
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
    100    86  100    72  100    14   2695    524 --:--:-- --:--:-- --:--:--  3739
    {
      "success": false,
      "error-codes": [
        "invalid-input-response"
      ],
      "messages": []
    }

    やってみた感想

    Honoを使うことで、ルーティングまわりがかなり書きやすくなりました。一方でhono/jsx単体でフロントエンドを作る場合、クライアント側のJavaScriptコードが混在するため、ちょっと慣れるのに時間がかかるかもしれません。個人的には、hono/jsxとReactを組み合わせて、サーバー側の処理はHono・クライアント側はReactに分離させた方が良いかなと思いました。もともと、「SSRのためのJSXですよ」と明言されていますしね。

    あるいは、Hono作者のyusukeさんが紹介されているように、「Cloudflare Pagesのfunctionsとして、Honoをデプロイする」のも有効かもしれません。

    参考

    https://zenn.dev/kameoncloud/articles/cdf8f67bd8ce6f

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark