แก้ปัญหา PHP Session บน Shared Hosting

ปัญหา "Login หมดอายุเอง" หรือ "ต้อง Login ใหม่ทุกครั้ง" เป็นเรื่องที่พบบ่อยมากบน Shared Hosting สาเหตุหลักมาจากพฤติกรรมของ PHP Session ที่บน Shared Hosting ทำงานต่างจาก VPS หรือ Server ส่วนตัว บทความนี้จะอธิบายสาเหตุและวิธีแก้ปัญหาทีละขั้นตอน

ทำไม PHP Session ถึงหมดอายุเร็วบน Shared Hosting

Session GC รันโดย User อื่น

Shared Hosting แชร์ Session Directory กัน เมื่อ User อื่น Trigger GC (Garbage Collection) อาจลบ Session ของ User คุณได้

session.gc_maxlifetime สั้นเกิน

ค่าเริ่มต้น 1440 วินาที (24 นาที) ถ้าไม่มี Activity Session จะหมดอายุและถูกลบ

Session Path ผิดหรือไม่มีสิทธิ์

Session ไม่ถูกเขียนได้เพราะ Path ไม่มีสิทธิ์ เมื่อ PHP อ่านกลับจะไม่เจอ Session

Cookie Domain/Path ผิด

Cookie ที่เก็บ Session ID ถูกตั้งค่าผิด ทำให้ Browser ไม่ส่ง Cookie กลับไปให้ Server

ตรวจสอบ Session Settings ปัจจุบัน

สร้างไฟล์ทดสอบ session_info.php เพื่อดูค่าที่ Server ใช้อยู่จริง (ลบทิ้งหลังใช้งาน):

<?php echo "gc_maxlifetime: " . ini_get('session.gc_maxlifetime') . "s\n"; echo "gc_probability: " . ini_get('session.gc_probability') . "\n"; echo "gc_divisor: " . ini_get('session.gc_divisor') . "\n"; echo "session.save_path: " . ini_get('session.save_path') . "\n"; echo "cookie_lifetime: " . ini_get('session.cookie_lifetime') . "\n"; echo "cookie_path: " . ini_get('session.cookie_path') . "\n";

วิธีแก้ที่ 1 — ตั้งค่าใน php.ini หรือ .htaccess

วิธีง่ายที่สุดคือเพิ่มค่าใน .htaccess หรือ php.ini ของโปรเจกต์:

# เพิ่มใน .htaccess php_value session.gc_maxlifetime 86400 php_value session.cookie_lifetime 86400 php_value session.gc_probability 1 php_value session.gc_divisor 100
# หรือเพิ่มใน php.ini (ถ้า DirectAdmin อนุญาต) session.gc_maxlifetime = 86400 session.cookie_lifetime = 86400 session.gc_probability = 1 session.gc_divisor = 100

ค่า 86400 = 24 ชั่วโมง: ปรับตามความต้องการของแอป — แนะนำไม่เกิน 86400 (1 วัน) สำหรับแอปทั่วไป

วิธีแก้ที่ 2 — ย้าย Session ไปโฟลเดอร์ส่วนตัว

ปัญหาหลักบน Shared Hosting คือ GC ของ User อื่นลบ Session คุณ แก้ได้โดยย้าย Session ไปเก็บในโฟลเดอร์ที่ User อื่นเข้าไม่ถึง:

<?php // ใส่ต้น script ก่อน session_start() $session_path = __DIR__ . '/sessions'; if (!is_dir($session_path)) { mkdir($session_path, 0700, true); } ini_set('session.save_path', $session_path); ini_set('session.gc_maxlifetime', 86400); ini_set('session.cookie_lifetime', 86400); session_start();

ระวัง: โฟลเดอร์ sessions/ ต้องไม่เข้าถึงได้จาก Web ให้เพิ่ม deny from all ใน sessions/.htaccess หรือวางโฟลเดอร์นอก public_html

# สร้าง sessions/.htaccess ป้องกันการเข้าถึงจาก Web Deny from all

วิธีแก้ที่ 3 — เก็บ Session ในฐานข้อมูล (แนะนำสำหรับแอปสำคัญ)

วิธีที่แข็งแกร่งที่สุดและแก้ปัญหา GC ได้ถาวรคือเก็บ Session ใน MySQL:

-- สร้างตาราง Session ใน MySQL CREATE TABLE php_sessions ( id VARCHAR(128) NOT NULL PRIMARY KEY, data TEXT NOT NULL, expires INT NOT NULL, INDEX idx_expires (expires) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php // Custom Session Handler เก็บใน MySQL class DBSessionHandler implements SessionHandlerInterface { private PDO $db; private int $lifetime; public function __construct(PDO $db, int $lifetime = 86400) { $this->db = $db; $this->lifetime = $lifetime; } public function open($path, $name): bool { return true; } public function close(): bool { return true; } public function read($id): string { $stmt = $this->db->prepare( 'SELECT data FROM php_sessions WHERE id=? AND expires>?' ); $stmt->execute([$id, time()]); return $stmt->fetchColumn() ?: ''; } public function write($id, $data): bool { $expires = time() + $this->lifetime; $stmt = $this->db->prepare( 'REPLACE INTO php_sessions (id, data, expires) VALUES (?, ?, ?)' ); return $stmt->execute([$id, $data, $expires]); } public function destroy($id): bool { $stmt = $this->db->prepare('DELETE FROM php_sessions WHERE id=?'); return $stmt->execute([$id]); } public function gc($max_lifetime): int|false { $stmt = $this->db->prepare('DELETE FROM php_sessions WHERE expires<?'); $stmt->execute([time()]); return $stmt->rowCount(); } } // ใช้งาน $pdo = new PDO('mysql:host=localhost;dbname=mydb;charset=utf8mb4', 'user', 'pass'); $handler = new DBSessionHandler($pdo, 86400); session_set_save_handler($handler, true); ini_set('session.gc_probability', 0); // ปิด GC ของ PHP ใช้ expires แทน session_start();

วิธีแก้ที่ 4 — ตั้งค่า Cookie ให้ถูกต้อง

ถ้า Session ยังคงอยู่แต่ Browser ไม่ส่ง Cookie กลับ ตรวจสอบการตั้งค่า Cookie:

<?php // ตั้งก่อน session_start() เสมอ ini_set('session.cookie_httponly', 1); // ป้องกัน XSS ini_set('session.cookie_secure', 1); // HTTPS เท่านั้น ini_set('session.cookie_samesite', 'Lax'); ini_set('session.cookie_domain', '.example.com'); // รองรับ Subdomain ini_set('session.use_strict_mode', 1); // ป้องกัน Session Fixation session_start();

วิธีแก้ที่ 5 — Regenerate Session ID เพื่อความปลอดภัย

ควร Regenerate Session ID ทุกครั้งที่ Login สำเร็จ เพื่อป้องกัน Session Fixation Attack:

<?php // หลัง Login สำเร็จเสมอ session_regenerate_id(true); // true = ลบ Session เก่า $_SESSION['user_id'] = $user['id']; $_SESSION['logged_at'] = time();

ตรวจสอบด้วย Debug Script

<?php session_start(); if (!isset($_SESSION['counter'])) { $_SESSION['counter'] = 0; $_SESSION['created_at'] = time(); } $_SESSION['counter']++; echo "Session ID: " . session_id() . "\n"; echo "Counter: " . $_SESSION['counter'] . "\n"; echo "Created: " . date('Y-m-d H:i:s', $_SESSION['created_at']) . "\n"; echo "Age: " . (time() - $_SESSION['created_at']) . "s\n";

รีเฟรชหน้าซ้ำๆ ถ้า Counter เพิ่มขึ้นต่อเนื่องแปลว่า Session ทำงานปกติ ถ้า Counter รีเซ็ตเป็น 1 ทุกครั้งแปลว่า Session ยังมีปัญหา

สรุปแนวทาง: แอปที่มีผู้ใช้น้อย → ย้าย Session ไปโฟลเดอร์ส่วนตัว (วิธีที่ 2) แอปที่มีผู้ใช้มาก → เก็บ Session ใน MySQL (วิธีที่ 3) ทั้งคู่ควรใช้ร่วมกับการตั้งค่า Cookie ที่ปลอดภัย (วิธีที่ 4)

การเปรียบเทียบ Session Storage แต่ละแบบ

แต่ละวิธีการจัดเก็บ Session มีข้อดีข้อเสียต่างกัน การเลือกให้เหมาะกับขนาดและลักษณะของแอปจะช่วยแก้ปัญหาได้อย่างยั่งยืน

วิธีจัดเก็บ เหมาะกับ ข้อดี ข้อจำกัด
ไฟล์ระบบ (Default)Local / Devตั้งค่าง่าย ไม่ต้องใช้ DBGC ของ User อื่นลบ Session ได้
โฟลเดอร์ส่วนตัวShared Hosting แอปขนาดเล็กแก้ GC ข้าม User ได้ ติดตั้งง่ายยังพึ่งระบบไฟล์ ถ้า Disk เต็มจะมีปัญหา
MySQL Databaseแอป Production ที่มีผู้ใช้มากเสถียรมาก ควบคุม Expiry ได้ดีเพิ่ม DB Load ต้อง Clean Up Expired
Redis / MemcachedVPS / High-Traffic Appเร็วมาก รองรับ Multi-Serverต้องติดตั้ง Extension เพิ่ม ไม่ใช่ทุก Hosting มี

Session Security — สิ่งที่ต้องทำควบคู่กับการแก้ปัญหาหมดอายุ

เมื่อแก้ปัญหา Session หมดอายุเร็วแล้ว ควรตรวจสอบความปลอดภัยของ Session ด้วย เพราะ Session ที่อยู่นานขึ้นมีความเสี่ยงด้านความปลอดภัยเพิ่มขึ้นตามไปด้วย

ป้องกัน Session Hijacking

Session Hijacking คือการที่ผู้ไม่หวังดีขโมย Session ID ของผู้ใช้เพื่อปลอมตัวเป็นผู้ใช้นั้น วิธีป้องกันที่สำคัญ:

ป้องกัน Session Fixation

Session Fixation คือการที่ผู้โจมตีบังคับให้ Victim ใช้ Session ID ที่ผู้โจมตีรู้อยู่แล้ว แก้ได้ง่ายๆ ด้วยคำสั่งเดียว:

<?php // ทำทันทีหลัง Login สำเร็จ ก่อนเขียน $_SESSION session_regenerate_id(true); $_SESSION['user_id'] = $authenticated_user_id; $_SESSION['login_time'] = time(); $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];

ตั้งเวลาหมดอายุ Session แบบ Absolute และ Idle

แทนที่จะพึ่ง gc_maxlifetime เพียงอย่างเดียว ให้ใช้การตรวจสอบเวลาเพิ่มเติมใน Code:

<?php session_start(); $max_idle = 1800; // หมดอายุถ้าไม่ Active 30 นาที $max_absolute = 86400; // หมดอายุแน่ๆ หลัง 24 ชั่วโมง if (isset($_SESSION['last_active'])) { if ((time() - $_SESSION['last_active']) > $max_idle) { session_destroy(); header('Location: /login.php?reason=idle'); exit; } } if (isset($_SESSION['login_time'])) { if ((time() - $_SESSION['login_time']) > $max_absolute) { session_destroy(); header('Location: /login.php?reason=expired'); exit; } } $_SESSION['last_active'] = time();

คำถามที่พบบ่อยเกี่ยวกับ PHP Session บน Hosting

เพิ่ม session.gc_maxlifetime แล้วยังหมดอายุเร็วอยู่ เป็นเพราะอะไร?

ปัญหานี้มักเกิดจาก Session GC ของ User อื่นบน Shared Hosting รัน GC ด้วย gc_maxlifetime ของตัวเอง (อาจสั้นกว่า) และลบ Session ของคุณ วิธีแก้ถาวรคือย้าย Session ไปโฟลเดอร์ส่วนตัว (วิธีที่ 2) หรือเก็บใน MySQL (วิธีที่ 3) ซึ่งจะไม่ถูก GC ของคนอื่นกระทบ

WordPress Session ต่างจาก PHP Session ทั่วไปอย่างไร?

WordPress ไม่ใช้ $_SESSION ของ PHP โดยตรง แต่ใช้ Authentication Cookies (wordpress_logged_in_*, wordpress_*) แทน ปัญหา "Login หมดอายุ" ใน WordPress จึงมักเกิดจาก Cookie Domain ผิด, HTTPS/HTTP ผสม (Mixed Content), หรือ WordPress Secret Keys ใน wp-config.php ถูก Regenerate แก้โดยตั้ง COOKIEPATH และ COOKIE_DOMAIN ใน wp-config.php ให้ถูกต้อง

ใช้ Redis เก็บ Session ได้บน Shared Hosting ไหม?

ขึ้นอยู่กับว่า Hosting รองรับ PHP Extension phpredis หรือ predis หรือไม่ AsiaGB Hosting บางแพ็กเกจมี Redis Extension ให้ ตรวจสอบได้จาก phpinfo() หรือสอบถาม Support สำหรับแอปที่ต้องการ Redis แนะนำให้เลื่อนไปใช้ VPS ซึ่งสามารถติดตั้ง Redis Service ได้เต็มรูปแบบ

ตรวจสอบและ Monitor PHP Session บน Production

หลังจากแก้ปัญหา Session หมดอายุแล้ว ควรมีระบบ Monitor เพื่อให้แน่ใจว่า Session ทำงานถูกต้องในระยะยาว โดยเฉพาะเมื่อมีการ Update PHP Version หรือ Plugin ที่อาจเปลี่ยน Session Settings โดยไม่ตั้งใจ

สร้างหน้า Session Health Check

สร้างหน้า PHP ที่เข้าถึงได้เฉพาะ Admin เพื่อตรวจสอบ Session Settings ในแต่ละ Deploy:

<?php // session_health.php — ป้องกันด้วย Basic Auth หรือ IP Whitelist session_start(); $settings = [ 'gc_maxlifetime' => ini_get('session.gc_maxlifetime'), 'cookie_lifetime' => ini_get('session.cookie_lifetime'), 'save_path' => ini_get('session.save_path'), 'cookie_httponly' => ini_get('session.cookie_httponly'), 'cookie_secure' => ini_get('session.cookie_secure'), 'use_strict_mode' => ini_get('session.use_strict_mode'), ]; $warnings = []; if ($settings['gc_maxlifetime'] < 3600) { $warnings[] = "gc_maxlifetime สั้นมาก ({$settings['gc_maxlifetime']}s) แนะนำ >= 3600"; } if (!$settings['cookie_httponly']) { $warnings[] = "cookie_httponly ปิดอยู่ — ความเสี่ยง XSS"; } if (!$settings['cookie_secure']) { $warnings[] = "cookie_secure ปิดอยู่ — Cookie อาจส่งผ่าน HTTP"; } echo "<pre>"; print_r($settings); if ($warnings) { echo "\n⚠️ Warnings:\n"; foreach ($warnings as $w) echo " - $w\n"; } else { echo "\n✅ Session settings ดูดี\n"; } echo "</pre>";

ตรวจ Session ใน MySQL แบบ Real-time

ถ้าใช้ MySQL Session Handler สามารถ Query ดู Session ที่กำลัง Active อยู่และ Session ที่ Expire แล้วแต่ยังไม่ถูกลบ:

-- ดู Session ที่ Active อยู่ในขณะนี้ SELECT id, expires, FROM_UNIXTIME(expires) AS expires_readable FROM php_sessions WHERE expires > UNIX_TIMESTAMP() ORDER BY expires DESC LIMIT 50; -- ดู Session ที่ Expire แล้ว (ควรถูกลบอัตโนมัติ) SELECT COUNT(*) AS expired_count FROM php_sessions WHERE expires <= UNIX_TIMESTAMP(); -- ลบ Session ที่ Expire แล้วด้วยตัวเอง (Cleanup) DELETE FROM php_sessions WHERE expires <= UNIX_TIMESTAMP();

สัญญาณว่า Session ยังมีปัญหา

หลังแก้ไขแล้ว ควรตรวจสอบสัญญาณต่อไปนี้เพื่อยืนยันว่าปัญหาหายจริง:

ถ้ายังพบปัญหาหลังทำตามทุกขั้นตอน ให้ส่ง Ticket ให้ Support พร้อมแนบ Output ของ session_info.php และ Error Log ล่าสุด เพื่อให้ทีมงานช่วยตรวจสอบ Config ระดับ Server ที่อาจ Override ค่าที่ตั้งไว้ นอกจากนี้ควรตรวจว่า php.ini ระดับ Server ไม่ได้ Lock ค่า session ไว้ด้วย Directive session.gc_maxlifetime เพราะในบาง Hosting ค่าจาก .htaccess จะไม่สามารถ Override ค่า Server ได้ถ้าถูกตั้งเป็น PHP_INI_SYSTEM

เพิ่ม Cron Job ทำความสะอาด MySQL Session

เพิ่ม Cron Job ใน DirectAdmin เพื่อลบ Session ที่หมดอายุออกจากฐานข้อมูลทุกวัน ลด Database Size ที่ไม่จำเป็น:

# เพิ่มใน DirectAdmin → Cron Jobs # ทุกวันตี 3 เช้า ลบ Session ที่ Expire แล้ว 0 3 * * * php -r "require '/home/USER/config.php'; \$pdo->exec('DELETE FROM php_sessions WHERE expires <= UNIX_TIMESTAMP()');" >> /home/USER/logs/session_cleanup.log 2>&1

Hosting รองรับ PHP Session เต็มรูปแบบ

AsiaGB Hosting รองรับ PHP Session Handler แบบกำหนดเอง, MySQL, และ Session Path ส่วนตัว เริ่มต้น 500 บาท/ปี

ดูแพ็กเกจ Hosting