htmx 逆引きレシピ
CSRFトークン等をヘッダ/パラメータに付けるには?
公開日:
最終更新日:
管理画面の保存APIでは、CSRFトークンや画面固定値など、フォーム入力以外の情報も一緒に送る場面がよくあります。
そこで、htmxの hx-headers / hx-vals / hx-include を使って、必要な情報を“安全にまとめて送信”します。
このページでは、CSRF対策・画面固定値を付与・フォーム外の値も同送を1つのデモで実現します。
送信値を hx-params で絞りつつ、運用しやすい形に整理するのがポイントです。
使用するhtmx属性
hx-post:POSTで保存し、結果HTMLを返して差し替える(保存/更新向き)hx-headers:リクエストヘッダを追加(例:X-CSRF-Tokenを送ってCSRF対策)hx-vals:フォーム値とは別に、固定パラメータを追加して送信(例:画面ID/用途など)hx-include:フォーム外の要素も送信に含める(例:ツールバーのselect/hiddenなど)hx-params:送信するパラメータを絞る(必要なキーだけにして安全&デバッグしやすくする)hx-encoding:送信形式を指定(例:multipart/form-dataでFormData送信)
利用シーン
- 「CSRF対策」:セッション型トークンをヘッダ送信し、保存APIの不正リクエストを防ぎたい
- 「画面固定値を一緒に送る」:画面IDや用途(保存/承認など)を固定で付与し、ログや分岐に使いたい
- 「フォーム外の値も含めたい」:ヘッダー/ツールバーの条件(テナント・操作ユーザーなど)を同時に送りたい
CSRFトークン等をヘッダ/パラメータに付けるには?
このデモは CSRF対策 と、画面固定値・フォーム外の値をまとめて送る例です。
hx-headers / hx-vals / hx-include / hx-params を同時に使います。
HTML(一部PHP)
<?php
// CSRFトークンが未生成なら作る
if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
// ランダムなトークンを生成する
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));
}
// CSRFトークンを取り出す
$csrfToken = (string)$_SESSION['csrf_token'];
?>
<div class="DEMO">
<h4>CSRFトークン等をヘッダ/パラメータに付けるには?</h4>
<section>
<h5>フォーム外の値(固定値/外部値)</h5>
<div class="CARD" style="display:grid; gap:.6rem;">
<p class="HTMX-NOTE">
ここはフォームの外です。<br>
でも <code>hx-include</code> で一緒に送れます。
</p>
<!-- フォーム外の固定値(hidden) -->
<input id="DEMO_CSRF_TENANT" type="hidden" name="tenant_id" value="TENANT-001">
<!-- フォーム外の操作値(select) -->
<label id="DEMO_CSRF_TOOLBAR">
操作ユーザー(フォーム外)
<select name="acting_as">
<option value="tanaka">tanaka</option>
<option value="sato">sato</option>
<option value="suzuki">suzuki</option>
</select>
</label>
<p class="HTMX-NOTE">
送信に含めるのは「tenant_id / acting_as」だけにして、意図しない値は送らないのが安全です。
</p>
</div>
</section>
<section class="MT2rm">
<h5>保存(CSRFヘッダ+固定値+フォーム外の値)</h5>
<form
id="DEMO_CSRF_FORM"
class="FORM"
method="post"
hx-post="/htmx/demo/_csrf_attach_save.php"
hx-target="#DEMO_CSRF_RESULT"
hx-swap="innerHTML"
hx-headers='{"X-CSRF-Token":"<?= htmlspecialchars($csrfToken, ENT_QUOTES, "UTF-8") ?>"}'
hx-vals='{"screen_id":"csrf-attach-demo","request_kind":"save"}'
hx-include="#DEMO_CSRF_TENANT, #DEMO_CSRF_TOOLBAR select"
hx-params="title,amount,tenant_id,acting_as,screen_id,request_kind"
hx-encoding="multipart/form-data"
>
<label>
タイトル
<input type="text" name="title" value="" placeholder="例:申請:端末貸与">
</label>
<label>
金額
<input type="text" name="amount" value="" placeholder="例:12000">
</label>
<button class="BTN is-ok" type="submit">
保存する
</button>
<p class="HTMX-NOTE">
CSRFトークンはヘッダに、画面固定値は <code>hx-vals</code> に、フォーム外の値は <code>hx-include</code> にまとめると運用が楽です。
</p>
</form>
<div id="DEMO_CSRF_RESULT" class="CARD">
<p class="HTMX-NOTE">ここに保存結果が表示されます。</p>
</div>
</section>
</div>
PHP
<?php
// 型を厳密に扱う
declare(strict_types=1);
// セッションを開始する(CSRF検証に使う)
session_start();
// HTMLとして返す
header('Content-Type: text/html; charset=UTF-8');
// HTMLエスケープ関数を用意する
function h(string $s): string {
// 特殊文字をエスケープする
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// セッションのCSRFトークンを取得する
$expectedToken = (string)($_SESSION['csrf_token'] ?? '');
// ヘッダからCSRFトークンを取得する(X-CSRF-Token)
$sentToken = (string)($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
// CSRFトークンが一致するか判定する
$isOk = ($expectedToken !== '' && hash_equals($expectedToken, $sentToken));
// CSRFがNGならエラー表示する(デモなので200で返す)
if (!$isOk) {
// エラーを返す
echo '<div class="FORM-RESULT is-ng">';
echo '<strong>CSRFトークンが不正です。</strong>';
echo '<div class="HTMX-NOTE">ヘッダの <code>X-CSRF-Token</code> を確認してください。</div>';
echo '</div>';
exit;
}
// タイトルを受け取る
$title = (string)($_POST['title'] ?? '');
// 金額を受け取る
$amount = (string)($_POST['amount'] ?? '');
// フォーム外の固定値(tenant_id)を受け取る
$tenantId = (string)($_POST['tenant_id'] ?? '');
// フォーム外の値(acting_as)を受け取る
$actingAs = (string)($_POST['acting_as'] ?? '');
// hx-valsの固定値(screen_id)を受け取る
$screenId = (string)($_POST['screen_id'] ?? '');
// hx-valsの固定値(request_kind)を受け取る
$requestKind = (string)($_POST['request_kind'] ?? '');
// 前後空白を除去する
$title = trim($title);
// 前後空白を除去する
$amount = trim($amount);
// タイトルが空なら補正する
if ($title === '') $title = '申請:新規作成';
// 金額が空なら補正する
if ($amount === '') $amount = '0';
// tenant_idが空なら補正する
if ($tenantId === '') $tenantId = 'TENANT-UNKNOWN';
// acting_asが空なら補正する
if ($actingAs === '') $actingAs = 'unknown';
// screen_idが空なら補正する
if ($screenId === '') $screenId = '(none)';
// request_kindが空なら補正する
if ($requestKind === '') $requestKind = '(none)';
// デモ用に少し待つ(通信してる感)
usleep(500000);
// 新しいIDを作る
$newId = random_int(1000, 99999);
// 現在時刻を作る
$now = date('H:i:s');
?>
<div class="FORM-RESULT is-ok">
<strong>保存しました。</strong>
<div>ID:<?= h((string)$newId) ?></div>
<div>タイトル:<?= h($title) ?></div>
<div>金額:<?= h($amount) ?></div>
<hr style="margin:.8rem 0; border:0; border-top:1px dashed rgba(0,0,0,.2);">
<div><strong>一緒に送られた値</strong></div>
<div>tenant_id(フォーム外) :<?= h($tenantId) ?></div>
<div>acting_as(フォーム外) :<?= h($actingAs) ?></div>
<div>screen_id(hx-vals) :<?= h($screenId) ?></div>
<div>request_kind(hx-vals) :<?= h($requestKind) ?></div>
<div>時刻:<?= h($now) ?></div>
<p class="HTMX-NOTE">
CSRFトークンはヘッダで検証し、画面固定値/フォーム外の値も同時に受け取れています。
</p>
</div>
デモ
CSRFトークン等をヘッダ/パラメータに付けるには?
フォーム外の値(固定値/外部値)
ここはフォームの外です。
でも hx-include で一緒に送れます。
送信に含めるのは「tenant_id / acting_as」だけにして、意図しない値は送らないのが安全です。
保存(CSRFヘッダ+固定値+フォーム外の値)
ここに保存結果が表示されます。
解説
HTMLでやっていること
hx-headersでX-CSRF-Tokenを付け、保存リクエストにCSRFトークンを同送します。hx-valsでscreen_idやrequest_kindを付け、画面固定値を毎回送ります。hx-includeでフォーム外のtenant_id(hidden)やacting_as(select)も送信に含めます。hx-paramsで送信キーを絞り、意図しない値が混ざらないようにします。hx-encoding="multipart/form-data"にして、FormData送信に寄せた構成にします。
PHPでやっていること
- セッションでCSRFトークンを保持し、リクエストヘッダの
X-CSRF-Tokenと比較して検証します。 - CSRFが不一致なら処理を止め、エラーHTMLを返します(保存しない)。
- 一致していれば、POST値(フォーム+フォーム外+固定値)をまとめて受け取り、保存結果として表示します。
ポイント:「安全のための値(CSRF)」と「画面文脈の値(固定値/外部値)」を分けて送ると、設計が整理しやすくなります。
htmxなら、フォームを汚さずに hx-headers / hx-vals / hx-include でスマートに実現できます。
※デモでは、便宜的に自作のCSSを使用してます。
参考リンク
このページの著者
経験:Webアプリ/業務システム
得意:PHP・JavaScript・MySQL・CSS
個人実績:フォーム生成基盤/クイズ学習プラットフォーム 等
詳しいプロフィールはこちら! もちもちみかんのプロフィール