コンテンツにスキップ

Claude CodeでWebアプリのユーザーアカウント管理を始める

⚠️ このハンズオンは制作途中です。動かしてみることはできますが、説明や手順が今後書き換わる可能性があります。気になる点があればフィードバックいただけると助かります。

このハンズオンはD1ハンズオンの続編。前回作った匿名一行掲示板の「会員番号」は、いまブラウザに紐づいている(同じブラウザなら会員1号のまま、別の端末で開くと別人になる)。これを、本人が保存する「ログインキー」に紐づいたアカウントに格上げする。キーを入れれば、どの端末からでも同じ会員番号として続けられる。

メールアドレスもパスワードも使わない。ユーザー登録の画面もいらない。ランダムな鍵(UUID)をひとつ発行して、それを持っている人を本人とみなす、といういちばん軽いアカウント管理を作る。

本記事は Claude Code を例に進めるが、プロンプトを渡す相手はまとめてエージェントと呼ぶ(Codex など他のコーディングエージェントでも、同じプロンプトで進められる)。

前提:作業フォルダは前回のハンズオン(D1ハンズオン)で作った ~/claude/my-bbsBasic認証のハンズオンで管理画面を足していても、いなくても進められる(今回いじるのは利用者側のアカウントで、管理画面とは別)。

1-1. 会員情報を「持ち運べる」ようにする

Section titled “1-1. 会員情報を「持ち運べる」ようにする”

前回の掲示板は、初めて投稿すると会員番号(1, 2, 3…)が払い出され、同じブラウザなら次回も同じ番号が使われた。これは手軽だが、その会員情報(いまは会員番号だけ)がそのブラウザにしか残らない。スマホで開けば別人(別の番号)になるし、履歴を消したりブラウザを変えたりすれば、それまでの自分は失われる。

「どの端末からでも同じ自分でいたい」となったら、会員情報をブラウザではなく、本人が持ち運べる何かに紐づける必要がある。その「何か」がログインキー。初回にランダムな鍵(UUID)を発行して本人に渡し、以降はその鍵を持っている人を「同じ会員」とみなす。別の端末でも鍵を入れれば、同じ会員として続けられる。

  • ログインキーの発行:初めて投稿した人にUUIDのキーを1つ渡す(会員番号もこのタイミングで払い出す)
  • キーで本人を見分ける:投稿のたびに鍵を照合し、どの会員かを特定する
  • 別端末で続ける:保存したキーを入力するか、専用リンクを開けば、どの端末からでも同じ会員情報でアクセスできる
  • キーの管理:自分のキーを表示・コピー・保存でき、漏れたら作り直せる

1-2. 3つの「名前」を分けて考える

Section titled “1-2. 3つの「名前」を分けて考える”

アカウントを作るうえで、いちばん大事なポイントがこれ。各利用者は、役割の異なる3つの識別子を持つ。

識別子役割秘密か
内部の主キーDBが内部で使う不変のID(投稿と会員を結びつけるなど)秘密ではない(が、むやみに見せない)
会員番号(表示ID)「会員1号」のように、みんなに見える名前公開
ログインキー(UUID)本人であることの証明。これを持つ人が本人秘密。他人に渡さない

とくに大事なのは、ログインキーは「会員番号」でも「主キー」でもないこと。ログインキーは合言葉(パスワードのランダム版)であって、DBの行を指すIDとは別系統。だから、

  • ログインキーはサーバーに生のまま保存しない(ハッシュにして保存する。パスワードと同じ扱い)
  • ログインキーはURLやログに出さない(漏れると乗っ取られる)
  • ログインキーは漏れたら作り直せる(新しい鍵に差し替えても、会員番号や投稿はそのまま残る。主キーと別系統だから)

この分離が、今回いちばん大事なところ。

1-3. アカウントは「軽い」代わりに弱点もある

Section titled “1-3. アカウントは「軽い」代わりに弱点もある”

このログインキー方式は、メールもパスワードも要らず手軽な反面、はっきりした弱点がある。鍵が漏れたら、その会員になりすませる(誰が使ったかも分からない)。鍵は本人も暗記できないので、保管も本人任せになる。

だから、この方式が向くのは漏れても致命的でない情報を扱うアカウントに限る(掲示板の投稿、アンケートの回答、趣味の記録など)。決済・健康・他人の個人情報のような重い情報を扱うなら、もっと本格的な認証(→ 最後の章で案内)にする。強いのは「推測されない」ことであって「漏れない」ことではない、と覚えておく。

  1. スキーマ変更:会員番号をログインキー(のハッシュ)に紐づける
  2. バックエンド:キーの発行と照合、投稿を本人に紐づける
  3. フロントエンド:キーの表示・保存・入力・作り直し
  4. デプロイと動作確認:別端末で同じ会員になれるか、鍵を作り直せるか
  5. セキュリティの要点と、次のステップ

ブラウザでの手作業はデプロイの確認くらいで、実装はすべてエージェントに依頼できる。

このハンズオンのプロンプトは、掲示板の標準構成(投稿テーブル posts/会員番号を払い出す仕組み)を前提にしている。D1ハンズオンでエージェントが別の名前や別の作りにしていた場合は読み替える。プロンプトに「既存の会員番号と投稿の仕組みに合わせて」と添えれば、エージェントが既存コードを読んで合わせてくれる。

2. 【データベース】スキーマ変更

Section titled “2. 【データベース】スキーマ変更”

いまの掲示板は「ブラウザごとに会員番号」を持っている。これを「ログインキーごとに会員番号」に変える。会員を表すテーブルを用意し、そこにキーのハッシュ会員番号を持たせ、投稿をその会員に紐づける。

エージェントに相談する。

Claude
いまの掲示板は、会員番号がブラウザごとに払い出されている。
これを「ログインキー方式のアカウント」に変えたい。
- 会員ごとにUUIDのログインキーを発行する。
- サーバーには生のキーを保存せず、SHA-256のハッシュだけを保存する(列は key_hash、UNIQUEにする)。
- 会員番号(連番の表示ID)は、この会員(アカウント)に紐づける。
- 投稿は、どの会員のものかを内部の主キーで紐づける。
まず、既存の会員番号と投稿の仕組みに合わせて、必要なスキーマ変更(新しいマイグレーションファイル)を提案して。
いきなり作らず、設計だけ先に見せて。

設計に納得したら、マイグレーションファイルを作ってもらう。

Claude
その設計で、マイグレーションファイルを作成して。
ファイルは migrations/0003_accounts.sql に保存して(番号は既存の続きに合わせて)。
既存データがある場合の移行(今のブラウザ会員をどう扱うか)も、コメントで方針を書いておいて。

key_hash にはUNIQUE制約を付ける(同じ鍵が2アカウントに割り当たらないように)。生のキーではなくハッシュを保存するのは、DBが漏れても生の鍵は出ないようにするため。パスワードをハッシュで保存するのと同じ考え方(ただしログインキーは長いランダム値なので、パスワードのような「遅いハッシュ+ソルト」までは要らず、SHA-256で足りる。この理屈をユーザーが決めるパスワードに一般化しないこと)。

3. 【バックエンド】キーの発行と照合

Section titled “3. 【バックエンド】キーの発行と照合”

ここが今回の中心。やることは3つ。

  1. 発行:ログインキーを持たない人が初めて投稿したら、新しいアカウントを作り、UUIDのキーと会員番号を払い出す。生のキーはこのときの応答で一度だけ返す(サーバーには以後ハッシュしか残らない)。
  2. 照合:キーを持っている人の投稿では、ヘッダで送られてきたキーをハッシュ照合し、どの会員かを特定する。
  3. 紐づけ:投稿をその会員(内部の主キー)に結びつけ、表示は会員番号で行う。

エージェントに依頼する。

Claude
既存の投稿API(掲示板なら /api/posts)と、さっきのアカウントのスキーマに合わせて、バックエンドを変更して。
リクエストのログインキーは、URLのクエリではなく HTTP ヘッダ x-app-key で受け取る。
処理は次のようにする。
- ヘッダにキーがあれば、SHA-256でハッシュ化して会員を照合する。一致した会員として扱う。
- ヘッダにキーが無い(初めての人)投稿では、新しいアカウントを作り、UUIDのログインキーと会員番号を払い出す。生成した生のキーは、その投稿の応答で一度だけ返す(サーバーにはハッシュだけ保存する)。
- 投稿はその会員に内部の主キーで紐づけ、表示用に会員番号を返す。
- ログインキーをレスポンスやログやURLに残さない(発行時に一度返すのを除く)。
既存のエンドポイント名・テーブル名は、いまのコードに合わせて。

なぜヘッダで受けるのか。キーを ?key=xxx のようにURLのクエリに載せると、サーバーやCDNのアクセスログにキーがそのまま残る(キーは合言葉なので、ログ経由で漏れると乗っ取られる)。HTTPヘッダに載せればURLには出ないので、この漏れを防げる。

キーの照合:送られてきたキーをハッシュ化して、key_hash が一致する会員を探すだけ。生のキーはDBに無いので、DBが漏れても照合の材料(ハッシュ)しか出ない。

4. 【フロントエンド】キーの表示・保存・入力

Section titled “4. 【フロントエンド】キーの表示・保存・入力”

利用者から見た動きを作る。ポイントは、発行されたキーを確実に本人へ渡し、次からも使えるようにすること。

エージェントに依頼する。

Claude
フロントエンド(掲示板の画面)を、ログインキー方式に合わせて変更して。
- ログインキーは localStorage に保存し、投稿などのAPI呼び出しでは毎回 x-app-key ヘッダに載せて送る。
- 初めて投稿してキーが発行されたら、「キーを保存」する画面を1回だけ挟む。要件は下記。
- すでにキーを持っている人が別の端末で続けられるよう、「ログインキーを入力」する欄も用意する。
- 専用リンク(別端末で開くと自動でログインするURL)は #key=(URLのフラグメント)で作る。読み込み時に location.hash からキーを読んで localStorage に保存し、すぐ hash を消す。?key=(クエリ)は使わない。
「キーを保存」画面の要件:
- コピー・専用リンクのコピー・メール(mailto)で保存できるボタンを並べる。
- 「このキーが無くすと二度と同じ会員に戻れない」旨を明記する。
- パスワードマネージャにも拾わせるため、保存はフォーム(form)のsubmitで行い、ユーザー名欄(会員番号など、readonlyでよい)とパスワード欄(キー、autocomplete="current-password")を持つ。ただし保存の主役はコピー・メールなど手動の手段にする(ブラウザによってはパスワードマネージャの保存提案が出ないため)。

キーはどこで見られる? ログイン中の端末は、キーを localStorage に持っている。だから「自分のキーを表示・コピー」する画面は、その localStorage から読んで表示するだけでよく、サーバーに問い合わせる必要はない(サーバーはハッシュしか持っていない)。表示のためにサーバーへキーを取りに行かないこと。

保存を強く促す理由:このキーが唯一の本人確認手段。無くして、かつどの端末にも残っていないと、その会員には二度と入れない(メールのような復旧手段が無い)。だから発行直後の「キーを保存」ステップを飛ばさせない。とくに localStorage は永続的な保存ではないという前提が大事。iPhone・iPad のブラウザ(Safari も Chrome も中身は同じ WebKit)は、そのサイトをしばらく(7日ほど)開かないでいると localStorage を自動で消すことがある。だから「ログインしたまま/ブラウザに入れっぱなしだから大丈夫」は通用しない。キーは必ず別の場所(自分宛てメール・パスワードマネージャなど)にも保存してもらう。

鍵が漏れたかもしれないときに、本人が新しい鍵に差し替えられるようにする。

Claude
ログイン中の会員が、自分のログインキーを作り直せるようにして。
- 新しいUUIDを生成してハッシュを保存し直し、古いキーは無効にする(会員番号や投稿はそのまま残す)。
- 新しいキーは発行時に一度だけ表示し、各端末で入れ直してもらう。
- 「作り直すと、古いキーを保存した端末は入れなくなる」旨を確認してから実行する。

「作り直し」と「紛失時の復旧」は別物。作り直し(ローテーション)は、いまログインしている本人が「漏れたかも」と思ったときに、新しい鍵へ差し替える操作。一方、鍵を無くして締め出された状態からの復旧はできない(本人だと確かめる手段が無いため)。ここがこの軽量方式の限界。作り直せるのは会員番号や投稿が主キーと別系統だから(鍵の列を書き換えるだけで、データは無傷)。

変更をコミット・push する(本番DBへのマイグレーション適用も忘れずに。→ D1ハンズオンのデプロイ手順)。

Claude
今回の変更をコミットして、本番DBにマイグレーションを適用してからデプロイして

デプロイできたら、本番URLで次を確認する。

  • 初めて投稿すると会員番号が付き、「キーを保存」画面が出る。キーをコピーしておく。
  • 別の端末(またはシークレットウィンドウ)で開くと、最初は別人(キーが無い)。「ログインキーを入力」に、さっきのキーを貼ると、同じ会員番号になる。
  • 専用リンク(#key=…)を別端末で開くと、自動で同じ会員になる。URLからキーが消えることも確認する。
  • キーを作り直すと、古いキーでは入れなくなり、新しいキーでは同じ会員のまま続けられる。
  • ブラウザの開発者ツールの Network で、投稿リクエストのURLにキーが載っていない(ヘッダに載っている)ことを確認する。
  • 生のキーはサーバーに保存しない(ハッシュのみ)。DBが漏れても、生の鍵は出ない。
  • キーはヘッダで送る。URL(クエリ)に載せない。載せるとサーバー/CDNのログに残る。専用リンクは #key=(フラグメント=サーバーに送られない)で。
  • キーをログ・エラー画面・レスポンスに出さない(発行時に一度返すのを除く)。
  • 他人のデータは「無い」ように扱う。「このキーではアクセスできない」ではなく「見つからない(404)」を返すと、存在を探られにくい。
  • 強いのは推測耐性であって漏洩耐性ではない。UUID(v4)のランダムな部分は122ビット(約 5.3×10³⁶ 通り)あり、仮に毎秒1兆回のペースで総当たりしても全部試すのに宇宙年齢の約1200万倍かかる。つまり当てる(推測する)のは事実上不可能。だが漏れたら終わり。漏れても致命的でない情報に限って使う

今回はアカウントの土台(本人を見分けて、持ち運べるようにする)まで。ここまでできると、次のような発展が同じ仕組みで作れる。

  • マイページ:会員が自分の投稿だけを一覧できる画面。投稿を会員に紐づけてあるので、「自分のキーで、自分の投稿を返すAPI」を足すだけ。SNSのプロフィールページのように育てられる。
  • 表示名・アイコン:会員番号のほかに、本人が決める表示名やアイコンを持たせる。
  • 本人だけの操作:自分の投稿だけ編集・削除できるようにする(本人のキーで照合して許可)。

そして、この軽量アカウントの弱点(漏れたら終わり/復旧できない)が気になるなら、次のステップがある。

「誰でも使える掲示板」から「自分のアカウントで続けられるアプリ」へ。ここがその入り口になる。

  • ログインキーは「アカウントそのもの」。会員番号(見せる番号)や内部の主キー(DBのID)とは別物、と最後まで混同しない。
  • キーが漏れたら作り直し(ローテーション)で差し替える。ただし攻撃者も同じキーで作り直せてしまう(先にやられると締め出される)ので、非センシティブ前提での割り切り。
  • どの端末にもキーが残っておらず、本人も控えていない場合、その会員には戻れない。しかも localStorage は放置で消えることがある(とくに iPhone・iPad)ので、「ログインしたままにしておけば安心」は当てにできない。だから発行時に必ずキーを別の場所(自分宛てメール等)に控えることを利用者に促す。
  • localStorage はオリジン単位。プレビュー用のデプロイURLは本番と別オリジンなので、キーも別扱いになる。案内は本番URLで。