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 ‐ 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-exception
でthrow
する形になっているくらいでしょうか。
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をデプロイする」のも有効かもしれません。