Webアプリにログイン機能を付けるだけなら、それほど難しくありません。
IDとパスワードを受け取り、正しければセッションを開始する。
それだけでも「ログインできる画面」は作れます。
しかし、実際に外部の利用者が登録し、本番環境で継続して使うWebアプリとして考えると、それだけでは足りません。
今回、自分で開発しているWebアプリを、公開登録から本人確認、アカウント成立、ログイン、本番運用までつなげたことで、
- 認証
- 認可
- セッション
- アカウント状態
- 契約状態
- ログイン履歴
- 管理者と一般利用者の分離
- 異常系
- セキュリティ
を一つの流れとして設計する必要がありました。
この記事では、実際にどのような観点で設計したのかを整理します。
この記事で公開しないもの
最初に一つだけ。
この記事は、実際に本番運用しているシステムを題材にしています。
そのため、
- 認証失敗を何回まで許容するか
- ロックをどれだけ継続するか
- セッションをどれだけ有効にするか
- 確認情報をどれだけ有効にするか
- 管理機能への具体的なアクセス制御
- 内部URL
- サーバーやネットワークの詳細構成
など、防御条件の推測につながる具体値や内部情報は公開しません。
この記事で扱うのは、何を考えて設計したかです。
具体値を伏せること自体も、本番システムについて公開する際の設計の一つだと考えています。
1. 認証は「パスワードが正しいか」だけではない
単純に考えると、ログインは次のように見えます。
ID入力
↓
パスワード照合
↓
一致
↓
ログイン成功
しかし本番では、その前後に確認することがあります。
契約は存在するか
↓
契約は利用可能な状態か
↓
ユーザーは存在するか
↓
ユーザーは利用可能な状態か
↓
資格情報は正しいか
↓
ログインを許可してよい状態か
↓
セッションを開始する
つまり、
認証情報が正しいことと、その人をシステムへ入れてよいことは別です。
パスワードだけ正しくても、契約やアカウントの状態によってはログインさせてはいけない場合があります。
2. 「認証」と「認可」を混ぜない
もう一つ意識したのが、
- 認証:誰なのか
- 認可:何をしてよいのか
を分けることです。
ログインできたからといって、すべての機能を使えるわけではありません。
例えば、
ログイン済み
│
├─ 一般利用者
│ └─ 一般利用者向け機能
│
└─ 管理者
└─ 管理機能
のように、ログイン後にも判断が必要です。
実装でも、
if (!$isAuthenticated) {
// ログイン画面へ
}
if (!$hasPermission) {
// アクセス拒否
}
という二つは意味が違います。
「ログインしているから表示してよい」としてしまうと、権限制御が崩れます。
3. セッションは「ログイン済みフラグ」ではない
認証基盤そのものは、公開登録を作るより前から用意していました。
しかし、外部の利用者が自分で登録し、本番で使い始めるところまでつなげると、セッション管理の意味がよりはっきりしました。
例えば、
$_SESSION['logged_in'] = true;
だけでは、本番運用には足りません。
考える必要があるのは、
- 誰のセッションなのか
- どの契約に属するのか
- いつ開始されたのか
- まだ有効なのか
- ログアウト済みではないか
- 有効条件を満たしているか
- 同時利用条件に違反していないか
といったことです。
イメージとしては、
SESSION
├─ user
├─ contract
├─ created_at
├─ last_activity
├─ status
└─ identifier
のように、セッション自体を一つの管理対象として扱う考え方です。
4. セッションをサーバー側でも管理する理由
ブラウザ側の状態だけに頼ると、運営側から制御しづらくなります。
そこで、サーバー側でもセッション状態を確認できるようにします。
これにより、
- ログアウト
- 強制失効
- 有効条件を満たさなくなったセッションの無効化
- 複数端末利用の制御
- 不審なセッションの確認
- 障害時の調査
などがしやすくなります。
概念的には、
Browser
│
│ session identifier
▼
Application
│
▼
Session Store
│
├─ ACTIVE
├─ EXPIRED
└─ REVOKED
という構造です。
重要なのは、
Cookieが存在していることと、有効なセッションであることを同一視しない
ことです。
5. ログアウトも処理である
ログインは意識して作りますが、ログアウトは軽く扱われがちです。
しかし、ログアウトも認証設計の一部です。
単に画面をログインページへ移動するだけではなく、
セッションを特定
↓
サーバー側で無効化
↓
ブラウザ側の状態を破棄
↓
再利用できないことを確認
まで考えます。
「ログアウトボタンを押したらログイン画面になった」
だけでは不十分です。
戻る操作や古い情報を使ったアクセスでも、保護された機能へ戻れないことを確認する必要があります。
6. 複数端末をどう扱うか
実際の利用では、
- PC
- スマートフォン
- ノートPC
- タブレット
など、複数端末からログインする可能性があります。
ここで、
何台でも自由にログインできてよいのか
という仕様判断が必要になります。
これはコードの問題というより、サービス設計です。
例えば制限する場合でも、
- 新しいログインを拒否する
- 古いセッションを失効させる
- 利用者に選択させる
など、複数の方法があります。
AIに実装を依頼する前に、
どういう利用を許可したいのか
を決めておかなければ、正しいコードは出てきません。
7. 認証失敗は「エラー表示」だけではない
ログイン失敗時に、
IDまたはパスワードが違います
と表示するだけなら簡単です。
しかし公開サービスでは、繰り返し失敗するアクセスをどう扱うかも考える必要があります。
そのため、
認証要求
↓
失敗履歴確認
↓
現在ログイン試行を許可できるか
↓
資格情報確認
↓
成功 / 失敗記録
という流れを意識します。
ここでは具体的な閾値は公開しませんが、
無制限に試行できる状態にはしない
という設計です。
8. エラーメッセージも情報になる
認証処理では、エラー表示の内容にも注意が必要です。
例えば、
そのユーザーは存在しません
と
パスワードが違います
を区別すると、第三者にユーザーの存在を推測される可能性があります。
利用者には原因を分かりやすく伝えたい。
一方で、攻撃者には必要以上の情報を与えたくない。
このバランスがあります。
そのため、
内部では原因を区別して記録し、外部には必要以上の情報を出さない
という考え方を取ります。
9. 公開登録とログインは別物ではない
今回、特に大きく変わったのがここです。
以前は、
ユーザーが存在する
↓
ログインする
ところから考えていました。
しかし、公開登録を作るとその前に、
登録要求
↓
入力検証
↓
本人確認
↓
登録状態管理
↓
契約成立
↓
ユーザー成立
↓
利用可能状態
↓
ログイン
があります。
つまり、認証設計はログイン画面から始まっているわけではありません。
誰をユーザーとして成立させるのか
から始まっています。
10. 「途中状態」を明確に持つ
登録処理では、途中状態の管理が重要です。
単純に、
登録済み / 未登録
だけでは足りません。
例えば概念としては、
受付
↓
本人確認待ち
↓
確認済み
↓
アカウント作成
↓
完了
のような途中状態があります。
途中状態を曖昧にすると、
- 確認していないのに利用できる
- 完了した登録をもう一度処理する
- 途中失敗データが残る
- 同じ人が複数回作成される
といった問題につながります。
11. 冪等性を意識する
Webでは、同じリクエストが一度だけ届くとは限りません。
- ダブルクリック
- ブラウザ再送
- 通信再試行
- 利用者による再操作
などがあります。
そのため、登録処理では、
同じ処理がもう一回来ても壊れないか
を確認します。
理想は、
1回目
登録成功
2回目
すでに処理済みとして安全に終了
です。
最悪なのは、
1回目
ユーザー作成
2回目
別ユーザーとしてもう一件作成
です。
AIにコードを書かせる場合でも、正常系だけを指示すると、この観点は抜けやすい部分でした。
12. 管理者ログインを分ける
一般利用者とシステム管理者では、持っている権限が違います。
そのため、管理系の入口を一般利用者と同じものとして扱わない設計にしています。
理由は、
同じ認証だから同じ入口でよい、とは限らない
からです。
管理側には、
- より強いアクセス制御
- 管理専用の認証判断
- 一般画面とは異なる権限確認
を持たせます。
具体的な内部構成は公開しませんが、考え方としては、
一般利用
────────
一般認証
↓
一般機能
管理
────────
管理認証
↓
追加のアクセス制御
↓
管理機能
のように責務を分けています。
13. ログイン履歴を残す
本番では、
ログインできた
だけではなく、
後から何が起きたか確認できる
ことも重要です。
そこで、ログインについても履歴を持ちます。
例えば概念的には、
timestamp
user
result
session
source
reason
などです。
ここでも重要なのは、
画面に表示する情報と、内部調査用の情報を分けること
です。
利用者に詳細を見せる必要はなくても、運営側では障害や不正アクセス調査のために必要になります。
14. テストは正常ログインだけでは足りない
認証機能のテストで、
正しいID
正しいパスワード
→ ログイン成功
だけ確認しても十分ではありません。
むしろ重要なのは異常系です。
例えば、
誤った資格情報
無効なユーザー
無効な契約
期限切れセッション
ログアウト済みセッション
複数端末
繰り返し認証失敗
登録途中
本人確認前
重複登録要求
直接URLアクセス
権限外画面へのアクセス
といった状態です。
認証処理では、
入れることより、入れてはいけない状態で入れないこと
の方が重要なテストになる場合があります。
15. 直接URLアクセスを忘れない
この問題は、以前の開発でも実際に経験しました。
UI上からリンクを消していても、アクセス制御にはなりません。
例えば、
メニューには表示されない
状態でも、URLを直接指定される可能性があります。
だから各画面や処理側でも、認証・認可を確認する必要があります。
概念的には、
if (!$authorized) {
http_response_code(403);
exit;
}
のような考え方です。
ここでの教訓は単純です。
画面遷移を制御することと、アクセスを制御することは別です。
16. AIはかなり使える。ただし質問する側の責任は残る
今回も、コード作成、設計確認、テスト作成などにAIをかなり使っています。
AIは、
- セッション管理案
- 認証処理
- DB設計
- エラー処理
- テストケース
まで提案できます。
ただし、
正常にログインできるコードを書いて
と頼むのと、
途中状態、セッション失効、権限制御、
重複実行、直接URLアクセスも含めて
本番運用で成立する認証設計を検討して
と頼むのでは、出てくるものが全く違います。
結局、
何を確認しなければいけないかを知らなければ、AIにも確認させられません。
ここが、AI時代でもシステム設計の知識が必要だと感じる理由です。
まとめ
認証機能を作るというと、
ID
+
パスワード
=
ログイン
に見えます。
しかし実際の本番システムでは、
公開登録
↓
本人確認
↓
アカウント状態
↓
契約状態
↓
認証
↓
認可
↓
セッション
↓
履歴
↓
失効
↓
運用
までつながっています。
そして、それぞれに正常系と異常系があります。
今回、本番までつないで改めて感じたのは、
ログイン画面は認証システムの入口ではなく、利用者から見えている一部分にすぎない
ということです。
AIによってコードを書く負担は確実に減っています。
一方で、
- 何を守るのか
- 何を許すのか
- どの状態を異常とするのか
- 失敗したときに何を残すのか
- どこまでAIに任せるのか
という設計判断までなくなったわけではありません。
むしろコードを書きやすくなったからこそ、「動いた」の先を考える力が以前より重要になっていると感じています。
関連記事
noteでは、この技術設計に入る前の話として、
という記事を書いています。
公開登録から利用者を受け入れるまでを、技術詳細よりも「なぜそこまで必要なのか」という視点で整理しています。