ในโลกของการพัฒนาซอฟต์แวร์ที่ให้ความสำคัญกับความปลอดภัย (Security) การป้องกันการเข้าถึงข้อมูลโดยไม่ได้รับอนุญาตถือเป็นสิ่งสำคัญสูงสุด แนวคิดในการ ‘บังคับให้ผู้ใช้ต้องล็อกอิน’ ไม่ได้หมายถึงแค่การแสดงหน้า Login แต่ยังรวมถึงการควบคุมระดับฐานข้อมูลว่า ข้อมูลใดบ้างที่ผู้ใช้รายนี้มีสิทธิ์มองเห็นหรือแก้ไข
สำหรับระบบที่ใช้ PostgreSQL (PG) การจัดการเรื่องสิทธิ์และการบังคับเข้าใช้งานอย่างเข้มงวดที่สุด ควรทำผ่านกลไกของ Row-Level Security (RLS) ซึ่งเป็นคุณสมบัติระดับสูงที่ช่วยให้เราสามารถกำหนดนโยบายการเข้าถึงข้อมูลได้ในระดับแถว (Row) โดยไม่ต้องเขียนโค้ดตรวจสอบสิทธิ์ซ้ำๆ ในทุก Query
🔑 หลักการบังคับ Login ด้วย PostgreSQL RLS
แทนที่จะพึ่งพาแค่ Application Layer ในการตรวจสอบว่าผู้ใช้ล็อกอินหรือไม่ เราจะยกระดับความปลอดภัยไปที่ Database Layer โดยตรง เมื่อเราเปิดใช้งาน RLS บนตารางใดๆ แล้ว ทุก Query ที่ถูกรันจะต้องผ่านนโยบาย (Policy) ที่กำหนดไว้ก่อนเสมอ หากไม่มี Policy ใดอนุญาตให้เข้าถึงแถวนั้น ข้อมูลนั้นก็จะถูกซ่อนจากผู้ใช้คนดังกล่าวโดยอัตโนมัติ
⚙️ ขั้นตอนการใช้งาน Row-Level Security
- เปิดใช้งาน RLS: ต้องกำหนดว่าตารางใดบ้างที่ต้องการการควบคุมสิทธิ์ระดับแถว
- สร้าง Policy: กำหนดเงื่อนไข (Policy) ว่าผู้ใช้คนไหนมีสิทธิ์เห็นข้อมูลอะไรได้บ้าง โดยอ้างอิงจาก Context ของเซสชันปัจจุบัน
ในตัวอย่างนี้ เราจะสมมติว่าเรามีตารางชื่อ user_data ที่เก็บข้อมูลส่วนตัวของผู้ใช้ และต้องการให้ผู้ใช้แต่ละคนสามารถดูหรือแก้ไขได้เฉพาะข้อมูลของตนเองเท่านั้น
💡 ตัวอย่างโค้ด: การสร้าง Policy บังคับสิทธิ์
เราจะใช้ฟังก์ชัน PG เพื่อจำลองการตรวจสอบว่า User ID ที่กำลังรัน Query นั้นตรงกับ Owner ID ของแถวนั้นหรือไม่:
SQL
-- 1. สร้างตารางตัวอย่าง (ถ้ายังไม่มี) และเปิดใช้งาน RLS บนตารางนั้นๆ
CREATE TABLE user_data (
id SERIAL PRIMARY KEY,
owner_id INT NOT NULL, -- ID ของเจ้าของข้อมูล
secret_info TEXT
);
ALTER TABLE user_data ENABLE ROW LEVEL SECURITY;
-- 2. สร้าง Policy: อนุญาตให้ผู้ใช้เห็นแถวที่ owner_id ตรงกับ User ID ที่กำหนดใน Session Context
CREATE POLICY user_access_policy ON user_data
FOR SELECT USING (owner_id = current_setting('app.user_id')::int);
-- 3. ตัวอย่างการจำลองการล็อกอินและทดสอบ:
-- สมมติว่าผู้ใช้ A (ID=10) ล็อกอินแล้ว เราต้องตั้งค่า Session Context ก่อน
SET app.user_id = '10';
SELECT * FROM user_data;
-- ผลลัพธ์: จะเห็นเฉพาะแถวที่ owner_id = 10 เท่านั้น
-- เมื่อผู้ใช้ B (ID=20) ล็อกอิน ระบบจะบังคับให้ Policy เปลี่ยนไปทันที
SET app.user_id = '20';
SELECT * FROM user_data;
-- ผลลัพธ์: จะเห็นเฉพาะแถวที่ owner_id = 20 เท่านั้น (ถ้ามี)
🚀 การใช้ฟังก์ชันเพื่อเพิ่มความซับซ้อนในการตรวจสอบ
หากการตรวจสอบสิทธิ์มีความซับซ้อนกว่าแค่การเปรียบเทียบ ID เช่น ต้องมีการเช็กว่าบัญชีนั้นยัง Active หรือไม่ เราสามารถสร้าง Function ที่รับผิดชอบการตัดสินใจนี้ได้ และนำ Policy ไปอ้างอิงจาก Function นั้นแทน:
SQL
CREATE OR REPLACE FUNCTION check_user_active(target_owner_id INT) RETURNS BOOLEAN AS $$
BEGIN
-- ตรวจสอบว่าผู้ใช้ที่กำลังรัน Query มีสิทธิ์เข้าถึงข้อมูลนี้หรือไม่
RETURN EXISTS (SELECT 1 FROM users WHERE user_id = current_setting('app.user_id')::int AND status = 'active');
END;
$$ LANGUAGE plpgsql;
-- นำ Function นี้ไปใช้ใน Policy แทนการเปรียบเทียบ ID โดยตรง
CREATE POLICY complex_access_policy ON user_data
FOR ALL USING (check_user_active(owner_id));
✅ สรุปและข้อควรระวัง
การใช้ Row-Level Security เป็นแนวทางที่แข็งแกร่งที่สุดในการบังคับควบคุมสิทธิ์ในระดับฐานข้อมูล ทำให้มั่นใจได้ว่าแม้ว่าโค้ด Application Layer จะมีช่องโหว่ (เช่น SQL Injection) ผู้โจมตีก็ยังไม่สามารถเข้าถึงข้อมูลที่เกินขอบเขตสิทธิ์ของผู้ใช้ปัจจุบันได้
อย่างไรก็ตาม การตั้งค่า RLS ต้องทำด้วยความระมัดระวังและต้องมีการจัดการ Session Context อย่างถูกต้องในระดับแอปพลิเคชัน เพื่อให้แน่ใจว่า current_setting('app.user_id') ถูกกำหนดอย่างแม่นยำทุกครั้งที่มีการเรียกใช้ Query