Gọi session_start() ở đầu trang, rồi đọc và ghi $_SESSION['key'] như mọi mảng khác: PHP lưu nó trên máy chủ và tìm lại được ở request tiếp theo của người xem. Với một giá trị nhỏ mà trình duyệt nên giữ, gọi setcookie('name', 'value', time() + 86400) và đọc lại ở các request sau từ $_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.";
Session và cookie chỉ có ý nghĩa qua nhiều request HTTP từ trình duyệt, nên các ví dụ cần đến chúng là code thường. Phần logic xung quanh chỉ là PHP bình thường trên mảng, và phần đó chạy được ngay tại đây: trong mỗi khối code chạy được, $session đóng vai $_SESSION, và mỗi lần gọi là một request.
Session PHP hoạt động thế nào
- Lần gọi
session_start()đầu tiên tạo một id ngẫu nhiên, gửi nó về trình duyệt trong cookie tênPHPSESSID, và đưa cho bạn một$_SESSIONrỗng. - Cuối request, PHP lưu
$_SESSIONtrên máy chủ (mặc định trong tệp, hoặc Redis, hoặc cơ sở dữ liệu). - Ở request tiếp theo, trình duyệt gửi cookie trở lại,
session_start()đọc id và nạp mảng đã lưu.
Trình duyệt không bao giờ thấy dữ liệu, chỉ thấy id. Đó là lý do id của người dùng đã đăng nhập phải nằm trong $_SESSION và không bao giờ nằm trong một cookie do bạn tự đặt: người xem có thể sửa cookie thành bất kỳ giá trị nào, nhưng không sửa được $_SESSION.
Đăng nhập và đăng xuất bằng session
Đăng nhập gồm: kiểm tra mật khẩu, tạo lại session id, lưu id người dùng. Đăng xuất là quá trình ngược lại, gồm ba bước.
<?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) quan trọng hơn vẻ ngoài của nó. Nếu kẻ tấn công khiến nạn nhân dùng một session id mà hắn biết (chẳng hạn qua một liên kết), và id vẫn giữ nguyên sau khi đăng nhập, thì kẻ tấn công cũng được đăng nhập theo. Một id mới khi đăng nhập sẽ bịt lỗ hổng đó. Bản thân việc kiểm tra mật khẩu được trình bày ở trang password_hash.
Cho session hết hạn khi không hoạt động
Cơ chế dọn dẹp riêng của PHP (session.gc_maxlifetime, mặc định 1440 giây) chạy ngẫu nhiên và không phải là thời gian chờ đáng tin cậy. Hãy lưu thời điểm của request gần nhất và tự kiểm tra. Khối code dùng các timestamp cố định để bạn thấy từng trường hợp; thử đổi $timeout hoặc các mốc thời gian.
Khoảng cách từ 09:05 đến 09:16:40 là 700 giây, dưới giới hạn 15 phút, nên người dùng vẫn đăng nhập. Khoảng cách tiếp theo là 1000 giây, nên session bị xóa.
Flash message
Flash message là thông báo được đặt ở một request và hiện đúng một lần ở request tiếp theo, thường là "Đã lưu." sau khi chuyển hướng. Lưu nó trong session, rồi đọc và xóa nó trong cùng một bước:
Đặt cookie với setcookie
setcookie() nhận tên, giá trị và một mảng tùy chọn (từ PHP 7.3 trở đi). Hãy đặt bốn tùy chọn này cho mọi cookie, trừ khi bạn có lý do để không làm vậy:
<?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';
Có hai điều hay khiến người mới vấp:
$_COOKIEkhông thay đổi trong cùng một request. Nó chứa những gì trình duyệt đã gửi kèm request này. Cookie bạn đặt bây giờ chỉ xuất hiện trong$_COOKIEở request tiếp theo.- Cookie là header, nên
setcookie()phải chạy trước mọi đầu ra, giống hệtsession_start()vàheader().
Để xóa một cookie, đặt lại nó với cùng tên và path cùng thời điểm hết hạn trong quá khứ: setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']).
Lưu một mảng trong cookie
Giá trị cookie là một chuỗi tối đa khoảng 4 KB, nên hãy lưu dữ liệu có cấu trúc dưới dạng JSON, và coi những gì nhận lại là không đáng tin: người xem có thể thay đổi nó. Không bao giờ unserialize() một cookie: hàm này có thể tạo đối tượng từ các lớp của bạn và chạy các magic method của chúng, trong khi json_decode() chỉ trả về mảng, chuỗi, số, boolean, null hoặc stdClass.
Trang json_encode giải thích các cờ. Với bất cứ thứ gì lớn hơn vài giá trị ngắn, hãy giữ dữ liệu trong session (hoặc cơ sở dữ liệu) và chỉ lưu một khóa trong cookie.
Token "ghi nhớ đăng nhập"
Cookie "ghi nhớ đăng nhập" không được chứa id người dùng hay hash mật khẩu. Hãy lưu một token ngẫu nhiên dài trong cookie và chỉ lưu hash của nó trong cơ sở dữ liệu, rồi so sánh bằng hash_equals():
Các giá trị thay đổi sau mỗi lần chạy, vì random_bytes() là ngẫu nhiên. Nếu cơ sở dữ liệu bị lộ, các hash vô dụng khi không có cookie, và hash_equals() so sánh trong thời gian không đổi nên phép kiểm tra không làm lộ thông tin về thời gian.
Session hay cookie: nên dùng cái nào
Session ($_SESSION) | Cookie ($_COOKIE) | |
|---|---|---|
| Lưu ở | Trên máy chủ | Trong trình duyệt |
| Người xem đọc hoặc sửa được | Không (chỉ thấy id) | Có |
| Kích thước | Tùy vào nơi lưu trữ của bạn | Khoảng 4 KB mỗi cookie |
| Tồn tại | Đến khi đóng trình duyệt hoặc session hết hạn | Đến thời điểm expires |
| Dùng cho | Người dùng đã đăng nhập, giỏ hàng, CSRF token, flash message | Giao diện, ngôn ngữ, lựa chọn đồng ý, token "ghi nhớ đăng nhập" |
Lỗi thường gặp: headers already sent
session_start(), setcookie() và header() đều gửi header HTTP, mà header chỉ gửi được trước phần nội dung. Bất kỳ đầu ra nào trước đó, kể cả một dòng trống trước <?php trong một tệp được include, đều gây ra:
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
Thông báo thứ hai cho biết đầu ra bắt đầu ở đâu (page.php:2): đến dòng đó và bỏ phần đầu ra đi, hoặc chuyển session_start() lên trên nó. Trong những tệp chỉ chứa PHP, bỏ thẻ đóng ?> sẽ tránh được khoảng trắng thừa phía sau nó trở thành đầu ra.
Câu hỏi thường gặp
Session và cookie trong PHP khác nhau thế nào?
Cookie được lưu trong trình duyệt của người xem và gửi kèm mỗi request, nên người xem đọc và sửa được nó. Session lưu dữ liệu trên máy chủ; trình duyệt chỉ giữ một session id ngẫu nhiên trong một cookie (PHPSESSID). Mọi thứ cần được tin cậy, như id của người dùng đã đăng nhập, hãy đặt trong $_SESSION.
Làm sao hủy session khi đăng xuất trong PHP?
Làm rỗng dữ liệu, xóa cookie session và hủy session đã lưu: $_SESSION = [];, rồi setcookie(session_name(), '', time() - 3600, '/'), rồi session_destroy();. Sau đó chuyển hướng để trang tiếp theo bắt đầu lại từ đầu.
Làm sao xóa một cookie trong PHP?
Đặt lại cookie với cùng tên, path và domain, cùng thời điểm hết hạn trong quá khứ: setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);. Thêm unset($_COOKIE['theme']) nếu phần còn lại của script hiện tại còn đọc nó, vì $_COOKIE chỉ thay đổi ở request tiếp theo.
Một session PHP tồn tại bao lâu?
Mặc định cookie session tồn tại đến khi đóng trình duyệt (session.cookie_lifetime = 0) và máy chủ có thể xóa dữ liệu session sau 1440 giây (24 phút) không hoạt động (session.gc_maxlifetime). Để có thời gian chờ cố định, hãy lưu thời điểm của request gần nhất vào session và tự kiểm tra.
Vì sao session_start() báo headers already sent?
Session id được gửi đi trong một header cookie, mà header phải được gửi trước mọi đầu ra. Một echo, đoạn HTML, hay thậm chí một dòng trống hoặc BOM trước <?php cũng khiến đầu ra được gửi trước. Hãy gọi session_start() ở ngay đầu script, trước khi in ra bất cứ thứ gì.