באתר עם חברים (חנות, קהילה, אזור אישי) צריך לזהות מבקר מחובר. הליבה מספקת לכך את המחלקה LOGIN: כניסה לפי שם משתמש וסיסמה, טוקן בעוגייה שמאומת מול טבלה, והגבלה של מספר מכשירים מחוברים. היא נפרדת לגמרי ממנהלי הפאנל (ראו התחברות וסשנים). בעמוד הזה: ההגדרות, הטבלאות, דוגמה מלאה להרשמה, כניסה ויציאה, וההגבלות שחשוב להכיר.
system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url. המחלקה נמצאת ב-system/collections/LOGIN.php.
אף קוד בליבה לא קורא ל-LOGIN: זו ספרייה לקוד האתר. הליבה לא מגיעה עם דפי הרשמה, שחזור סיסמה או ניהול משתמשים, ואת אלה כותבים באתר. עוגיות וסשן כלליים מתוארים בעוגיות וסשן.
שתי שיטות, ואחת מומלצת#
LOGIN | COOKIES::user_* (ישן) | |
|---|---|---|
| הטוקן | MISC::encode(json) מאומת מול CRM_users_logins | MISC::encode([...]) בלבד, ללא טבלה |
| שם עוגייה | CRM_login_<table> | CRM_user_<table> |
| מגבלת מכשירים | כן ($max_devices, ברירת מחדל 3) | לא |
| יציאה בצד שרת | כן (מוחק את הטוקן מהטבלה) | רק מוחק את העוגייה |
| גיבוב סיסמה | md5, bcrypt או wp | md5 בלבד |
השיטה הישנה פועלת על כל טבלה בשם $tableName ותומכת רק ב-md5, ולכן אתרים חדשים צריכים להשתמש ב-LOGIN. פירוט השיטה הישנה בעוגיות וסשן.
הגדרה#
המשתנים הסטטיים נקבעים ב-application/includes/init.php:
// application/includes/init.php
LOGIN::$table = "users"; // ברירת מחדל: users (הטבלה CRM_users)
LOGIN::$encryption = "bcrypt"; // md5 (ברירת מחדל) | bcrypt | wp
LOGIN::$max_devices = 3; // 0 = בלי מגבלה
LOGIN::$login_use_ip = false; // true = הטוקן תקף רק מכתובת ה-IP שהונפק לה
LOGIN::$encryption ברירת המחדל היא md5, גיבוב מהיר בלי salt שנשבר בקלות. הגדירו bcrypt לפני הרשמת המשתמש הראשון, כי מעבר מאוחר מחייב לאפס סיסמאות או לגבב מחדש בכל כניסה.אם $encryption = "wp", הקוד משתמש במחלקה PasswordHash (פורמט הסיסמאות של WordPress) שהליבה לא כוללת. השתמשו בזה רק באתר שמטמיע אותה בעצמו, למשל בעת העברה מ-WordPress.
הטבלאות#
הטבלאות CRM_users ו-CRM_users_logins לא נוצרות אוטומטית על ידי עדכון הליבה; יוצרים אותן ידנית. הסכמה המינימלית שהקוד דורש:
CREATE TABLE `CRM_users` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_user` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `CRM_users_logins` (
`id` INT NOT NULL AUTO_INCREMENT,
`uid` INT NOT NULL,
`token` TEXT NOT NULL,
`ip` VARCHAR(45) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `idx_uid` (`uid`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
מותר להוסיף עמודות (אימייל, שם, תאריך הרשמה), והליבה תתעלם מהן. username חייב להיות ייחודי: הכניסה מחפשת שורה לפי שם המשתמש ולוקחת את הראשונה. הטבלה נקראת דרך ה-builder כ-users (בלי CRM_).
אם הטבלה שלכם נקראת אחרת, הגדירו LOGIN::$table; ה-builder יפנה אליה, אבל טבלת הטוקנים CRM_users_logins קבועה בקוד ואינה ניתנת לשינוי.
המתודות#
| מתודה | פרמטרים | החזרה |
|---|---|---|
LOGIN::login_by_details($username, $password, $sessTime = 24*7*30*12) | שם משתמש וסיסמה בטקסט גלוי. $sessTime בשעות | true כשהעוגייה נשלחה, false כשהכניסה נכשלה |
LOGIN::login_by_id($id, $sessTime = 24*7*30*12) | מזהה משתמש. דורש שורה עם active != "0" | כמו למעלה |
LOGIN::is_logged() | bool | |
LOGIN::get_id() | מזהה משתמש (int), או false כשלא מחובר | |
LOGIN::get_token() | ערך העוגייה, או false | |
LOGIN::logout() | אין (מוחק עוגייה ושורת טוקן) |
איך כניסה עובדת#
login_by_detailsמנקה את שם המשתמש (strtolower,trim) ואת הסיסמה (trim), מחפשת שורה לפיusername, ומשווה לפיLOGIN::$encryption. שורה לא קיימת או סיסמה שגויה מחזירותfalseללא הבחנה.login_by_idבודקת שהמשתמש פעיל, בונה טוקןMISC::encode(json_encode(["uid", "ip", "tbl", "time"])), מוסיפה שורה ל-CRM_users_logins(uid,token,ip) ושולחת עוגייהCRM_login_<table>דרךCOOKIES::set. העוגייה נשלחת עםsecureבלבד ללאhttponly, כלומר היא עובדת רק ב-HTTPS וקוד JavaScript יכול לקרוא אותה.is_loggedמפענחת את הטוקן, ואם$max_devices > 0בודקת שהוא נמצא בין$max_devicesהטוקנים האחרונים של אותו משתמש. כניסה רביעית (במגבלה 3) מבטלת בפועל את הוותיקה ביותר.
הכניסה ממירה את שם המשתמש לאותיות קטנות ומסירה רווחים בקצוות, וגם את הסיסמה היא מנקה מרווחים בקצוות. בהרשמה שמרו את שם המשתמש באותיות קטנות ואת הסיסמה אחרי trim, אחרת המשתמש לא יוכל להיכנס. כמו כן שם משתמש שמכיל גרש (') לא יתאים, כי הערך עובר DB::escape ידני וגם escape של ה-builder; מומלץ להגביל שם משתמש לאותיות, ספרות וסימנים פשוטים, או להשתמש באימייל בלבד.
דוגמה מלאה#
קונטרולר account עם הרשמה, כניסה ויציאה. על כתיבת קונטרולר והרישום שלו ב-CRM_modules ראו קונטרולרים.
<?php
// application/controllers/account.php
class account extends wz_controller
{
// GET /account : עמוד אזור אישי, רק למחוברים
function index()
{
if (!LOGIN::is_logged()) return "REQUIRE_LOGIN";
$user = DB::get_val("users", LOGIN::get_id());
return $this->view("account/home", ["user" => $user]);
}
// POST /account/register
function register()
{
$_GET["pmode"] = "empg";
header("Content-Type: application/json; charset=utf-8");
$email = strtolower(trim($_POST["email"] ?? ""));
$pass = trim($_POST["password"] ?? "");
if (!MISC::is_valid_email($email) || strlen($pass) < 8) {
return json_encode(["ok" => false, "error" => "invalid"]);
}
if (DB::get_val("users", ["username" => $email])) {
return json_encode(["ok" => false, "error" => "exists"]);
}
// dt_encode מחזירה גיבוב bcrypt, והיא נטענת עם הספרייה bcrypt
$this->load->library("bcrypt");
$u = DB::update("users");
$u->set("username", $email);
$u->set("password", dt_encode($pass));
$u->set("active", 1);
if (!$u->insert()) return json_encode(["ok" => false, "error" => "db"]);
// כניסה מיידית אחרי ההרשמה, 30 יום (720 שעות)
LOGIN::login_by_id($u->new_id, 24 * 30);
return json_encode(["ok" => true]);
}
// POST /account/login
function login()
{
$_GET["pmode"] = "empg";
header("Content-Type: application/json; charset=utf-8");
$ok = LOGIN::login_by_details($_POST["email"] ?? "", $_POST["password"] ?? "", 24 * 30);
return json_encode(["ok" => (bool)$ok]);
}
// POST /account/logout
function logout()
{
$_GET["pmode"] = "empg";
// במקום LOGIN::logout(): הטוקן עובר escape לפני שהוא נכנס ל-SQL
$t = LOGIN::get_token();
if ($t) DB::sql("DELETE FROM CRM_users_logins WHERE token = '" . DB::escape($t) . "'");
COOKIES::kill("login_" . LOGIN::$table);
return "";
}
}
הרישום ב-CRM_modules:
INSERT INTO CRM_modules (modulesDir, moduleName, active, ord) VALUES ('', 'account', 1, 0);
ולאחר מכן ניקוי מטמון המודולים: cache_engine::remove("modules"); (ראו קונטרולרים).
כל מתודה ציבורית היא כתובת, ולכן מומלץ לבדוק $_SERVER["REQUEST_METHOD"] === "POST" בפעולות שמשנות מצב, ולצרף להן הגנת CSRF (ראו CSRF, XSS ו-SQL injection). LOGIN לא מגינה על הבקשות עצמן.
$u->new_id
אחרי insert() המזהה החדש נמצא ב-$u->new_id; הדוגמה משתמשת בו כדי להיכנס מיד אחרי ההרשמה. DB::insert לא קיימת (הבילדרים).בדיקת מחובר בתבנית ובקוד#
LOGIN::is_logged() יכולה להיקרא מכל מקום, וכל קריאה מפענחת את העוגייה ופונה פעם אחת ל-DB כש-$max_devices > 0. אם אתם קוראים לה כמה פעמים בבקשה, שמרו את התוצאה במשתנה.
דף שמוגש ממטמון עמוד לא יכול להכיל תוכן אישי: הדף נשמר פעם אחת ומוגש לכולם. עמודים שתלויים במחובר לא מסמנים PAGE::$cache_this_page, או שהתוכן האישי נטען בקריאת AJAX נפרדת.
מגבלות ומלכודות#
- אין הגבלת ניסיונות.
LOGINלא סופרת כניסות כושלות ולא חוסמת. הוסיפו מגבלה משלכם (לפי IP ולפי שם משתמש) עלaccount/login, ותגובה אחידה לכל כישלון. - הטוקן לא פג בשרת. שורת הטוקן ב-
CRM_users_loginsנמחקת רק ביציאה, והשדהtimeבטוקן לא נבדק. הפקיעה היא פקיעת העוגייה בדפדפן בלבד (ברירת מחדל:24*7*30*12שעות, כשבע שנים). הטבלה גדלה עם כל כניסה, ורק$max_devicesהטוקנים האחרונים של כל משתמש בתוקף. מומלץ לנקות שורות ישנות במשימת cron, בלי למחוק את הטוקנים האחרונים של המשתמשים. - שינוי סיסמה לא מנתק מכשירים. כדי לנתק את כל המכשירים של משתמש, מחקו את שורות
CRM_users_loginsשלו (למשל בעת שינוי סיסמה או חסימה). חסימה דרךactive = 0חוסמת כניסות חדשות בלבד:is_loggedלא בודקת אתactive, ולכן טוקן קיים ממשיך לעבוד עד שמוחקים אותו. logout()משרשרת את ערך העוגייה ל-SQL ללא escape. קוד הליבה מוחק את השורה עםDB::sql("DELETE ... WHERE token = '" . $token . "'"), והערך מגיע מעוגייה שהלקוח שולט בה. העוגייה היא קלט של הלקוח, ולכן בקוד האתר מומלץ לא לקרוא ל-LOGIN::logout(), ובמקומה:$t = LOGIN::get_token(); if ($t) DB::sql("DELETE FROM CRM_users_logins WHERE token = '" . DB::escape($t) . "'"); COOKIES::kill("login_" . LOGIN::$table);.- הטוקן מוצפן, לא חתום. הוא נוצר עם
MISC::encode(ראו הצפנה). אם מפתחות ההצפנה נשארו בברירת המחדל, מי שמכיר אותם יכול לייצר עוגייה. עם$max_devices > 0זה נעצר כי הטוקן חייב להימצא גם בטבלה; עם0ההגנה היחידה היא סודיות המפתחות. הגדירו מפתחות ייחודיים (הקשחה) ואל תכבו את מגבלת המכשירים. $login_use_ip = trueנועל את הטוקן לכתובת ה-IP שהונפק לה. משתמשי נייד ורשתות שמחליפות כתובת ינותקו, ולכן ברירת המחדל היאfalse.