PHPでログイン機能を実装する方法について

採用はこちら

PHPでログイン機能を実装する場合は、メールアドレスやパスワードを確認するだけでは不十分です。

安全なログイン機能を作るためには、パスワードのハッシュ化、データベースへの安全な問い合わせ、セッション管理、ログアウト処理などを組み合わせる必要があります。

PHPには、password_hash()password_verify()、セッション機能など、ログイン処理に利用できる標準機能が用意されています。

ここでは、PHPとMySQL、PDOを使ってログイン機能を実装する基本的な方法を解説します。

目次

PHPのログイン機能の仕組み

PHPのログイン機能では、ユーザーが入力したメールアドレスやパスワードを確認し、正しいユーザーであるかを判定します。

一般的な流れは次のとおりです。

  1. ユーザー登録時にメールアドレスとパスワードを受け取る
  2. パスワードをpassword_hash()でハッシュ化する
  3. メールアドレスとパスワードハッシュをデータベースへ保存する
  4. ログイン画面でメールアドレスとパスワードを入力してもらう
  5. データベースから該当ユーザーを検索する
  6. password_verify()でパスワードを照合する
  7. 認証に成功したらセッションへユーザーIDを保存する
  8. ログインが必要なページではセッションを確認する
  9. ログアウト時にセッション情報を削除する

基本的には、この流れを理解するとPHPのログイン機能を実装しやすくなります。

ログイン機能に必要なファイルを用意する

シンプルなログイン機能であれば、次のようなファイル構成にできます。

/login-sample
│
├── db.php
├── register.php
├── login.php
├── dashboard.php
└── logout.php

db.php

データベースへ接続するためのファイルです。

register.php

ユーザー登録処理を行います。

login.php

メールアドレスとパスワードを使ってログイン処理を行います。

dashboard.php

ログインしたユーザーだけがアクセスできるページです。

logout.php

セッションを破棄してログアウト処理を行います。

実際のサービスでは、共通処理やHTMLをさらに分離したり、Laravelなどのフレームワークを利用したりする場合もあります。

usersテーブルを作成する

まずは、ユーザー情報を保存するためのテーブルをMySQLに作成します。

CREATE TABLE users (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

パスワードは平文で保存しない

passwordカラムには、ユーザーが入力したパスワードをそのまま保存してはいけません。

PHPのpassword_hash()を使って生成したハッシュ値を保存します。

例えば、ユーザーが次のパスワードを登録したとします。

password123

データベースには、この文字列そのものではなく、password_hash()によって生成されたハッシュ値を保存します。

passwordカラムは十分な長さを確保する

PASSWORD_DEFAULTで利用されるハッシュ方式は、将来的に変更される可能性があります。

そのため、パスワードハッシュを保存するカラムには十分な長さを確保しておくことが重要です。

例えば、次のようにVARCHAR(255)としておけば扱いやすいでしょう。

password VARCHAR(255) NOT NULL

PHPからMySQLへ接続する

PHPからMySQLへ接続する場合はPDOを利用できます。

db.phpを次のように作成します。

<?php

$dsn = 'mysql:host=localhost;dbname=sample;charset=utf8mb4';
$dbUser = 'root';
$dbPassword = '';

try {

    $pdo = new PDO(
        $dsn,
        $dbUser,
        $dbPassword,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]
    );

} catch (PDOException $e) {

    error_log($e->getMessage());

    exit('データベースに接続できませんでした。');
}

データベース接続情報を直接公開しない

実際のWebサービスでは、データベースのパスワードなどを公開ディレクトリ内のPHPファイルへ直接記述するのは避けた方が安全です。

環境変数や設定ファイルなどを利用し、第三者から確認できない場所で管理する方法が一般的です。

また、本番環境ではデータベースの詳細なエラー内容をブラウザへ表示しないようにします。

詳細情報はログへ記録し、ユーザーには一般的なエラーメッセージだけを表示する方が安全です。

ユーザー登録処理を実装する

ユーザー登録では、パスワードをハッシュ化してからデータベースへ保存します。

例えば、register.phpは次のように実装できます。

<?php

require_once 'db.php';

$message = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $email = trim($_POST['email'] ?? '');
    $password = $_POST['password'] ?? '';

    if ($email === '' || $password === '') {

        $message =
            'メールアドレスとパスワードを入力してください。';

    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {

        $message =
            '正しいメールアドレスを入力してください。';

    } elseif (strlen($password) < 8) {

        $message =
            'パスワードは8文字以上にしてください。';

    } else {

        $hash = password_hash(
            $password,
            PASSWORD_DEFAULT
        );

        try {

            $stmt = $pdo->prepare(
                'INSERT INTO users (email, password)
                 VALUES (:email, :password)'
            );

            $stmt->execute([
                ':email' => $email,
                ':password' => $hash
            ]);

            $message =
                '登録が完了しました。';

        } catch (PDOException $e) {

            error_log($e->getMessage());

            $message =
                '登録処理中にエラーが発生しました。';
        }
    }
}
?>

password_hash()でパスワードをハッシュ化する

重要なのは次の部分です。

$hash = password_hash(
    $password,
    PASSWORD_DEFAULT
);

password_hash()は、パスワード保存用のハッシュを生成するPHP標準関数です。

独自にMD5やSHA-1を使ってパスワードを保存する方法は避けます。

例えば、次のような実装は推奨されません。

$password = md5($_POST['password']);
$password = sha1($_POST['password']);

パスワード保存には、PHPのパスワードAPIを利用するのが基本です。

8文字以上はあくまで実装例

先ほどのサンプルでは、次のようにしています。

strlen($password) < 8

ただし、「8文字以上なら十分に安全」という意味ではありません。

実際のサービスでは、サービスの性質やセキュリティ要件に応じてパスワードポリシーを決める必要があります。

最低文字数だけでなく、ログイン試行制限や多要素認証なども組み合わせて安全性を高めます。

ログイン処理を実装する

続いて、ログイン処理を作成します。

ログイン処理では、次の流れで認証を行います。

メールアドレス入力
↓
データベースからユーザー検索
↓
password_verify()でパスワード照合
↓
認証成功
↓
セッションID更新
↓
ユーザーIDをセッションへ保存

例えば、login.phpは次のように実装できます。

<?php

session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

session_start();

require_once 'db.php';

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $email = trim($_POST['email'] ?? '');
    $password = $_POST['password'] ?? '';

    $stmt = $pdo->prepare(
        'SELECT id, email, password
         FROM users
         WHERE email = :email
         LIMIT 1'
    );

    $stmt->execute([
        ':email' => $email
    ]);

    $user = $stmt->fetch();

    if (
        $user &&
        password_verify(
            $password,
            $user['password']
        )
    ) {

        session_regenerate_id(true);

        $_SESSION['user_id'] = $user['id'];

        header('Location: dashboard.php');
        exit;

    } else {

        $error =
            'メールアドレスまたはパスワードが正しくありません。';
    }
}
?>

password_verify()でパスワードを照合する

ログイン時には、password_verify()を使ってパスワードを確認します。

password_verify(
    $password,
    $user['password']
)

第1引数には、ユーザーが入力したパスワードを指定します。

第2引数には、データベースへ保存されているハッシュ値を指定します。

パスワードが一致すればtrue、一致しなければfalseが返されます。

例えば次のように利用できます。

$hash = password_hash(
    'password123',
    PASSWORD_DEFAULT
);

if (
    password_verify(
        'password123',
        $hash
    )
) {

    echo 'パスワードが一致しました。';
}

PDOのプリペアドステートメントを使用する

ユーザーが入力したメールアドレスをSQL文へ直接埋め込むのは避けます。

例えば、次のようなコードは適切ではありません。

$sql =
    "SELECT *
     FROM users
     WHERE email = '$email'";

ユーザー入力をそのままSQLへ埋め込むと、SQLインジェクションの原因になる可能性があります。

PDOではプレースホルダーを利用します。

$stmt = $pdo->prepare(
    'SELECT *
     FROM users
     WHERE email = :email'
);

$stmt->execute([
    ':email' => $email
]);

このように、SQL文と値を分離して扱うことが重要です。

セッションでログイン状態を管理する

HTTP通信では、通常はリクエストごとに状態が独立しています。

そのため、ログインしたユーザーを継続的に識別するためにセッションを利用します。

PHPでは次のようにセッションを開始します。

session_start();

ログイン成功後は、ユーザーIDなどをセッションへ保存します。

$_SESSION['user_id'] =
    $user['id'];

例えば、

$_SESSION['user_id'] = 10;

となっていれば、ユーザーIDが10のユーザーがログインしていると判断できます。

ログイン成功時にセッションIDを更新する

ログインに成功したタイミングでは、セッションIDを更新しておくことが重要です。

session_regenerate_id(true);

例えば次のようにします。

if (
    $user &&
    password_verify(
        $password,
        $user['password']
    )
) {

    session_regenerate_id(true);

    $_SESSION['user_id'] =
        $user['id'];
}

ログイン前後でセッションIDを更新することで、セッション固定攻撃への対策になります。

ただし、session_regenerate_id()を実行するだけでセッション対策が完了するわけではありません。

HTTPSやCookie設定、セッション有効期限なども含めて対策する必要があります。

セッションCookieを安全に設定する

本番環境では、セッションCookieにも適切な設定を行います。

例えば次のように設定できます。

session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

session_start();

Secure

Secureを有効にすると、セッションCookieをHTTPS通信で送信するように制限できます。

そのため、基本的にHTTPS環境で使用します。

HttpOnly

HttpOnlyを有効にすると、JavaScriptからCookieへ直接アクセスされにくくなります。

XSSが発生した場合の被害軽減につながる設定です。

SameSite

SameSiteは、別サイトからのリクエスト時にCookieを送信する条件を制御するための設定です。

CSRF対策を補助する役割があります。

なお、session_set_cookie_params()session_start()より前に実行する必要があります。

ログインが必要なページを作成する

ログインしているユーザーだけがアクセスできるページでは、セッションにユーザーIDが存在するか確認します。

例えばdashboard.phpは次のようにします。

<?php

session_start();

if (!isset($_SESSION['user_id'])) {

    header('Location: login.php');
    exit;
}
?>

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>マイページ</title>
</head>
<body>

<h1>マイページ</h1>

<p>
    ログインしています。
</p>

<form method="post" action="logout.php">
    <button type="submit">
        ログアウト
    </button>
</form>

</body>
</html>

セッションがなければログインページへ戻す

次の部分でログイン状態を確認しています。

if (!isset($_SESSION['user_id'])) {

    header('Location: login.php');
    exit;
}

セッションにユーザーIDが存在しなければ、ログインページへリダイレクトします。

ユーザー情報をデータベースから取得する

マイページでメールアドレスやユーザー名を表示する場合は、セッションに保存したユーザーIDからデータベースを検索します。

$stmt = $pdo->prepare(
    'SELECT id, email
     FROM users
     WHERE id = :id'
);

$stmt->execute([
    ':id' => $_SESSION['user_id']
]);

$user = $stmt->fetch();

ユーザーが存在しない場合は、セッションを終了してログインページへ戻すとよいでしょう。

if (!$user) {

    $_SESSION = [];

    session_destroy();

    header('Location: login.php');
    exit;
}

HTMLへ出力するときはエスケープする

取得したメールアドレスなどをHTMLへ表示するときは、適切にエスケープします。

<?= htmlspecialchars(
    $user['email'],
    ENT_QUOTES,
    'UTF-8'
) ?>

重要なのは、「データベースから取得した値だからエスケープする」という考え方ではありません。

どこから取得したデータであっても、HTMLへ動的な文字列として出力する場合は、出力先に応じた適切なエスケープが必要です。

ログアウト機能を実装する

ログアウトでは、セッション情報を削除します。

状態を変更する処理なので、単純なGETリンクではなくPOSTで処理する方が適切です。

ログアウトボタンは次のようにできます。

<form method="post" action="logout.php">
    <button type="submit">
        ログアウト
    </button>
</form>

logout.phpでは次のように処理します。

<?php

session_start();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {

    http_response_code(405);
    exit;
}

$_SESSION = [];

if (ini_get('session.use_cookies')) {

    $params =
        session_get_cookie_params();

    setcookie(
        session_name(),
        '',
        time() - 42000,
        $params['path'],
        $params['domain'],
        $params['secure'],
        $params['httponly']
    );
}

session_destroy();

header('Location: login.php');
exit;

CSRF対策も重要

ログアウトやユーザー情報変更など、ログイン後の状態を変更する操作ではCSRF対策も検討する必要があります。

CSRFとは、ログイン済みユーザーのブラウザを悪用し、本人が意図していない操作をWebサイトへ送信させる攻撃です。

例えばCSRFトークンを次のように生成できます。

$_SESSION['csrf_token'] =
    bin2hex(random_bytes(32));

フォームへ埋め込みます。

<input
    type="hidden"
    name="csrf_token"
    value="<?= htmlspecialchars(
        $_SESSION['csrf_token'],
        ENT_QUOTES,
        'UTF-8'
    ) ?>"
>

受信側では、次のように確認できます。

if (
    !isset($_POST['csrf_token']) ||
    !hash_equals(
        $_SESSION['csrf_token'],
        $_POST['csrf_token']
    )
) {

    exit('不正なリクエストです。');
}

実際のWebサービスでは、トークンの有効期限や再生成方法なども含めて設計します。

ログイン失敗時のメッセージにも注意する

ログインに失敗した場合に、

メールアドレスが存在しません。

と表示すると、そのメールアドレスが登録されているかどうかを第三者に推測される可能性があります。

そのため、次のように統一したメッセージを表示する方法があります。

メールアドレスまたはパスワードが正しくありません。

例えば、PHPでは次のようにします。

$error =
    'メールアドレスまたはパスワードが正しくありません。';

ログイン試行回数を制限する

ログインフォームでパスワードを無制限に試せる状態にすると、ブルートフォース攻撃やパスワードリスト攻撃のリスクが高まります。

そのため、本番環境ではログイン試行回数を制限する仕組みも重要です。

例えば、次のような情報を組み合わせて制御します。

  • 一定時間内のログイン失敗回数
  • アカウント単位の失敗回数
  • IPアドレス
  • 端末情報
  • 段階的な待機時間
  • 必要に応じた多要素認証

単純に「5回失敗したら永久ロック」という仕組みにすると、第三者が他人のアカウントを意図的にロックできる可能性があります。

そのため、サービスに応じて適切な方式を設計する必要があります。

password_needs_rehash()でハッシュを更新できる

PASSWORD_DEFAULTを使用している場合、PHPの将来のバージョンで推奨アルゴリズムが変更される可能性があります。

そのような場合は、password_needs_rehash()を利用できます。

if (
    password_needs_rehash(
        $user['password'],
        PASSWORD_DEFAULT
    )
) {

    $newHash = password_hash(
        $password,
        PASSWORD_DEFAULT
    );
}

ログイン成功時などに確認し、必要であれば新しいハッシュへ更新する設計が可能です。

PHPログイン機能で必要なセキュリティ対策

実際に公開するWebサービスでログイン機能を運用する場合は、最低限次のポイントを意識する必要があります。

  • パスワードはpassword_hash()で保存する
  • パスワード照合にはpassword_verify()を使う
  • PDOのプリペアドステートメントを使う
  • ログイン成功後にセッションIDを更新する
  • セッションCookieにSecure、HttpOnly、SameSiteを設定する
  • HTTPSを利用する
  • HTML出力時に適切なエスケープを行う
  • CSRF対策を行う
  • ログイン試行回数を制限する
  • DBエラーの詳細をユーザーへ表示しない
  • データベース接続情報を安全に管理する
  • 必要に応じて多要素認証を導入する
  • パスワードリセット機能を安全に設計する
  • ログインやパスワード変更などの操作ログを適切に管理する

PHPログイン機能の基本的な流れ

PHPログイン機能の全体像をまとめると、次のようになります。

【ユーザー登録】

メールアドレス
+
パスワード
↓
password_hash()
↓
データベースへ保存


【ログイン】

メールアドレス
+
パスワード
↓
PDOでユーザー検索
↓
password_verify()
↓
認証成功
↓
session_regenerate_id()
↓
$_SESSION['user_id']へ保存
↓
マイページへ移動


【ログイン後のページ】

session_start()
↓
$_SESSION['user_id']を確認
↓
存在する
→ ページ表示

存在しない
→ login.phpへ移動


【ログアウト】

POSTリクエスト
↓
CSRF確認
↓
セッション情報削除
↓
session_destroy()
↓
login.phpへ移動

PHPのログイン機能はセキュリティまで考えて実装することが重要

PHPで基本的なログイン機能を作る場合は、password_hash()password_verify()、PDO、セッションを中心に実装できます。

特に重要なのは、パスワードを平文で保存しないことと、ユーザー入力をSQLへ直接埋め込まないことです。

また、ログイン成功後にはセッションIDを更新し、CookieにはSecureやHttpOnlyなどの設定を行う必要があります。

ただし、単に「ログインできるコード」を作ることと、「安全に運用できる認証システム」を作ることは同じではありません。

実際のWebサービスでは、HTTPS、CSRF対策、XSS対策、ログイン試行制限、パスワードリセット、多要素認証、ログ監視なども含めて設計することが重要です。

PHPのログイン機能を実装するときは、認証処理だけを見るのではなく、セッションやCookie、データベース、フォームなどを含めて、Webアプリケーション全体のセキュリティを考えるようにしましょう。

以上、PHPでログイン機能を実装する方法についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次