Menu

PHPのセッションとCookie:session_startとsetcookie

すべてのページの先頭でsession_start()を呼び、$_SESSION['key']に値を保存します。PHPはそれをサーバーに保持し、Cookieを通して訪問者を覚えます。setcookie('name', 'value', $options)はブラウザに小さな値を保存し、$_COOKIEで読み戻します。ログイン、ログアウト、フラッシュメッセージ、Cookieのオプション、セキュリティの設定を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

ページの先頭でsession_start()を呼び、ほかの配列と同じように$_SESSION['key']を読み書きします。PHPはそれをサーバーに保存し、訪問者の次のリクエストで再び見つけます。ブラウザに保持させる小さな値なら、setcookie('name', 'value', time() + 86400)を呼び、あとのリクエストで$_COOKIE['name']から読み戻します。

<?php
session_start();                        // before any output
setcookie('theme', 'dark', time() + 60 * 60 * 24 * 30);   // 30 days, also before output

$_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1;
$theme = $_COOKIE['theme'] ?? 'light';  // set on an earlier visit
echo "You have opened this page {$_SESSION['visits']} times.";

セッションとCookieは、ブラウザからの複数のHTTPリクエストにまたがってこそ意味があるので、それが必要な例は普通のコードにしています。その周りのロジックは配列に対する普通のPHPで、その部分はここで実行できます。実行できる各ブロックでは$sessionが$_SESSIONの代わりを務め、各呼び出しが1回のリクエストです。

PHPのセッションの仕組み

  1. 最初のsession_start()がランダムなIDを作り、PHPSESSIDという名前のCookieでブラウザに送り、空の$_SESSIONを用意します。
  2. リクエストの終わりに、PHPは$_SESSIONをサーバーに保存します(デフォルトではファイル、またはRedisやデータベース)。
  3. 次のリクエストでブラウザがCookieを送り返し、session_start()がIDを読んで保存された配列を読み込みます。

ブラウザに見えるのはデータではなくIDだけです。だからこそ、ログイン中のユーザーのIDは自分で作ったCookieではなく$_SESSIONに置くべきなのです。訪問者は自分のCookieを任意の値に編集できますが、$_SESSIONは編集できません。

セッションでユーザーをログイン、ログアウトさせる

ログインは、パスワードを確認し、セッションIDを作り直し、ユーザーIDを保存することです。ログアウトはその逆で、3つのステップがあります。

<?php
// login.php
session_start();

$user = find_user_by_email($_POST['email'] ?? '');
if ($user && password_verify($_POST['password'] ?? '', $user['password_hash'])) {
    session_regenerate_id(true);        // new id after login: stops session fixation
    $_SESSION['user_id'] = $user['id'];
    header('Location: /dashboard.php');
    exit;
}
$error = 'Wrong email or password.';
<?php
// any protected page
session_start();
if (!isset($_SESSION['user_id'])) {
    header('Location: /login.php');
    exit;
}
<?php
// logout.php
session_start();
$_SESSION = [];                                     // 1. forget the data

$p = session_get_cookie_params();                   // 2. delete the cookie
setcookie(session_name(), '', time() - 3600, $p['path'], $p['domain'], $p['secure'], $p['httponly']);

session_destroy();                                  // 3. delete the stored session
header('Location: /');
exit;

session_regenerate_id(true)は見た目以上に重要です。攻撃者が自分の知っているセッションIDを(たとえばリンクを通して)被害者に使わせることができ、ログイン後もIDが同じままなら、攻撃者もログインしてしまいます。ログイン時に新しいIDを作れば、その穴をふさげます。パスワードの確認そのものはpassword_hashのページで扱っています。

操作がないときにセッションを期限切れにする

PHP自身の後片付け(session.gc_maxlifetime、デフォルトで1440秒)はランダムに実行され、信頼できるタイムアウトではありません。最後のリクエストの時刻を保存して、自分で確認しましょう。このブロックは各ケースがわかるよう固定のタイムスタンプを使っています。$timeoutや時刻を変えてみてください。

09:05から09:16:40までの間隔は700秒で15分の上限より短いので、ユーザーはログインしたままです。次の間隔は1000秒なので、セッションがクリアされます。

フラッシュメッセージ

フラッシュメッセージは、あるリクエストで設定し、次のリクエストで1回だけ表示するもので、典型的にはリダイレクト後の「保存しました」です。セッションに保存し、1ステップで読んで削除します。

setcookieでCookieを設定する

setcookie()は、名前、値、オプションの配列(PHP 7.3以降)を受け取ります。理由がない限り、すべてのCookieに次の4つのオプションを設定しましょう。

<?php
setcookie('theme', 'dark', [
    'expires'  => time() + 60 * 60 * 24 * 30,   // 30 days; 0 means "until the browser closes"
    'path'     => '/',                          // send it on every page, not just this folder
    'secure'   => true,                         // only over HTTPS
    'httponly' => true,                         // JavaScript cannot read it
    'samesite' => 'Lax',                        // not sent on most cross-site requests
]);

$theme = $_COOKIE['theme'] ?? 'light';

つまずきやすい点が2つあります。

  • $_COOKIEは同じリクエストの中では変わりません。 それが持っているのは、ブラウザがこのリクエストと一緒に送ったものです。今設定したCookieは、次のリクエストで$_COOKIEに現れます。
  • Cookieはヘッダーです。 そのためsetcookie()は、session_start()やheader()とまったく同じく、何かを出力する前に実行しなければなりません。

Cookieを削除するには、同じ名前とpathで過去の有効期限を付けてもう一度設定します:setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/'])。

Cookieに配列を保存する

Cookieの値は最大で約4KBの文字列なので、構造化されたデータはJSONで保存し、戻ってきたものは信頼できないものとして扱いましょう。訪問者が変更できるからです。Cookieを決してunserialize()してはいけません。自分のクラスのオブジェクトを作ってそのマジックメソッドを実行できてしまいますが、json_decode()が返すのは配列、文字列、数値、真偽値、null、stdClassだけです。

フラグについてはjson_encodeのページで扱っています。いくつかの短い値より大きいものは、セッション(またはデータベース)にデータを置き、Cookieにはキーだけを保存しましょう。

「ログインしたままにする」トークン

「ログインしたままにする」のCookieに、ユーザーIDやパスワードのハッシュを入れてはいけません。長いランダムなトークンをCookieに、そのハッシュだけをデータベースに保存し、hash_equals()で比較します。

random_bytes()はランダムなので、値は実行のたびに変わります。データベースが漏えいしても、Cookieがなければハッシュは役に立たず、hash_equals()は一定の時間で比較するので、確認から時間の情報が漏れることもありません。

セッションとCookie:どちらを使うか

セッション($_SESSION)Cookie($_COOKIE)
保存場所サーバーブラウザ
訪問者が読めるか、編集できるかできない(IDだけ)できる
サイズストレージ次第Cookie1つあたり約4KB
続く期間ブラウザを閉じるか、セッションが期限切れになるまでexpiresの時刻まで
使いどころログイン中のユーザー、カート、CSRFトークン、フラッシュメッセージテーマ、言語、同意の選択、「ログインしたままにする」トークン

よくあるミス:headers already sent

session_start()、setcookie()、header()はどれもHTTPのヘッダーを送り、ヘッダーは本文より前にしか送れません。includeしたファイルの<?phpの前の空行でさえ、先に何かを出力すると次のようになります。

Warning: session_start(): Session cannot be started after headers have already been sent in /var/www/page.php on line 3
Warning: Cannot modify header information - headers already sent by (output started at /var/www/page.php:2) in /var/www/page.php on line 4

2つ目のメッセージは出力が始まった場所(page.php:2)を教えてくれます。その行に行って出力を取り除くか、session_start()をその上に移しましょう。PHPだけを含むファイルでは、閉じタグの?>を省けば、そのあとの余計な空白が出力になるのを防げます。

よくある質問

PHPのセッションとCookieの違いは何ですか?

Cookieは訪問者のブラウザに保存され、リクエストのたびに送られるので、訪問者が読んだり変えたりできます。セッションはデータをサーバーに保存し、ブラウザが持つのはCookie(PHPSESSID)の中のランダムなセッションIDだけです。ログイン中のユーザーIDのように信頼できなければならないものは$_SESSIONに置きましょう。

PHPでログアウト時にセッションを破棄するには?

データを空にし、セッションのCookieを削除し、保存されたセッションを破棄します:$_SESSION = [];、次にsetcookie(session_name(), '', time() - 3600, '/')、そしてsession_destroy();。そのあとリダイレクトして、次のページがまっさらな状態で始まるようにします。

PHPでCookieを削除するには?

同じ名前、パス、ドメインで、過去の有効期限を付けてもう一度設定します:setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);。$_COOKIEが変わるのは次のリクエストなので、今のスクリプトの残りがそれを読むならunset($_COOKIE['theme'])もします。

PHPのセッションはどれくらい続きますか?

デフォルトでは、セッションのCookieはブラウザを閉じるまで続き(session.cookie_lifetime = 0)、サーバーは操作のないまま1440秒(24分)たつとセッションのデータを削除することがあります(session.gc_maxlifetime)。決まったタイムアウトにするには、最後のリクエストの時刻をセッションに保存して自分で確認します。

session_start()でheaders already sentと出るのはなぜですか?

セッションIDはCookieのヘッダーで運ばれ、ヘッダーはどんな出力よりも前に送らなければならないからです。<?phpの前のecho、HTML、空行やBOMでさえ、先に出力を送ってしまいます。何かを出力する前、スクリプトのいちばん先頭でsession_start()を呼びましょう。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める