Appearance
ログイン・認証・パスワード設計
| 項目 | 内容 |
|---|---|
| Issue | #2594 |
| 親 Issue | #2381 有料記事の体制を整える |
| 関連 Issue | #2390 / #2936 / #2974 |
| ステータス | 設計 |
| 最終更新 | 2026-07-28(#2974: 登録成功の IP 単位レート制限) |
1. 概要
有料記事 Phase 2 に向け、サイト独自デザインの会員登録・ログイン・パスワードリセット画面を提供する。認証の中核(パスワードハッシュ化、セッション Cookie、パスワードリセットトークン)は WordPress core に全面委譲し、自前で暗号化アルゴリズムやセッション管理を実装しない。
2. パスワード保管・検証方針
2.1 WordPress core への委譲(採用)
| 処理 | 使用する WordPress 関数 | 保管先 |
|---|---|---|
| パスワードハッシュ化 | wp_hash_password( $plaintext ) | wp_users.user_pass |
| パスワード検証 | wp_check_password( $plaintext, $hash ) | — |
| ログイン | wp_signon( $credentials, $secure ) | Cookie(core が設定) |
| ユーザー作成 | wp_insert_user( $userdata ) | wp_users + wp_usermeta |
| パスワードリセット要求 | retrieve_password( $user_login ) | メール + user_activation_key |
| リセットキー検証 | check_password_reset_key( $key, $login ) | — |
| パスワード更新 | reset_password( $user, $new_pass ) | wp_users.user_pass |
2.2 自前ハッシュ実装を行わない理由
- セキュリティ: WordPress core は bcrypt(PHP
password_hash)等の現行ベストプラクティスを採用し、バージョンアップでアルゴリズム移行(rehash)も core が担当する。 - 互換性: 既存 WP 管理画面・REST API・プラグイン(WooCommerce 等)と同一のユーザー基盤を共有できる。
- 監査範囲の縮小: 自前実装すると暗号化・タイミング攻撃対策・ソルト管理が自前責任となり、決済連携と合わせてリスクが増大する。
- PCI DSS: カード情報は Stripe Checkout 等に委譲するため、自サイトで扱う機密はパスワードのみ。WP core の実績あるハッシュで十分。
2.3 平文パスワードの取り扱い
| ルール | 理由 |
|---|---|
| 平文パスワードを DB・ログ・セッションに保存しない | 漏洩時の被害最小化 |
| リクエスト処理中のみメモリ上で保持 | wp_hash_password 呼び出し直後に破棄 |
| HTTPS 必須(本番) | 通信経路での平文漏洩防止 |
3. カスタムロール・Capability 設計
3.1 ロール定義
| ロール | 用途 | 付与方法 |
|---|---|---|
member | 一般会員(有料コンテンツ購入者) | 会員登録完了時に wp_insert_user の role で付与 |
subscriber | 不採用(member に統一) | — |
member ロールの Capability(実装時に add_role() で登録):
| Capability | 説明 |
|---|---|
read | ログイン済み一般権限(WP 標準) |
read_paid_article | 購入済み有料記事の全文閲覧(カスタム) |
3.2 閲覧権限の判定フロー
有料記事(paid_article CPT)の閲覧可否は Capability 単体では決まらない。Service 層で以下を合成判定する。
判定優先順位:
current_user_can( 'edit_post', $post_id )→ 編集者は常に全文PaidArticlePurchaseRepository::exists( $user_id, $post_id )→ 単号購入済みMemberSubscriptionRepository::has_active_plan( $user_id, $plan_key )→ サブスク加入(将来)- 上記いずれも該当なし → リードのみ(#2384 推奨)
4. 会員登録フロー
サイト独自の登録フォームから WordPress ユーザーを作成する。手動確認手順は 会員登録 シナリオテスト手順 を参照する。
4.0 通しフロー概要(ユーザー視点)
登録 → 確認メール → メール認証 → ログイン → マイページまでの正常系。登録直後は自動ログインしない。メール未認証でもログイン自体は可能だが、有料記事購入はゲートされる(詳細は 有料記事決済 シナリオテスト手順 へ委譲)。
4.0.1 画面遷移図
会員登録まわりの画面遷移は、正常系と主要なガード分岐を 1 枚で追えるように Mermaid の flowchart で管理する。シーケンス図は処理主体の責務確認に使い、この図はユーザーが表示される画面とリダイレクト先の確認に使う。
補足:
- 登録成功時は
/member-login/?registered=1へ移動し、確認メール案内を表示する。ここではログイン状態にしない。 /member-verify-email/?uid=&token=の成功時(未ログイン)は/member-login/?verified=1へ移動し、確認完了メッセージを表示する。- ログイン中に自分自身の
uid付き確認リンクを開いた場合は検証を実行し、成功時は/member-profile/?verified=1へ移動する(マイページでのメールアドレス変更後の再確認を想定)。 - メール未認証ユーザーでも
/member-login/からログインできる。ただし有料記事購入開始時は/member-verify-email/?email_not_verified=1へ誘導する。 - ログイン後の
redirect_toは同一オリジンのみ許可し、未ログインの購入導線から戻る場合は購入対象記事へ戻す。
4.1 登録 POST シーケンス(実装)
ログイン画面側は registered=1 を受け取り、「ご登録ありがとうございます。確認メールを送信しました…」を表示する(MemberLoginController / MemberAuthCopy)。
4.2 入力バリデーション
| 項目 | ルール |
|---|---|
| メール | 形式チェック、email_exists() で重複拒否 |
| パスワード | 最小 8 文字(WP デフォルトに合わせる)、確認入力一致 |
| 表示名 | 必須、サニタイズ(sanitize_text_field)、重複禁止(登録・プロフィール更新共通。get_users() で候補取得後に display_name を trim 済み入力と厳密一致(===)で判定。大小文字・全半角の追加正規化はしない。DB 一意制約はないため同時更新時はベストエフォート) |
| 利用規約同意 | チェック必須 |
4.3 メール確認
| 項目 | 方針 |
|---|---|
| トークン | メール/URL は平文(ランダム 32 バイト hex)。meta member_email_verify_token には hash_hmac( 'sha256', $token, wp_salt( 'auth' ) ) のみ保存する |
| 有効期限 | 24 時間(meta member_email_verify_expires_at) |
| 確認後 | member_email_verified = 1、トークン meta 削除、購入可能状態に遷移 |
| 既存平文 | ハッシュ保存導入前に発行された未使用平文トークンは互換比較せず無効。ユーザーは確認メール再送で新トークンを取得する |
| 画面 URL | /member-verify-email/?uid=&token= |
| 再送 | 同画面のフォーム POST。存在有無・確認済みを漏らさない汎用メッセージ |
4.3.1 確認リンク(GET)シーケンス
ログイン画面側は verified=1 を受け取り、「メールアドレスの確認が完了しました。ログインしてご利用ください。」を表示する(MemberLoginController / MemberEmailVerificationCopy::VERIFY_SUCCESS)。
登録情報変更ページ側は verified=1 を受け取り、「メールアドレスの確認が完了しました。」を表示する(MemberProfileController / MemberEmailVerificationCopy::VERIFY_SUCCESS_PROFILE)。マイページでメールアドレスを変更した直後はログイン状態のまま確認メールが届くため、この導線を使う。
4.3.2 確認メール再送(POST)シーケンス
再送成功時はトークンが差し替わるため、古いメール内リンクは無効になる。
4.4 規約同意・再同意フロー
利用規約の改定時に、既存会員へ再同意を求める。同意状態は wp_usermeta に保持する。
| 項目 | 方針 |
|---|---|
| 同意バージョン | member_terms_accepted_version(DB 現行 terms.version=LegalDocumentVersionService::get_current_terms_version() と一致で同意済み。フォールバックは MemberTermsConsentConstants::CURRENT_VERSION) |
| 同意日時 | member_terms_accepted_at(UNIX タイムスタンプ文字列) |
| 初回同意 | 会員登録フォームのチェックボックス送信時に記録 |
| 再同意 UI | 対象ページ上の モーダル(同意必須エリアのみマスク)。購入ブロック時の受け皿として /member-terms-reaccept/ も同一 UI |
| ゲート単位 | ページ丸ごとリダイレクトしない。同意必須の本文のみ非表示。同意不要な操作(ログアウト・退会など)は通常どおり利用可 |
| ゲート対象 | マイページ本体・購入済み一覧・登録情報変更フォーム・有料記事購入 |
ユーザー動線(全体像)
Phase 1: 非会員 → 会員登録(初回規約同意)
Phase 2: 会員の通常利用(規約改訂前)
Phase 3: 規約改訂 → 再同意
画面一覧(ユーザー視点)
| 画面 | URL(例) | 非会員 | 会員(同意済) | 会員(再同意必要) |
|---|---|---|---|---|
| サイト一般 | / 等 | 閲覧可 | 閲覧可 | 閲覧可 |
| 会員登録 | /member-register/ | 利用可 | — | — |
| ログイン | /member-login/ | 利用可 | 利用可 | 利用可 |
| 利用規約 | /terms/ | 閲覧可 | 閲覧可 | 閲覧可 |
| マイページ | /member-mypage/ | ログインへ | 閲覧可 | 本体マスク+モーダル。ログアウトは通常どおり可 |
| 購入済み一覧 | /member-purchased-articles/ | ログインへ | 閲覧可 | 一覧マスク+モーダル |
| 登録情報変更 | /member-profile/ | ログインへ | 利用可 | 登録情報フォームマスク+モーダル。退会は通常どおり可 |
| 有料記事購入 | 記事ページ → Checkout | ログインへ | 購入可 | /member-terms-reaccept/ へ誘導(同一モーダル UI) |
| 規約再同意 | /member-terms-reaccept/ | — | — | 購入ゲート等の受け皿(モーダル) |
運用: サイト運営 → 法務文書で利用規約の新版を作成し「現行版にする」。固定ページ /terms/ へ自動同期され、未同意会員には再同意が求められる。初期シード用定数は LegalPagesCopy / MemberTermsConsentConstants::CURRENT_VERSION(DB 空時のフォールバック)。詳細は 法務文書版管理.md。
5. ログインフロー
wp-login.php は使用せず、サイト内のログインページから認証する。
5.1 ログイン後のリダイレクト
| 条件 | 遷移先 |
|---|---|
redirect_to パラメータあり(同一オリジンのみ) | 指定 URL |
| 購入フロー中 | 購入対象の有料記事ページ |
| 通常 | マイページ(購入済み一覧) |
redirect_to の検証: wp_validate_redirect() または自前で同一ホストのみ許可(オープンリダイレクト防止)。
6. パスワードリセットフロー
WordPress core のパスワードリセット機構を利用し、UI のみ独自実装する。
- メール本文のリセット URL(
wp-login.php/ SiteGuard 改名後のログインパス、クエリ順やwp_langの有無を問わない)は、会員確認画面 URL(/member-password-reset/?key=&login=)に差し替える。 wp-login.phpまたは SiteGuard 改名ログインへaction=rp(もしくはresetpass)でkeyとlogin付きアクセスがあった場合は、ロールを問わず会員確認画面へリダイレクトする(WordPress 標準のパスワード再設定 UI は公開しない)。
7. CSRF 対策
7.1 WordPress Nonce
| フォーム | nonce アクション名 | 検証関数 |
|---|---|---|
| 会員登録 | member_register | wp_verify_nonce() |
| ログイン | member_login | wp_verify_nonce() |
| パスワードリセット要求 | member_password_reset_request | wp_verify_nonce() |
| パスワード再設定 | member_password_reset_confirm | wp_verify_nonce() |
実装パターン: 管理画面の AdminAjaxNonceVerifier と同様に、フロント向け MemberFormNonceVerifier トレイトを Service/Controller 層で共通化する。
8. ブルートフォース対策・レート制限
WordPress core にはログイン試行回数の標準制限がないため、以下を Transient 方式で行う。
8.1 採用方針: WordPress Transient によるレート制限
| 対象 | キー例 | 閾値 | ロックアウト |
|---|---|---|---|
| IP 単位(ログイン) | member_login_fail_ip_{md5(ip)} | 5 回 / 15 分 | 15 分 |
| ユーザー名単位 | member_login_fail_user_{md5(login)} | 5 回 / 15 分 | 15 分 |
| IP 単位(登録失敗) | member_register_ip_{md5(ip)} | 3 回 / 1 時間 | 1 時間 |
| IP 単位(登録成功) | member_register_success_ip_{md5(ip)} | 10 回 / 1 時間 | 1 時間 |
| IP 単位(PW リセット) | member_reset_ip_{md5(ip)} | 3 回 / 1 時間 | 1 時間 |
失敗カウンタの解除: ログイン成功時は該当の失敗 transient を削除する(正規ユーザーのロックアウト解除)。会員登録では登録成功時に失敗キー(member_register_ip_*)を削除し、成功キー(member_register_success_ip_*)へ record_attempt する。
実装参照:
| 役割 | クラス / 定数 |
|---|---|
| 共通 Util | MemberRateLimiter |
| ログイン | MemberLoginController + MemberLoginConstants |
| 登録 | MemberRegisterController + MemberRegisterConstants |
| PW リセット | MemberPasswordResetController + MemberPasswordResetConstants |
| 超過時メッセージ | ログイン等: Messages::RATE_LIMIT_EXCEEDED / 会員登録: MemberRegisterCopy::RATE_LIMIT_EXCEEDED |
監査テーブル方式の比較・再検討メモは ログイン試行監査テーブル-検討.md を参照。
9. セキュリティチェックリスト
| 項目 | 対応 |
|---|---|
| HTTPS | 本番必須 |
| nonce 検証 | 全フォーム POST |
| パスワード平文非保存 | core 委譲 |
| ログイン失敗メッセージ | ユーザー存在を漏らさない汎用文言 |
redirect_to 検証 | 同一オリジンのみ |
| レート制限 | Transient(§8.1) |
| セッション Cookie | secure / httponly は WP 設定に従う |
10. 画面一覧(参考)
| 種別 | 概要 |
|---|---|
| 公開画面 | ログイン・登録・リセット画面 |
| Service | MemberLoginService 等 |
| Controller | MemberLoginController 等 |