COOKIES, SESSION, LOGIN, CRYPT

רפרנס מלא למחלקות COOKIES, SESSION, LOGIN ו-CRYPT: כל מתודה ציבורית עם חתימה מדויקת, ברירות מחדל, התנהגות בפועל, וההבדל בין הצפנת CRYPT להצפנת MISC::encode.

⏱ 15 דק' קריאה 2682 מילים ערוך דף זה ב-GitHub

ארבע מחלקות סטטיות בתיקיית system/collections/ (api/core/collections/ בריפו) מטפלות בזהות גולש ובמצב בין בקשות: COOKIES כותבת ומוחקת עוגיות, SESSION עוטפת את סשן ה-PHP, LOGIN מנהלת כניסת משתמשי צד-לקוח מול הטבלה users, ו-CRYPT מצפינה מחרוזות. הדף הזה הוא הרפרנס; הסבר מושגי ודוגמאות שימוש מלאות ב-משתמשי צד-לקוח, COOKIES ו-SESSION ו-הצפנה וקידוד.

מה אינו כאן

כניסת האדמין (ADMIN, עוגיית admin_session, טבלת CRM_adminPanel_sessions) היא מנגנון נפרד, מתואר ב-התחברות, סשנים ו-WIZZO ID. היא כן משתמשת ב-COOKIES::set, COOKIES::set_secure ו-COOKIES::kill שמתוארות כאן.

COOKIES#

כל העוגיות שהליבה כותבת נקראות CRM_<name>: הקידומת CRM_ נוספת אוטומטית בכל המתודות, ואתם מעבירים רק את <name>. הדומיין של העוגייה הוא . ואחריו הערך CONFIG::$base_domain (שווה ל-CONFIG_USER::$set_base_domain אם הוגדר, אחרת ל-HTTP_HOST). path הוא / ו-secure תמיד true.

משתנים סטטיים#

משתנהברירת מחדלמשמעות
COOKIES::$login_use_iptrueהאם user_is_logged מחייבת שה-IP הנוכחי זהה ל-IP שבו הונפקה העוגייה (ראו למטה)
COOKIES::$samesite"None"ערך SameSite ש-set() כותבת. set_secure() לא קוראת לו (תמיד Lax)
COOKIES::$prefix"user_"קידומת שם העוגייה של user_log: העוגייה היא CRM_user_<table>

COOKIES::set()#

public static function set($ckiName, $ckiValue, $ckiHours = 0, $base_domain = true)

כותבת עוגייה רגילה ומעדכנת גם את $_COOKIE (כך שקריאה מיידית ל-get רואה אותה).

פרמטרמשמעות
$ckiNameשם בלי CRM_
$ckiValueערך (מחרוזת)
$ckiHoursתוקף בשעות מעכשיו. 0 = עוגיית סשן (נמחקת עם סגירת הדפדפן)
$base_domaintrue: .<CONFIG::$base_domain>. false: מחפש בין platform_data["domain"] ו-allowed_domains את הדומיין השווה ל-base_domain, ואם אין כזה משתמש ב-platform_data["domain"]

מחזירה את תוצאת setcookie (bool). התכונות: secure=true, httponly=false, samesite=COOKIES::$samesite.

COOKIES::set("last_seen", (string)time(), 24 * 30);   // עוגיית CRM_last_seen למשך 30 יום
שימו לב
set() אינה HttpOnly עוגייה שנכתבה ב-set() נגישה ל-JavaScript ונשלחת גם בבקשות cross-site (SameSite=None). אל תשמרו בה ערכי הזדהות. לכל ערך רגיש השתמשו ב-set_secure(). ההתנהגות הזו חלה גם על user_log ועל LOGIN (שניהם כותבים דרך set).

COOKIES::set_secure()#

public static function set_secure($ckiName, $ckiValue, $ckiHours = 0, $base_domain = true)

כמו set() עם httponly=true ו-samesite=Lax קבועים. כך נכתבות עוגיית הסשן של האדמין (admin_session) ועוגיית ה-CSRF (admin_csrf). מחזירה bool.

COOKIES::get(), is_set()#

public static function get($ckiName)
public static function is_set($ckiName)

get מחזירה את הערך מ-$_COOKIE["CRM_<name>"] או false כשאין. is_set מחזירה bool על קיום המפתח.

COOKIES::kill()#

public static function kill($ckiName, $base_domain = true)

מוחקת את העוגייה (setcookie עם תוקף בעבר) ומסירה אותה גם מ-$_COOKIE. לא מחזירה ערך. חשוב: המחיקה לא שולחת secure/samesite, והדומיין חייב להתאים לזה שבו נכתבה העוגייה, לכן העבירו את אותו $base_domain שבו כתבתם.

COOKIES::set_hours(), set_secure_hours()#

public static function set_hours($name, $hours, $base_domain = true)
public static function set_secure_hours($name, $hours, $base_domain = true)

מאריכות עוגייה קיימת: אם is_set($name), קוראות את הערך וכותבות אותו מחדש עם $hours חדשות (דרך set או set_secure). תמיד מחזירות false, גם כשהצליחו, ולכן אל תסתמכו על ערך ההחזרה.

COOKIES::user_log(), user_log_details(), user_logout(), user_is_logged(), user_get_id()#

מנגנון הזדהות ישן לטבלת משתמשים כלשהי. לאתרים חדשים השתמשו ב-LOGIN שמחליף אותו.

public static function user_log($tableName, $id, $sessTime = 72)
public static function user_log_details($tableName, $userName, $pass, $sessTime = 72)
public static function user_logout($tableName)
public static function user_is_logged($tableName)
public static function user_get_id($tableName)
מתודהפעולההחזרה
user_logמוודאת שהשורה $id קיימת ב-$tableName, ושומרת עוגיית CRM_user_<table> (דרך set) שמכילה MISC::encode(["uid", "ip", "tbl"]) לפרק זמן של $sessTime שעותתוצאת set, או false אם אין שורה
user_log_detailsמחפשת שורה עם username = MISC::special_chars($userName) ו-password = md5($pass), ואם נמצאה קוראת ל-user_logכמו user_log, או false
user_logoutkill של העוגייהאין
user_is_loggedמפענחת את העוגייה ובודקת: uid > 0, tbl תואם, שהשורה עדיין קיימת, ושה-IP תואם (אלא אם COOKIES::$login_use_ip === false; כתובות של Wizzo, לפי WIZZO_NET::is_wizzo_strict(), פטורות מבדיקת ה-IP)bool
user_get_iduid מהעוגייה אם user_is_logged, אחרת falseint או false
שים לב

סיסמאות מושוות כ-md5 בלי salt, והזהות כולה יושבת בתוך העוגייה (אין סשן בצד שרת, אין ביטול). user_is_logged רק בודקת שהשורה קיימת, לא שהמשתמש עדיין פעיל. אל תבנו על המנגנון הזה אתר חדש.

SESSION#

המחלקה SESSION ב-system/collections/SESSION.php עוטפת את סשן ה-PHP הרגיל ($_SESSION, קובץ ב-session.save_path). הליבה לא מגדירה שם סשן, פרמטרי עוגייה או handler משלה, כך שההתנהגות נקבעת מ-php.ini. היא אינה קשורה לסשן האדמין (CRM_adminPanel_sessions) ולא לסשן של LOGIN.

הליבה משתמשת בה לכמה דברים פנימיים: כתובת החזרה של טופס (frm_<form name> ב-Form.php), שאילתת השלמה אוטומטית של שדות (ac_<sessID>), ומטמון תשובת MISC::ip_info (ip_check).

כל קריאה פותחת וסוגרת סשן

כל מתודה קוראת ל-session_start() ואחריה ל-session_write_close(). לכן אסור לקרוא ל-SESSION::* אחרי שהודפס פלט (ייווצר "headers already sent"), ושימוש תכוף עולה בנעילת קובץ וכתיבה לדיסק בכל קריאה. אל תערבבו אותה עם $_SESSION ישיר באותה בקשה: ה-$_SESSION כבר סגור.

SESSION::init(), close()#

public static function init()
public static function close()

init מתחילה סשן אם הוא לא פעיל (session_start()), close קוראת ל-session_write_close(). שתי המתודות נקראות מתוך שאר המתודות ואין צורך לקרוא להן בעצמכם.

SESSION::set()#

public static function set($key, $val)

שומרת ערך תחת $key ומחזירה true. אובייקט, מערך, bool או null מאוחסנים כ-serialize($val); מחרוזת או מספר שלם נשמרים כמו שהם. כל השאר (מערך, אובייקט, bool, null, מספר עשרוני) עובר serialize.

SESSION::get()#

public static function get($key)

מחזירה את הערך, או מחרוזת ריקה "" כשהמפתח לא קיים. כל ערך שנראה כמו מחרוזת serialize (MISC::IsSerialized) עובר unserialize אוטומטית, גם אם שמרתם אותו כמחרוזת רגילה.

SESSION::remove(), is_set(), destroy()#

public static function remove($key)
public static function is_set($key)
public static function destroy()
מתודההתנהגות
removeמוחקת את המפתח; מחזירה true
is_settrue אם SESSION::get($key) != "", כלומר גם ערך ריק או false נחשב "לא קיים"
destroyמבצעת unset($_SESSION) ו-session_write_close(). לא קוראת ל-session_destroy(), ולכן אינה מנתקת סשן באמת: קוד שרוצה ניתוק מלא צריך session_destroy() בעצמו
SESSION::set("cart", ["items" => [3, 5]]);   // נשמר כ-serialize
$cart = SESSION::get("cart");                // מערך
if (SESSION::is_set("cart")) SESSION::remove("cart");

LOGIN#

כניסת משתמשי צד-לקוח (אנשים שנרשמו לאתר, לא אדמינים). המחלקה ב-system/collections/LOGIN.php. העוגייה היא CRM_login_<table> (עם הטבלה ברירת המחדל: CRM_login_users) והיא נכתבת דרך COOKIES::set, כלומר לא HttpOnly ו-SameSite=None. הטוקן הוא MISC::encode(json_encode(["uid", "ip", "tbl", "time"])), וכל כניסה נרשמת גם בטבלה CRM_users_logins.

הטבלאות#

הטבלאות אינן נוצרות על ידי עדכון הגרסה ואינן מיוצאות מה-master. אתר שמשתמש ב-LOGIN יוצר אותן בעצמו (שמות הטבלה בלי הקידומת CRM_ כש-CONFIG::$db_prefix הוא CRM):

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;

עמודות נוספות ב-users מותרות. ראו סכמת הטבלאות ו-כל הטבלאות.

משתנים סטטיים#

הגדירו אותם ב-application/includes/init.php (לפני שימוש).

משתנהברירת מחדלמשמעות
LOGIN::$table"users"שם טבלת המשתמשים (בלי CRM_). גם חלק משם העוגייה ומה שנבדק ב-tbl של הטוקן
LOGIN::$encryption"md5"אלגוריתם בדיקת הסיסמה: "md5", "bcrypt" או "wp"
LOGIN::$max_devices3כמה כניסות אחרונות לכל משתמש תקפות. 0 מבטל את ההגבלה (ואת הבדיקה מול users_logins)
LOGIN::$login_use_ipfalsetrue מחייב REMOTE_ADDR זהה לזה שבטוקן. שימו לב: שונה מ-COOKIES::$login_use_ip (שברירת המחדל שלו true)

אלגוריתמי סיסמה:

$encryptionבדיקהמה אתם שומרים ב-password
md5$user["password"] == md5($password)md5($password) (חלש, ללא salt)
bcryptBcrypt::verify (מחלקת Bcrypt מ-libraries/bcrypt.php)(new Bcrypt)->hash($password), בפורמט $2y$08$...
wpPasswordHash(8, FALSE)->CheckPasswordhash של WordPress. הליבה לא מכילה את מחלקת PasswordHash, האתר חייב לטעון אותה בעצמו
ברירת המחדל חלשה

הגדירו LOGIN::$encryption = "bcrypt" לפני שמכניסים משתמש ראשון. מעבר מאוחר מ-md5 דורש להמיר את כל הסיסמאות.

LOGIN::login_by_details()#

public static function login_by_details($username, $password, $sessTime = 24*7*30*12)

מנקה את שם המשתמש (trim, strtolower, DB::escape), מחפשת DB::get_val(LOGIN::$table, ["username" => ...]), בודקת את הסיסמה לפי LOGIN::$encryption, ואם תואמת קוראת ל-login_by_id. מחזירה את תוצאת login_by_id (bool של הכתיבה) או false כשהמשתמש לא נמצא או הסיסמה שגויה. ברירת המחדל של $sessTime היא 60480 שעות (כשבע שנים).

LOGIN::login_by_id()#

public static function login_by_id($id, $sessTime = 24*7*30*12)

מתחברת כמשתמש לפי id בלי בדיקת סיסמה (שימושי להתחברות אחרי הרשמה או דרך OAuth). נכשלת (false) כשאין שורה או כש-active == "0". אחרת: בונה טוקן, מכניסה שורה ל-CRM_users_logins (uid, token, ip), וכותבת את העוגייה login_<table> ל-$sessTime שעות.

LOGIN::is_logged()#

public static function is_logged()

true כשכל התנאים מתקיימים: קיימת עוגייה, הטוקן מפוענח ל-uid > 0 ו-tbl שווה ל-LOGIN::$table, ה-IP תואם (רק כש-$login_use_ip), והטוקן נמצא בין LOGIN::$max_devices הטוקנים האחרונים של המשתמש ב-CRM_users_logins (ממוינים לפי id DESC). כניסה רביעית מסלקת את הראשונה.

הערה
active נבדק רק בכניסה משתמש שנחסם (active = 0) אחרי שנכנס נשאר מחובר עד שהטוקן שלו נדחק החוצה או נמחק. כדי לנתק משתמש מיד, מחקו את שורות CRM_users_logins שלו.

LOGIN::get_id()#

public static function get_id()

מחזירה את uid (int) של המשתמש המחובר, או false כשאינו מחובר (is_logged() כוללת את כל הבדיקות).

LOGIN::get_token()#

static function get_token()

מחזירה את ערך העוגייה login_<table> כמות שהוא, או false. הטוקן כולל נתונים מוצפנים בלבד, אבל אל תחשפו אותו.

LOGIN::logout()#

public static function logout()

מוחקת את העוגייה (COOKIES::kill) ואת שורת הטוקן מ-CRM_users_logins. אין ערך החזרה.

הטוקן נכנס ל-SQL ללא
DB::escape logout() בונה DELETE ... WHERE token = '<cookie value>' מהערך הגולמי של העוגייה. עוגייה היא קלט של הלקוח. כל עוד הקוד לא מסנן את הערך, אל תקראו ל-LOGIN::logout() מכתובת פתוחה לכולם בלי שהוכחתם שהמבקר מחובר (LOGIN::is_logged()), ושקלו לממש התנתקות באתר שלכם עם DB::escape($token).

דוגמה מלאה#

// application/includes/init.php
LOGIN::$encryption  = "bcrypt";
LOGIN::$max_devices = 5;

// application/controllers/account.php
class account extends wz_controller
{
    function login()
    {
        PAGE::$cache_this_page = false;                       // אל תשמרו בקאש דף שמשתנה לפי משתמש

        if (!empty($_POST["username"]))
        {
            if (LOGIN::login_by_details($_POST["username"], $_POST["password"]))
                { header("Location: /account"); exit; }
            return $this->view("account/login", ["error" => 1]);
        }
        return $this->view("account/login", ["error" => 0]);
    }

    function index()
    {
        PAGE::$cache_this_page = false;
        if (!LOGIN::is_logged()) return "REQUIRE_LOGIN";      // ראו ROUTER ו-MODULE

        $user = DB::get_val("users", LOGIN::get_id());
        return $this->view("account/home", ["user" => $user]);
    }

    function register()
    {
        $this->load->library("bcrypt");
        $bc = new Bcrypt;
        $ins = DB::update("users");
        $ins->set_var("username", strtolower(trim($_POST["username"])));
        $ins->set_var("password", $bc->hash($_POST["password"]));
        $ins->set_var("active", 1);
        $ins->insert();
        $user = DB::get_val("users", ["username" => DB::escape(strtolower(trim($_POST["username"])))]);
        return $user ? LOGIN::login_by_id($user["id"]) : false;
    }
}
מטמון עמודים והמשתמש המחובר
cache_engine::cache_php_check() מגיש עמוד שמור לפני שהקונטרולר רץ. עמוד שנבנה עם משתמש מחובר ונשמר יוגש לכולם. בכל מתודה שתלויה בזהות המשתמש הציבו PAGE::$cache_this_page = false. ראו מטמון עמודים מלא.

CRYPT#

הצפנה סימטרית כללית מבוססת AES-256-CBC, ב-system/collections/CRYPT.php. בניגוד ל-MISC::encode, ה-IV אקראי לכל הצפנה והערך עובר דרך JSON ולא serialize, ולכן הוא בטוח יותר לשימוש חדש.

CRYPT::set_secret_key()#

public static function set_secret_key($key)

מציבה את סוד ההצפנה. ברירת המחדל היא wizzocms, ערך שידוע לכל מי שקרא את הקוד. קראו לה ב-application/includes/init.php עם סוד אקראי ארוך, ייחודי לאתר, שנשמר מחוץ ל-git.

CRYPT::encode()#

public static function encode($data)

מצפינה כל ערך שניתן להמרה ל-JSON. null או מחרוזת ריקה מחזירים "". התהליך: מפתח = sha256(secret) בינארי; IV אקראי של 16 בתים; json_encode($data, JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK); openssl_encrypt (AES-256-CBC, פלט base64); והתוצאה היא base64url(IV . ciphertext) בלי padding (+/ מוחלפים ב--_, = מוסר), כך שמתאימה ל-URL ולעוגייה.

שימו לב
JSON_NUMERIC_CHECK מחרוזת שנראית כמספר הופכת למספר לפני ההצפנה: "007" יחזור כ-7, ומספר טלפון "0501234567" יחזור כ-501234567. הצפינו מספרים כאלה בתוך מערך עם שדה לא מספרי, או עטפו אותם כך שלא יתחילו בספרה.

CRYPT::decode()#

public static function decode($data)

הפעולה ההפוכה. ערך ריק מחזיר "". מחזירה את הערך המקורי (מערך, מספר, מחרוזת) לאחר json_decode. כשהפענוח נכשל (מפתח שגוי, מחרוזת פגומה) מוחזר false.

CRYPT::set_secret_key("a-long-random-per-site-secret");

$token = CRYPT::encode(["order" => 4812, "exp" => time() + 3600]);   // מחרוזת base64url
$data  = CRYPT::decode($token);                                       // ["order" => 4812, "exp" => ...]
if ($data === false || ($data["exp"] ?? 0) < time()) { /* לא תקף */ }
הצפנה בלי אימות

AES-CBC בלי MAC מבטיח סודיות אך לא שלמות: מי שמשנה את המחרוזת עלול לקבל פענוח משובש במקום שגיאה. אם חשוב לכם לזהות שינוי, הוסיפו שדה חתימה (hash_hmac) בתוך הנתונים ובדקו אותו אחרי decode.

MISC::encode ו-MISC::decode (בשימוש COOKIES ו-LOGIN)#

COOKIES::user_log ו-LOGIN מצפינות את הטוקן דרך MISC::encode/MISC::decode (ראו עזרי MISC). ההבדלים מ-CRYPT:

CRYPTMISC::encode / decode
אלגוריתםAES-256-CBCAES-256-CBC
IVאקראי, נשמר בתוך הפלטקבוע, נגזר ממחרוזת ה-IV הסודית (אותו קלט נותן תמיד אותו פלט)
סריאליזציהJSONserialize / unserialize
פלטbase64url ללא paddingbase64 כפול, עם -_, במקום +/=
סוד ברירת מחדלwizzocmsThis is my secret key (וגם IV This is my secret iv)
הגדרהCRYPT::set_secret_key($key)MISC::set_encrypt_secret_keys($key, $iv)
ערך ריק""""
חובה להחליף את ברירות המחדל

אתר שלא קרא ל-MISC::set_encrypt_secret_keys() עם סוד משלו מצפין את כל העוגיות, טוקני LOGIN, עוגיית admin_lang ועוד באמצעות מפתח ציבורי. מי שיודע את המפתח יכול לזייף ערכים, ו-MISC::decode מריצה unserialize על התוצאה. הוסיפו ב-application/includes/init.php:

MISC::set_encrypt_secret_keys("<random 32+ chars, unique per site>", "<another random string>");
CRYPT::set_secret_key("<third random string>");

הקובץ נטען לפני כל קוד אחר שמצפין או מפענח. החלפת הסוד מבטלת את כל העוגיות והטוקנים הקיימים (משתמשים ייאלצו להיכנס מחדש). ראו צ'קליסט הקשחה.

ראו גם#

מצאתם טעות או חוסר? תקנו את הדף או פתחו Issue בריפו. התיעוד נכתב מתוך הקוד של ליבה 5.0.115.