EzCode # Bài 15 — Đăng nhập, đăng xuất, phân quyền hiển thị
Học xong bài này bạn sẽ:
- Xác thực mật khẩu bằng
password_verify()mà không cần biết mật khẩu gốc- Ghi trạng thái đăng nhập vào session và chuyển hướng theo vai trò
- Làm menu tự đổi theo việc người dùng đã đăng nhập hay chưa
Cần học trước: Bài 14
1. Vấn đề đặt ra
Ở Bài 14, bạn lưu mật khẩu dưới dạng chuỗi băm 60 ký tự không đọc được:
$2y$10$9bgjTjuatn1OhdtuaygleepnGSYjvGPkZgQXoflzqfhA8S15zaNl2
Bây giờ người dùng gõ 12345abcd vào ô mật khẩu. Làm sao đối chiếu?
Bạn không thể giải mã chuỗi băm — hàm băm là một chiều, đó chính là điểm mạnh của nó.
Bạn cũng không thể băm lại 12345abcd rồi so sánh trực tiếp, vì mỗi lần băm cho ra muối khác nhau nên kết quả sẽ khác.
Bài 14 mục 6 đã hé lộ câu trả lời: password_verify(). Bài này dùng nó để dựng chức năng đăng nhập hoàn chỉnh, và làm cho menu trên đầu trang biết bạn là ai.
2. Kiến thức mới
2.1. password_verify()
<?php
if (password_verify($matKhauNguoiGo, $chuoiBamTrongDb)) {
echo "Đúng mật khẩu";
}
?>
Hàm này làm bốn bước, như Bài 14 đáp án 3 đã giải thích:
- Đọc phần đầu chuỗi băm để biết thuật toán và hệ số công việc
- Cắt lấy 22 ký tự muối từ chính chuỗi đó
- Băm lại mật khẩu vừa gõ bằng đúng muối và hệ số đó
- So kết quả với phần băm gốc
Trả về true hoặc false. Không bao giờ trả về mật khẩu.
Thứ tự hai tham số rất quan trọng. Viết ngược lại password_verify($chuoiBam, $matKhau) sẽ luôn trả về false, và bạn sẽ ngồi gỡ lỗi rất lâu vì không có thông báo nào cả.
Cách nhớ: mật khẩu người gõ trước, chuỗi trong database sau.
2.2. Kiểm tra hai điều kiện trong một câu
$user = $userModel->getByEmail($email);
if ($user && password_verify($password, $user['password'])) {
Câu điều kiện có hai vế nối bằng &&:
$user— có tìm thấy người dùng với email đó khôngpassword_verify(...)— mật khẩu có đúng không
Thứ tự bắt buộc phải như vậy. PHP dùng cơ chế đánh giá tắt: với &&, nếu vế trái đã sai thì vế phải không được chạy.
Nhờ đó khi email không tồn tại, $user là false, và PHP bỏ qua password_verify(). Nếu chạy, nó sẽ truy cập $user['password'] trên một giá trị false và sinh cảnh báo.
Thử đảo thứ tự:
if (password_verify($password, $user['password']) && $user) { // SAI
Với email không tồn tại, bạn sẽ nhận Warning: Trying to access array offset on value of type bool.
Đây là mẫu bạn sẽ gặp lại nhiều lần: kiểm tra sự tồn tại trước, kiểm tra nội dung sau.
2.3. Vì sao thông báo lỗi phải mơ hồ
$errors[] = "Email hoặc mật khẩu không đúng";
Chỉ một thông báo duy nhất cho cả hai trường hợp: email không tồn tại, và mật khẩu sai.
Nghe có vẻ kém thân thiện. Nhưng nếu bạn tách ra:
if (!$user) {
$errors[] = "Email này chưa đăng ký"; // NGUY HIỂM
} elseif (!password_verify(...)) {
$errors[] = "Mật khẩu không đúng"; // NGUY HIỂM
}
thì bạn vừa tạo ra một công cụ dò tìm tài khoản.
Kẻ tấn công viết một đoạn script thử hàng nghìn email. Email nào nhận thông báo "Mật khẩu không đúng" nghĩa là email đó có tài khoản trên hệ thống. Họ thu được một danh sách người dùng thật để tấn công tiếp — thử mật khẩu phổ biến, gửi email lừa đảo nhắm đúng đối tượng.
Với một website học tập thì hậu quả còn nhẹ. Với website hẹn hò, y tế, hay tài chính thì việc lộ "người này có tài khoản ở đây" đã là vi phạm quyền riêng tư nghiêm trọng.
Nguyên tắc: thông báo đăng nhập thất bại phải giống hệt nhau trong mọi trường hợp.
2.4. Ghi trạng thái đăng nhập vào session
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_name'] = $user['name'];
$_SESSION['user_role'] = $user['role'];
Ba khóa này là toàn bộ trạng thái đăng nhập của EzCode. Mọi kiểm tra quyền trong 8 bài còn lại đều dựa vào chúng.
| Khóa | Dùng để |
|---|---|
user_id |
Kiểm tra đã đăng nhập chưa; làm khóa truy vấn dữ liệu của người dùng |
user_name |
Hiện "Xin chào, ..." trên menu |
user_role |
Chặn quyền vào khu vực giảng viên |
Vì sao lưu role vào session thay vì hỏi database mỗi lần?
Ưu điểm: nhanh hơn nhiều. Mỗi lần kiểm tra quyền chỉ là đọc một biến trong bộ nhớ, không phải một truy vấn database.
Nhược điểm: dữ liệu có thể cũ. Nếu quản trị viên đổi vai trò của một người từ teacher thành student, người đó vẫn giữ quyền giảng viên cho tới khi đăng xuất và đăng nhập lại.
Với EzCode thì chấp nhận được vì vai trò hầu như không đổi. Với hệ thống có phân quyền phức tạp thì phải hỏi lại database, hoặc có cơ chế buộc đăng xuất khi quyền thay đổi.
Vì sao không lưu cả mảng $user vào session?
Vì mảng đó chứa cả password — chuỗi băm. Đưa nó vào session là để dữ liệu nhạy cảm ở thêm một nơi nữa mà không cần thiết. Chỉ lưu đúng ba thứ cần dùng là lựa chọn đúng.
2.5. Chuyển hướng theo vai trò
if ($user['role'] === 'teacher') {
header("Location: ?ctrl=teacher&act=dashboard");
} else {
header("Location: ?ctrl=page&act=home");
}
exit();
Giảng viên vào thẳng dashboard, học viên về trang chủ.
Chú ý exit() nằm sau cả khối if/else, không lặp lại trong từng nhánh. Dù đi nhánh nào cũng dừng ở đó. Cách viết này gọn và không thể quên.
Dùng === ba dấu bằng để so chuỗi — an toàn hơn == như Bài 2 mục 2.4 đã bàn.
2.6. Xác thực và phân quyền
Hai khái niệm này hay bị nhầm, nhưng chúng khác nhau rõ ràng:
| Xác thực (Authentication) | Phân quyền (Authorization) | |
|---|---|---|
| Trả lời câu hỏi | Bạn là ai? | Bạn được làm gì? |
| Xảy ra khi nào | Một lần, lúc đăng nhập | Mỗi lần truy cập tài nguyên |
| Trong EzCode | password_verify() và ghi $_SESSION |
Kiểm tra $_SESSION['user_role'] |
Bài này làm phần xác thực. Phân quyền thật sự nằm ở Bài 17, khi TeacherController::__construct() chặn học viên khỏi khu vực giảng viên.
Một điều quan trọng cần phân biệt ngay: đổi giao diện theo vai trò không phải là phân quyền.
Ẩn nút "Dashboard" khỏi menu của học viên chỉ là tiện ích giao diện. Học viên vẫn gõ thẳng ?ctrl=teacher&act=dashboard vào thanh địa chỉ được.
Phân quyền thật phải nằm ở phía máy chủ, trong Controller. Bài 17 sẽ làm.
2.7. Menu đổi theo trạng thái
<?php if (isset($_SESSION['user_id'])): ?>
<li><a href="?ctrl=user&act=profile">Xin chào, <?= htmlspecialchars($_SESSION['user_name']) ?></a></li>
<?php if ($_SESSION['user_role'] === 'teacher'): ?>
<li><a href="?ctrl=teacher&act=dashboard">Dashboard</a></li>
<?php endif; ?>
<li><a href="?ctrl=user&act=logout">Đăng xuất</a></li>
<?php else: ?>
<li><a href="?ctrl=user&act=register">Đăng ký</a></li>
<li><a href="?ctrl=user&act=login">Đăng nhập</a></li>
<?php endif; ?>
Hai tầng if lồng nhau:
- Tầng ngoài: đã đăng nhập hay chưa
- Tầng trong: nếu đã đăng nhập, có phải giảng viên không
Ba trạng thái menu có thể xảy ra:
| Trạng thái | Menu hiện |
|---|---|
| Khách | Trang chủ, Khóa học, Đăng ký, Đăng nhập |
| Học viên | Trang chủ, Khóa học, Xin chào ..., Đăng xuất |
| Giảng viên | Trang chủ, Khóa học, Xin chào ..., Dashboard, Đăng xuất |
Mỗi if: phải có endif; riêng, và thứ tự đóng phải ngược thứ tự mở — giống dấu ngoặc.
3. Áp dụng vào EzCode
3.1. Thêm login và logout vào Controller
Chèn vào Controllers/UserController.php sau phương thức register().
📁
Controllers/UserController.php— sửa
public function login()
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
$errors = [];
if (empty($email)) $errors[] = "Email không được để trống";
if (empty($password)) $errors[] = "Mật khẩu không được để trống";
if (empty($errors)) {
include_once "./Models/User.php";
$userModel = new User();
$user = $userModel->getByEmail($email);
if ($user && password_verify($password, $user['password'])) {
// Đăng nhập thành công
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_name'] = $user['name'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['success'] = "Đăng nhập thành công!";
// Redirect dựa vào role
if ($user['role'] === 'teacher') {
header("Location: ?ctrl=teacher&act=dashboard");
} else {
header("Location: ?ctrl=page&act=home");
}
exit();
} else {
$errors[] = "Email hoặc mật khẩu không đúng";
}
}
}
include_once "./Views/user_login.php";
}
public function logout()
{
session_destroy();
header("Location: ?ctrl=page&act=home");
exit();
}
Phương thức login() theo đúng khuôn mẫu form ở Bài 14 mục 2.5: kiểm tra POST, lấy dữ liệu, gom lỗi, xử lý, chuyển hướng, và nạp View ở ngoài khối if.
Điểm khác so với register(): chỉ có hai quy tắc kiểm tra cơ bản. Không kiểm tra định dạng email — vì nếu email sai định dạng thì nó cũng không tồn tại trong database, và thông báo "Email hoặc mật khẩu không đúng" đã đủ.
logout() chỉ ba dòng, bạn đã đọc kỹ ở Bài 5 mục 3.2.
Một điểm chưa hoàn thiện của
logout(): nó không đặt thông báo nào. Người dùng bấm Đăng xuất và bị đưa về trang chủ trong im lặng, không có xác nhận nào.Bài 5 đáp án 3 đã giải thích vì sao thêm thông báo lại khó:
session_destroy()xóa luôn thông báo vừa ghi. Giải pháp là hủy phiên rồi mở phiên mới. Bạn có thể tự làm nếu muốn.
3.2. Tạo View đăng nhập
📁
Views/user_login.php— tạo mới
<main class="register-page">
<div class="register-container">
<h2>Đăng nhập</h2>
<?php if (isset($_SESSION['success'])): ?>
<div style="background-color: #d4edda; color: #155724; padding: 10px; border-radius: 5px; margin-bottom: 15px;">
<?= $_SESSION['success'] ?>
</div>
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error'])): ?>
<div style="background-color: #f8d7da; color: #721c24; padding: 10px; border-radius: 5px; margin-bottom: 15px;">
<?= $_SESSION['error'] ?>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<?php if (isset($errors) && !empty($errors)): ?>
<div style="background-color: #f8d7da; color: #721c24; padding: 10px; border-radius: 5px; margin-bottom: 15px;">
<?php foreach ($errors as $error): ?>
<div><?= $error ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form method="POST" class="register-form">
<div>
<label for="email">Email *</label>
<input type="email" id="email" name="email" value="<?= isset($_POST['email']) ? htmlspecialchars($_POST['email']) : '' ?>" required>
</div>
<div>
<label for="password">Mật khẩu *</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Đăng nhập</button>
</form>
<div style="margin-top: 20px; text-align: center;">
<p>Chưa có tài khoản? <a href="?ctrl=user&act=register" style="color: #0868b4;">Đăng ký ngay</a></p>
</div>
</div>
</main>
File này có ba khối thông báo, nhiều hơn user_register.php một khối:
$_SESSION['success']— màu xanh, dùng cho thông báo "Đăng ký thành công! Vui lòng đăng nhập." từ Bài 14$_SESSION['error']— màu đỏ, dùng cho thông báo từ trang khác chuyển tới$errors— màu đỏ, lỗi ngay trong lần gửi form này
Đây là View đầy đủ nhất về mặt thông báo trong cả project. Nhiều View khác chỉ có một hoặc hai khối, và course_list.php thì không có khối nào — đúng vấn đề mà Bài 13 bài tập 3 đã phát hiện.
Class CSS dùng lại register-page và register-container của trang đăng ký. Hợp lý: hai trang có bố cục giống nhau, không cần CSS riêng.
Ô email có giữ lại giá trị, ô mật khẩu không — đúng nguyên tắc ở Bài 14 mục 2.8.
3.3. Hiểu phần menu động
Bạn đã gõ đoạn này từ Bài 10 nhưng chưa hiểu hết. Giờ nó bắt đầu hoạt động.
📁
Views/layout_header.php— sửa
<?php if (isset($_SESSION['user_id'])): ?>
<!-- User đã đăng nhập -->
<li><a href="?ctrl=user&act=profile">Xin chào, <?= htmlspecialchars($_SESSION['user_name']) ?></a></li>
<?php if ($_SESSION['user_role'] === 'teacher'): ?>
<li><a href="?ctrl=teacher&act=dashboard">Dashboard</a></li>
<?php endif; ?>
<li><a href="?ctrl=user&act=logout">Đăng xuất</a></li>
<?php else: ?>
<!-- User chưa đăng nhập -->
<li><a href="?ctrl=user&act=register">Đăng ký</a></li>
<li><a href="?ctrl=user&act=login">Đăng nhập</a></li>
<?php endif; ?>
Không cần sửa gì — đoạn này đã có sẵn từ Bài 10. Từ bây giờ nhánh trên mới chạy được, vì $_SESSION['user_id'] đã được login() ghi vào.
Đây là chỗ đáng học nhất về mặt phong cách:
<?= htmlspecialchars($_SESSION['user_name']) ?>
Dòng này có lọc HTML. Người dùng đặt tên là <script>alert(1)</script> khi đăng ký, thì tên đó hiện ra nguyên văn trên menu chứ không chạy.
Hãy tự thử: đăng ký một tài khoản với họ tên là <b>Test</b>, rồi đăng nhập. Menu sẽ hiện đúng chuỗi <b>Test</b> chứ không phải chữ Test in đậm.
⚠️ Lưu ý: So sánh dòng trên với dòng ngay trong
Views/user_login.phpmà bạn vừa viết:<?= $_SESSION['error'] ?> <!-- không lọc -->Cùng đọc từ
$_SESSION, cùng in ra HTML, nhưng một chỗ cóhtmlspecialchars()một chỗ không.Sự thiếu nhất quán này là lỗi số 10 trong Bài 22, và nó cho thấy vì sao lọc dữ liệu phải là thói quen tự động chứ không phải quyết định từng lần. Chỉ cần một chỗ quên là lỗ hổng mở ra.
Bài 22 bài tập 2 sẽ dựng một hàm
e()dùng chung để không bao giờ quên nữa.
4. Chạy thử
Thử 1 — Đăng nhập bằng tài khoản học viên.
Mở ?ctrl=user&act=login. Điền:
- Email:
hhoang02052004@gmail.com - Mật khẩu:
12345abcd
Kết quả:
- Bạn được đưa về trang chủ
- Menu đổi thành: Trang chủ, Khóa học, Xin chào, Hoàng Nguyễn, Đăng xuất
- Không có mục Dashboard
Thử 2 — Kiểm tra session. Tạo tạm hoc/xem_session.php:
<?php
session_start();
echo "<pre>";
print_r($_SESSION);
echo "</pre>";
?>
Mở nó, bạn thấy:
Array
(
[user_id] => 34
[user_name] => Hoàng Nguyễn
[user_role] => student
)
Đúng ba khóa mà login() đã ghi. Khóa success không còn vì trang chủ... thực ra vẫn còn, vì Views/page_home.php không có khối hiển thị flash nên không ai unset() nó.
Đây là một phát hiện nhỏ: thông báo "Đăng nhập thành công!" mà login() ghi vào không bao giờ được hiện ra khi đăng nhập bằng tài khoản học viên, vì trang chủ không hiển thị flash message. Nó sẽ nằm im trong session cho tới khi bạn mở một trang có khối hiển thị.
Thử 3 — Đăng nhập bằng tài khoản giảng viên.
Đăng xuất trước, rồi đăng nhập với:
- Email:
nguyenhuyhoang2004k3a@gmail.com - Mật khẩu:
12345
Kết quả: bạn được chuyển tới ?ctrl=teacher&act=dashboard, và nhận lỗi:
Warning: include_once(./Controllers/TeacherController.php): Failed to open stream
Đúng như dự kiến — TeacherController tới Bài 17 mới có. Hãy quay về trang chủ bằng cách gõ địa chỉ, và bạn sẽ thấy menu có mục Dashboard.
Thử 4 — Sai mật khẩu. Đăng xuất, rồi đăng nhập với email đúng nhưng mật khẩu sai. Kết quả: Email hoặc mật khẩu không đúng.
Thử tiếp với email hoàn toàn không tồn tại. Kết quả: thông báo y hệt. Đúng nguyên tắc ở mục 2.3.
Thử 5 — Tài khoản mẫu cũ. Đăng nhập bằng teacher1@example.com với bất kỳ mật khẩu nào. Luôn thất bại.
Lý do đã giải thích ở Bài 7 đáp án 3: cột password của tài khoản đó chứa chuỗi giả hashedpassword1, không phải chuỗi băm hợp lệ. password_verify() không đọc được và trả về false.
Thử 6 — Tài khoản bạn tự tạo ở Bài 14. Đăng nhập bằng email và mật khẩu bạn đã đăng ký. Thành công, vì password_hash() đã tạo chuỗi băm thật.
Thử 7 — Đăng xuất. Bấm Đăng xuất. Menu trở về Đăng ký và Đăng nhập. Mở lại hoc/xem_session.php — mảng rỗng.
5. Bài tập
Bài tập 1
Thêm ô "Ghi nhớ email" vào form đăng nhập.
Khi người dùng tick ô đó và đăng nhập thành công, lưu email vào cookie sống 30 ngày:
setcookie('remembered_email', $email, time() + 30 * 24 * 60 * 60);
Lần sau mở trang đăng nhập, ô email tự điền sẵn từ $_COOKIE['remembered_email'].
Nếu không tick, xóa cookie đi.
Lưu ý bảo mật: chỉ lưu email, tuyệt đối không lưu mật khẩu vào cookie. Hãy tự giải thích vì sao trước khi xem đáp án.
Bài tập 2
Bốn phương thức của UserController mà bạn sẽ viết ở Bài 16 đều bắt đầu bằng đúng ba dòng giống nhau:
if (!isset($_SESSION['user_id'])) {
header("Location: ?ctrl=user&act=login");
exit();
}
Hãy viết một phương thức private tên kiemTraDangNhap() để gom chúng lại, rồi dùng nó thay cho ba dòng đó.
Sau đó trả lời: gom lại như vậy tốt hơn ở điểm nào?
Bài tập 3
Trả lời bằng lời, không cần code:
Một người dùng đang đăng nhập với vai trò teacher. Quản trị viên vào phpMyAdmin đổi cột role của người đó thành student.
- Người đó có mất quyền vào dashboard ngay không? Vì sao?
- Phải làm gì để mất quyền?
- Đề xuất một cách sửa để việc đổi vai trò có hiệu lực ngay, và nêu nhược điểm của cách đó.
6. Đáp án
Đáp án bài tập 1
📄 Views/user_login.php — thêm ô tick vào form
<div>
<label>
<input type="checkbox" name="remember" value="1"
<?= isset($_COOKIE['remembered_email']) ? 'checked' : '' ?>>
Ghi nhớ email
</label>
</div>
Và sửa ô email để ưu tiên lấy từ cookie:
<input type="email" id="email" name="email"
value="<?= htmlspecialchars($_POST['email'] ?? $_COOKIE['remembered_email'] ?? '') ?>" required>
📄 Controllers/UserController.php — thêm vào nhánh đăng nhập thành công
if ($user && password_verify($password, $user['password'])) {
// Ghi nhớ email nếu người dùng tick
if (isset($_POST['remember'])) {
setcookie('remembered_email', $email, time() + 30 * 24 * 60 * 60, '/');
} else {
setcookie('remembered_email', '', time() - 3600, '/');
}
$_SESSION['user_id'] = $user['id'];
// ...phần còn lại giữ nguyên...
Về setcookie():
| Tham số | Ý nghĩa |
|---|---|
'remembered_email' |
Tên cookie |
$email |
Giá trị |
time() + 30*24*60*60 |
Thời điểm hết hạn, tính bằng giây từ 1970 |
'/' |
Đường dẫn — / nghĩa là mọi trang trên tên miền |
Cách xóa cookie: đặt thời điểm hết hạn vào quá khứ bằng time() - 3600. Trình duyệt thấy cookie đã hết hạn và tự bỏ đi.
Chuỗi $_POST['email'] ?? $_COOKIE['remembered_email'] ?? '' dùng hai toán tử ?? nối tiếp: ưu tiên giá trị vừa gõ, không có thì lấy cookie, không có nữa thì để trống.
Vì sao tuyệt đối không lưu mật khẩu vào cookie?
Bốn lý do, mỗi lý do đủ để cấm:
- Cookie nằm trên máy người dùng. Bấm F12, tab Application, là đọc được nguyên văn.
- Người dùng kiểm soát cookie. Họ sửa được, và mọi tiện ích trình duyệt cũng đọc được.
- Cookie gửi kèm mọi yêu cầu. Nếu website chưa có HTTPS, mật khẩu bay qua mạng dưới dạng chữ thường trong từng lần tải trang.
- Máy tính dùng chung. Ở quán net hay máy công ty, người tiếp theo mở trình duyệt là có mật khẩu của bạn.
Chức năng "ghi nhớ đăng nhập" thật của các website lớn hoạt động khác hẳn: họ sinh một token ngẫu nhiên dùng một lần, lưu chuỗi băm của token đó trong database, và cookie chỉ chứa token. Token bị lộ thì thu hồi được; mật khẩu bị lộ thì không.
Đáp án bài tập 2
📄 Controllers/UserController.php
private function kiemTraDangNhap()
{
if (!isset($_SESSION['user_id'])) {
header("Location: ?ctrl=user&act=login");
exit();
}
}
Dùng ở đầu mỗi phương thức cần đăng nhập:
public function profile()
{
$this->kiemTraDangNhap();
include_once "./Models/User.php";
// ...
}
Ba lý do gom lại tốt hơn:
-
Sửa một chỗ. Muốn đổi thành ghi thêm thông báo "Vui lòng đăng nhập trước", bạn sửa một phương thức thay vì bốn chỗ. Sót một chỗ là lỗ hổng.
-
Không thể quên. Khi thêm phương thức mới, một dòng
$this->kiemTraDangNhap();dễ nhớ hơn ba dòng. Và nếu quên thì dễ phát hiện hơn khi đọc lại code. -
Tên nói lên ý định.
$this->kiemTraDangNhap();đọc là hiểu ngay. Ba dòngif (!isset(...))phải đọc kỹ mới biết nó làm gì.
Vì sao để private?
Vì index.php gọi được mọi phương thức public từ URL — đúng rủi ro mà Bài 9 mục 2.6 đã cảnh báo. Nếu để public, ai đó gõ ?ctrl=user&act=kiemTraDangNhap sẽ chạy được nó.
Ở đây thì vô hại, nhưng nguyên tắc là: phương thức nội bộ luôn để private.
Vì sao EzCode không làm sẵn? Vì đây là project học tập, và tác giả viết theo cách trực tiếp nhất. Bạn có thể áp dụng cải tiến này — nó không làm lệch cấu trúc so với repo, chỉ gọn hơn.
Cách làm chuẩn hơn nữa là dùng middleware: một lớp kiểm tra chạy tự động trước mọi phương thức, giống như TeacherController::__construct() ở Bài 17. Bạn hoàn toàn có thể áp dụng cùng kỹ thuật đó cho UserController — nhưng cẩn thận, vì register() và login() phải cho phép người chưa đăng nhập vào.
Đáp án bài tập 3
Câu 1 — Người đó có mất quyền ngay không?
Không. Người đó vẫn vào được dashboard bình thường.
Lý do: TeacherController::__construct() ở Bài 17 sẽ kiểm tra $_SESSION['user_role'], mà giá trị đó được ghi một lần duy nhất lúc đăng nhập. Nó là một bản sao trong file phiên trên máy chủ, hoàn toàn độc lập với database.
Quản trị viên đổi cột role trong bảng users, nhưng không ai đụng tới file phiên. PHP không có cơ chế nào tự đồng bộ hai nơi đó.
Câu 2 — Phải làm gì để mất quyền?
Người đó phải đăng xuất rồi đăng nhập lại. Khi đó login() chạy lại, đọc role mới từ database và ghi đè vào session.
Hoặc chờ phiên hết hạn — mặc định PHP xóa file phiên sau 24 phút không hoạt động.
Hoặc quản trị viên xóa tay file phiên trên máy chủ, nhưng cách này không thực tế.
Câu 3 — Cách sửa và nhược điểm.
Cách sửa: hỏi lại database mỗi lần kiểm tra quyền, thay vì tin session.
public function __construct()
{
if (!isset($_SESSION['user_id'])) {
header("Location: ?ctrl=user&act=login");
exit();
}
include_once "./Models/User.php";
$userModel = new User();
$user = $userModel->getById($_SESSION['user_id']);
if (!$user || $user['role'] !== 'teacher') {
session_destroy();
header("Location: ?ctrl=user&act=login");
exit();
}
}
Ba nhược điểm:
-
Thêm một truy vấn cho mỗi lần tải trang trong khu vực giảng viên. Với vài chục người dùng thì không đáng kể; với vài nghìn thì là gánh nặng thật.
-
Thêm một kết nối database. Vì mỗi Model tự tạo
new Database()— đúng lỗi số 16 mà Bài 6 đã cảnh báo. Sửa xong lỗi này lại làm lỗi kia nặng thêm. -
Người dùng bị đá ra giữa chừng. Đang soạn dở một khóa học thì bị đăng xuất, mất hết dữ liệu chưa lưu. Trải nghiệm rất tệ nếu quản trị viên đổi nhầm rồi đổi lại.
Cách cân bằng mà các hệ thống thật hay dùng:
- Lưu thêm
$_SESSION['role_checked_at']là thời điểm kiểm tra gần nhất - Chỉ hỏi lại database nếu đã quá 5 phút kể từ lần kiểm tra trước
- Với thao tác nhạy cảm như xóa dữ liệu hay rút tiền thì luôn hỏi lại, không cần chờ
Cách này giảm số truy vấn xuống rất nhiều mà vẫn bảo đảm quyền hạn không lệch quá 5 phút.
Đây là một ví dụ điển hình của đánh đổi trong lập trình: không có lựa chọn nào hoàn hảo, chỉ có lựa chọn phù hợp với quy mô và mức độ nhạy cảm của hệ thống. Với EzCode, cách của tác giả — tin session — là hợp lý.
➡️ Bài tiếp theo: Bài 16 — Trang cá nhân và trang Vào học
⬅️ Về mục lục · Bài trước
All rights reserved