#author("2025-04-28T20:51:05+09:00","default:Shigure","Shigure")
~> [[Category/Security]]
#author("2025-04-30T01:22:50+09:00","default:Shigure","Shigure")
~> [[Category/*CreatedByAI]]



* セキュアなフォーム認証の実装(PHP + JS) [#secure_login]

** ▼ 前提条件 [#j69a2b87]
- サーバーサイドはPHP
- フロントエンドはHTML + JavaScript(任意)
- データベース(例:MySQL)にユーザー情報が保存されている
- パスワードは `password_hash()` でハッシュ化済み

** ▼ バージョン1:PHPのみのセキュア認証フォーム [#x352e0c5]

*** login.php(ログイン画面) [#vd6b34cd]
 #code(php){{
<?php
session_start();
$csrf_token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $csrf_token;
?>

<form method="post" action="auth.php">
    <label for="username">ユーザー名:</label>
    <input type="text" name="username" required><br>

    <label for="password">パスワード:</label>
    <input type="password" name="password" required><br>

    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">

    <button type="submit">ログイン</button>
</form>
 #}}

*** auth.php(認証処理) [#ycd2835a]
 #code(php){{
<?php
session_start();

// CSRFトークン確認
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die('CSRFトークンが無効です');
}

$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';

// DB接続(例)
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');

// SQLインジェクション対策
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();

if ($user && password_verify($password, $user['password'])) {
    session_regenerate_id(true);
    $_SESSION['loggedin'] = true;
    $_SESSION['username'] = $user['username'];
    echo "ログイン成功!";
} else {
    echo "ユーザー名またはパスワードが間違っています";
}
 #}}

** ▼ バージョン2:PHP + JavaScriptでのフォーム(UX改善) [#u5d7255a]

*** login.php(クライアント側入力チェックあり) [#q1be9d5b]
 #code(php){{
<?php
session_start();
$csrf_token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $csrf_token;
?>

<form id="loginForm" method="post" action="auth.php">
    <label for="username">ユーザー名:</label>
    <input type="text" id="username" name="username" required><br>

    <label for="password">パスワード:</label>
    <input type="password" id="password" name="password" required><br>

    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">

    <button type="submit">ログイン</button>
</form>

<script>
document.getElementById('loginForm').addEventListener('submit', function(e) {
    const username = document.getElementById('username').value.trim();
    const password = document.getElementById('password').value.trim();

    if (username === '' || password === '') {
        e.preventDefault();
        alert("ユーザー名とパスワードは必須です。");
    }
});
</script>
 #}}

*** auth.php は同じ内容を再利用可能 [#we53bb47]

** ▼ セキュリティ対策チェックリスト [#vdd2ec1a]
- [x] `password_hash()` / `password_verify()` でパスワード保護
- [x] `session_regenerate_id(true);` でセッション固定攻撃対策
- [x] CSRFトークンを hidden に埋め込む
- [x] SQLインジェクション対策(PDO + プレースホルダ)
- [x] 出力時は `htmlspecialchars()` でXSS対策

** ▼ 補足:次に実装する機能例 [#o861f8b2]
- [[ユーザー登録(signup.php)]]
- [[ログアウト処理(logout.php)]]
- [[ログイン済み専用ページ(dashboard.php)]]
- [[2段階認証(TOTPなど)]]

#br
#br
#br
#br
#br
#br
#br
#hr
#br
#br
#br
#br
#br
#br
#br

* データベースを使わないセキュアなログイン認証 [#dbfree_login]

** ▼ 概要 [#j3i1c7ea]
- ユーザー情報はPHPコード内に定義(配列)
- パスワードは `password_hash()` でハッシュ化済み
- データベース接続不要
- 小規模・個人用におすすめ

** ▼ login.php(ログインフォーム) [#x4kk19e2]
 #code(php){{
<?php
session_start();
$csrf_token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $csrf_token;
?>

<form method="post" action="auth.php">
    <label for="username">ユーザー名:</label>
    <input type="text" name="username" required><br>

    <label for="password">パスワード:</label>
    <input type="password" name="password" required><br>

    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">

    <button type="submit">ログイン</button>
</form>
 #}}

** ▼ auth.php(認証処理) [#z3sa2k21]
 #code(php){{
<?php
session_start();

// CSRFチェック
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die('CSRFトークンが無効です');
}

$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';

// 事前登録されたユーザー(パスワードは password_hash() の出力)
$users = [
    'admin' => '$2y$10$8s4zM5k7BnEl9P57lY2qDO1LVJ3AsGHDDe8zUlqD9uJXsM5Xx9RUW', // password: mypass123
    'user'  => '$2y$10$ZK1K9yUO7cV9GrGc9eZXzO.xFNZ4kg9w6FxJYVbmuMyO0r3aPpGQK'  // password: secret456
];

// ユーザー存在とパスワード検証
if (isset($users[$username]) && password_verify($password, $users[$username])) {
    session_regenerate_id(true);
    $_SESSION['loggedin'] = true;
    $_SESSION['username'] = $username;
    echo "ログイン成功!";
} else {
    echo "ユーザー名またはパスワードが間違っています";
}
 #}}

** ▼ パスワードハッシュの生成方法(事前に実行) [#genpass]
以下のようなスクリプトで一度ハッシュを生成して、auth.php にコピペします。
 #code(php){{
<?php
echo password_hash('mypass123', PASSWORD_DEFAULT);
 #}}

** ▼ 注意点・セキュリティ強化策 [#tips]
- [x] パスワードは必ず `password_hash()` でハッシュ化して保存
- [x] CSRFトークンを使用
- [x] `session_regenerate_id(true);` をログイン時に使用
- [x] 配列にユーザー名とハッシュを直接記述するだけなので、他のファイルからは読み込まれないよう注意

** ▼ この方式が向いているケース [#usecases]
- 個人利用サイト
- 小規模管理画面(ブログ管理など)
- ユーザー追加は手動でも問題ない環境