ปัญหาของการแชร์ฐานข้อมูลในระบบ SaaS
การสร้างแอปพลิเคชันแบบ Multi-Tenancy (ระบบที่รองรับลูกค้าหลายรายในฐานข้อมูลชุดเดียวกัน) เป็นความท้าทายที่โปรแกรมเมอร์มือใหม่มักมองข้าม ความปลอดภัยเป็นเรื่องสำคัญที่สุด เพราะเราต้องมั่นใจว่าบริษัท A จะไม่มีวันเห็นข้อมูลของบริษัท B ได้เด็ดขาด หากเราใช้การเขียนคำสั่ง WHERE tenant_id = 'company_a' ในทุกๆ จุดที่ดึงข้อมูลออกมา เรากำลังแบกความเสี่ยงมหาศาลไว้บนบ่า
ลองจินตนาการว่าคุณเขียนคำสั่ง Query (คำสั่งถามข้อมูลจากฐานข้อมูล) มาเป็นร้อยจุดในแอปพลิเคชันของคุณ หากวันหนึ่งคุณลืมใส่เงื่อนไขกรองข้อมูลไปเพียงจุดเดียว ผลที่ตามมาคือหายนะ ข้อมูลการเงินหรือความลับของลูกค้าจะรั่วไหลทันที และนั่นคือจุดจบของความน่าเชื่อถือในฐานะนักพัฒนา นี่คือเหตุผลที่เราไม่ควรฝากความปลอดภัยไว้กับแค่โค้ดฝั่งแอปพลิเคชันเพียงอย่างเดียว
ในฐานะโปรแกรมเมอร์ที่เพิ่งเริ่มต้น คุณควรเรียนรู้ที่จะใช้เครื่องมือที่มีอยู่ให้เกิดประโยชน์สูงสุด PostgreSQL มีฟีเจอร์ระดับเทพที่เรียกว่า Row-Level Security หรือ RLS (ระบบรักษาความปลอดภัยระดับแถวข้อมูล) ซึ่งมันจะทำหน้าที่เป็นยามเฝ้าประตูที่คอยกรองข้อมูลให้เราโดยอัตโนมัติ ไม่ว่าเราจะเขียนโค้ดพลาดลืมกรองข้อมูลอย่างไร ระบบฐานข้อมูลก็จะดึงมาให้เฉพาะข้อมูลที่ลูกค้าคนนั้นมีสิทธิ์เห็นเท่านั้น
Row-Level Security คืออะไรและทำไมต้องใช้
RLS เปรียบเสมือนการที่คุณมีสมุดบัญชีเล่มใหญ่เล่มเดียว แต่คุณใส่แว่นวิเศษที่ทำให้มองเห็นเฉพาะบรรทัดที่เป็นของตัวเองเท่านั้น แม้คนอื่นจะมาเปิดสมุดเล่มเดียวกัน แต่พวกเขาก็จะมองเห็นแค่บรรทัดที่เป็นของเขาเองเช่นกัน ระบบนี้ทำงานลึกถึงระดับ Kernel (แกนกลางของระบบปฏิบัติการหรือซอฟต์แวร์) ทำให้ปลอดภัยกว่าการเขียนโค้ดกรองข้อมูลเองหลายเท่า
เมื่อเราเปิดใช้งาน RLS ระบบจะตรวจสอบตัวตนของ Session (รอบการใช้งานของผู้ใช้ที่เชื่อมต่อกับฐานข้อมูล) ว่าเป็นใครและมีสิทธิ์อะไรบ้าง จากนั้นมันจะแอบใส่เงื่อนไขการกรองข้อมูลเข้าไปในทุกคำสั่งที่เราส่งไปโดยอัตโนมัติ คุณไม่จำเป็นต้องกังวลว่าลืมใส่ WHERE หรือไม่ เพราะฐานข้อมูลจะจัดการให้คุณเองอย่างเบ็ดเสร็จ
ประโยชน์ที่ชัดเจนที่สุดคือการลดโอกาสเกิด Bug (ข้อผิดพลาดของโค้ด) ที่ร้ายแรง หากคุณเป็นมือใหม่ที่กำลังสร้าง Portfolio (แฟ้มสะสมผลงาน) การนำ RLS มาใช้จะช่วยให้โปรเจกต์ของคุณดูเป็นมืออาชีพและน่าเชื่อถือมากขึ้น เพราะมันแสดงให้เห็นว่าคุณให้ความสำคัญกับความปลอดภัยของข้อมูลลูกค้าเป็นอันดับหนึ่งตั้งแต่วันแรกที่เริ่มเขียนโปรแกรม
ขั้นตอนการเปิดใช้งาน RLS ในตารางข้อมูล
ขั้นตอนแรกเราต้องเริ่มจากการสร้างตารางข้อมูลและเปิดใช้งานระบบนี้ก่อน การทำเช่นนี้เป็นการบอก PostgreSQL ว่าตารางนี้ต้องการการปกป้องเป็นพิเศษนะ ไม่ใช่ใครจะเข้ามาอ่านก็ได้ตามใจชอบ ลองมาดูตัวอย่างการสร้างตารางใบแจ้งหนี้กันครับ
-- สร้างตารางข้อมูลใบแจ้งหนี้
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id VARCHAR(64) NOT NULL,
amount NUMERIC(10, 2) NOT NULL,
description TEXT
);
-- เปิดใช้งานระบบรักษาความปลอดภัยระดับแถว
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ในโค้ดชุดแรก เราสร้างตาราง invoices (ใบแจ้งหนี้) ที่มีคอลัมน์ tenant_id เพื่อระบุว่าข้อมูลนี้เป็นของใคร ต่อมาเราใช้คำสั่ง ALTER TABLE เพื่อเปิดใช้งานฟีเจอร์ RLS ซึ่งเป็นหัวใจสำคัญของความปลอดภัยในขั้นตอนนี้
ผลลัพธ์คือ ตารางนี้จะถูกล็อคไว้ทันที หากคุณลองสั่ง SELECT * FROM invoices หลังจากเปิดใช้งาน RLS โดยยังไม่ได้ตั้งค่า Policy (นโยบายการเข้าถึงข้อมูล) คุณจะไม่เห็นข้อมูลอะไรเลยแม้แต่แถวเดียว เพราะฐานข้อมูลจะถือว่าคุณไม่มีสิทธิ์เข้าถึงข้อมูลใดๆ จนกว่าเราจะไปกำหนดกฎเกณฑ์ให้มัน
การสร้างนโยบายความปลอดภัยเพื่อแยกข้อมูล
เมื่อเปิดใช้งานแล้ว เราต้องสร้าง Policy เพื่อบอกว่า ใครมีสิทธิ์เห็นข้อมูลอะไรบ้าง เราจะใช้ตัวแปร Session ที่ชื่อว่า app.current_tenant_id เป็นตัวกำหนด หาก tenant_id ของแถวนั้นตรงกับตัวแปรที่ตั้งค่าไว้ ระบบก็จะอนุญาตให้เข้าถึงข้อมูลได้ทันที
การเขียน Policy แบบนี้ช่วยให้เราไม่ต้องเขียนเงื่อนไขซ้ำซากในโค้ดฝั่งโปรแกรมอีกต่อไป นี่คือตัวอย่างการสร้างกฎความปลอดภัยที่เข้มงวดครับ
-- กำหนดนโยบายให้เห็นเฉพาะข้อมูลของตัวเอง
CREATE POLICY tenant_isolation_policy ON invoices
AS RESTRICTIVE
USING (tenant_id = current_setting('app.current_tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true));
บรรทัดแรกคือการสร้างชื่อนโยบาย ส่วน USING คือเงื่อนไขสำหรับการอ่านข้อมูล และ WITH CHECK คือเงื่อนไขสำหรับการเพิ่มหรือแก้ไขข้อมูล โดยทั้งสองส่วนจะเช็คกับตัวแปร app.current_tenant_id ที่เราจะส่งค่าเข้ามาจาก Backend (โค้ดฝั่งเซิร์ฟเวอร์) ของเรา
ผลลัพธ์คือ เมื่อเราตั้งค่าตัวแปรใน Session ให้เป็น company_a แล้วสั่ง SELECT ข้อมูล PostgreSQL จะกรองเอาเฉพาะแถวที่มี tenant_id เป็น company_a มาให้คุณเท่านั้น ข้อมูลของบริษัทอื่นจะถูกซ่อนไว้เหมือนไม่มีตัวตนอยู่จริงในฐานข้อมูล
การเชื่อมต่อกับแอปพลิเคชันของคุณ
ในฝั่งของ Backend เมื่อมีผู้ใช้ส่งคำขอเข้ามา เราต้องดึง Tenant ID (รหัสอ้างอิงลูกค้า) ออกมาจาก JWT (โทเค็นที่ใช้ยืนยันตัวตน) แล้วนำไปตั้งค่าให้ฐานข้อมูลก่อนเริ่มทำงานทุกครั้ง เพื่อให้ RLS ทำงานได้อย่างถูกต้องตามผู้ใช้งานคนนั้นๆ
นี่คือตัวอย่างการใช้ SQLAlchemy (เครื่องมือจัดการฐานข้อมูลในภาษา Python) เพื่อตั้งค่าตัวแปรและดึงข้อมูลโดยไม่ต้องใส่คำสั่ง WHERE เลยสักนิดครับ
def get_tenant_invoices(db, tenant_id):
# ตั้งค่าตัวแปรในฐานข้อมูลก่อนเริ่ม Query
db.execute(text("SELECT set_config('app.current_tenant_id', :tenant, true)"), {"tenant": tenant_id})
# ดึงข้อมูลโดยไม่ต้องมี WHERE tenant_id = ...
return db.execute(text("SELECT * FROM invoices")).fetchall()
ฟังก์ชัน set_config คือการส่งค่า tenant_id เข้าไปใน Session ของฐานข้อมูลเพื่อให้ RLS นำไปใช้กรองข้อมูล ต่อมาคำสั่ง SELECT * FROM invoices จะทำงานภายใต้กฎที่เราสร้างไว้ ทำให้เราได้แค่ข้อมูลของลูกค้าคนนั้นๆ มาใช้งาน
ผลลัพธ์คือโค้ดของคุณจะสะอาดขึ้นมาก และปลอดภัยอย่างยิ่ง คุณสามารถเขียนคำสั่งดึงข้อมูลได้สั้นๆ โดยไม่ต้องกลัวว่าข้อมูลจะรั่วไหลไปถึงลูกค้าคนอื่น เพราะฐานข้อมูลทำหน้าที่เป็นด่านสุดท้ายในการตรวจสอบสิทธิ์ให้คุณเสมอนั่นเอง
ข้อควรระวังสำหรับมือใหม่
มีจุดสำคัญที่ห้ามลืมเด็ดขาดคือ Superuser (ผู้ดูแลระบบสูงสุดของฐานข้อมูล) จะสามารถมองเห็นข้อมูลทุกอย่างได้โดยไม่สน RLS ดังนั้นห้ามใช้บัญชี Admin ในการเชื่อมต่อแอปพลิเคชันเด็ดขาด ให้สร้าง Role (บัญชีผู้ใช้งาน) แยกสำหรับแอปพลิเคชันโดยเฉพาะเสมอ
นอกจากนี้ การทดสอบระบบด้วยตัวเองเป็นสิ่งสำคัญมาก คุณควรสร้างข้อมูลจำลองของบริษัท A และ B ขึ้นมา แล้วลองทดสอบดึงข้อมูลดูว่าถ้าเปลี่ยน Session แล้ว ข้อมูลที่ได้เปลี่ยนไปตามจริงหรือไม่ การทำแบบนี้จะช่วยให้คุณมั่นใจว่าระบบทำงานถูกต้องก่อนนำไปใช้งานจริง
จำไว้ว่าความปลอดภัยคือสิ่งที่ต้องสร้างตั้งแต่เริ่ม ไม่ใช่สิ่งที่ค่อยมาแก้ทีหลัง โปรแกรมเมอร์ที่เก่งไม่ได้วัดกันที่เขียนโค้ดเร็วแค่ไหน แต่อยู่ที่การเขียนโค้ดที่เชื่อถือได้และปกป้องข้อมูลของผู้ใช้งานได้อย่างมั่นคง การฝึกฝนเรื่อง RLS ตั้งแต่ตอนนี้คือการลงทุนที่คุ้มค่าที่สุดสำหรับอาชีพสายนี้ครับ
สรุป
การใช้ PostgreSQL Row-Level Security คือวิธีที่ฉลาดที่สุดในการป้องกันข้อมูลรั่วไหลในระบบ Multi-Tenant เพราะมันย้ายภาระความปลอดภัยจากโค้ดที่ซับซ้อนมาไว้ที่ฐานข้อมูลที่แข็งแกร่งแทน การทำเช่นนี้ช่วยลด Bug และทำให้คุณทำงานได้อย่างมั่นใจมากขึ้น ไม่ว่าจะเขียนโปรเจกต์ขนาดเล็กหรือใหญ่แค่ไหนก็ตาม
ลองนำไปปรับใช้ในโปรเจกต์ SaaS (ซอฟต์แวร์ที่ขายแบบเช่าใช้) ของคุณดูครับ เริ่มจากการสร้างตารางเล็กๆ แล้วลองตั้งค่า Policy ดูว่ามันกรองข้อมูลได้จริงไหม เมื่อคุณเข้าใจหลักการทำงานนี้แล้ว คุณจะก้าวข้ามจากโปรแกรมเมอร์มือใหม่ไปสู่ระดับที่เข้าใจความปลอดภัยของระบบอย่างถ่องแท้ ซึ่งเป็นทักษะที่บริษัทชั้นนำมองหาเสมอ
จงจำไว้เสมอว่าข้อมูลของลูกค้าคือสิ่งมีค่าที่สุด การเลือกใช้เครื่องมือที่ถูกต้องเพื่อปกป้องข้อมูลเหล่านั้น คือเครื่องพิสูจน์ความเป็นมืออาชีพของคุณ ขอให้สนุกกับการเขียนโค้ดและสร้างระบบที่ปลอดภัยตั้งแต่วันนี้นะครับ หากติดปัญหาตรงไหนให้ลองกลับมาดูขั้นตอนการตั้งค่า Session อีกครั้ง เพราะจุดนั้นคือหัวใจของการเชื่อมต่อที่ถูกต้องที่สุดครับ
ที่มา: PostgreSQL Row-Level Security: Multi-Tenancy Without Leaking Tenant Data — DEV Community