KoderKoder.ai
ราคาองค์กรการศึกษาสำหรับนักลงทุน
เข้าสู่ระบบเริ่มต้นใช้งาน

ผลิตภัณฑ์

ราคาองค์กรสำหรับนักลงทุน

ทรัพยากร

ติดต่อเราสนับสนุนการศึกษาบล็อก

กฎหมาย

นโยบายความเป็นส่วนตัวข้อกำหนดการใช้งานความปลอดภัยนโยบายการใช้งานที่ยอมรับได้แจ้งการละเมิด

โซเชียล

LinkedInTwitter
Koder.ai
ภาษา

© 2026 Koder.ai สงวนลิขสิทธิ์

หน้าแรก›บล็อก›Row-level security (RLS) ของ PostgreSQL สำหรับ SaaS: นโยบายที่ได้ผล
14 ต.ค. 2568·3 นาที

Row-level security (RLS) ของ PostgreSQL สำหรับ SaaS: นโยบายที่ได้ผล

Row-level security ของ PostgreSQL สำหรับ SaaS ช่วยบังคับการแยก tenant ในฐานข้อมูล เรียนรู้ว่าเมื่อใดควรใช้ วิธีเขียนนโยบาย และสิ่งที่ควรหลีกเลี่ยง

Row-level security (RLS) ของ PostgreSQL สำหรับ SaaS: นโยบายที่ได้ผล

ปัญหาจริงที่ RLS พยายามแก้ในแอป SaaS

ในแอป SaaS บั๊กด้านความปลอดภัยที่อันตรายที่สุดมักเป็นบั๊กที่โผล่มาหลังจากที่คุณสเกลขึ้น คุณเริ่มจากกฎง่าย ๆ เช่น “ผู้ใช้เห็นได้เฉพาะข้อมูลของ tenant ของตัวเอง” แล้วคุณปล่อย endpoint ใหม่เร็ว ๆ เพิ่มคำสั่งรายงาน หรือเพิ่ม join ที่โดยไม่ตั้งใจข้ามการตรวจสอบ

การยืนยันสิทธิ์ฝั่งแอปจะพังเมื่อมีความซับซ้อนเพราะกฎกระจายตัว ตัวคอนโทรลเลอร์หนึ่งเช็ก tenant_id อีกตัวเช็กสมาชิก งานแบ็คกราวด์ลืมเช็กไป และทาง "admin export" อยู่ในสถานะ "ชั่วคราว" เป็นเดือน ๆ แม้ทีมรอบคอบก็ยังพลาด spot ได้

Row-level security (RLS) ของ PostgreSQL แก้ปัญหาเฉพาะ: ทำให้ฐานข้อมูลบังคับว่าแถวใดมองเห็นได้สำหรับคำร้องหนึ่ง ๆ แบบเดียวกับที่มิดเดิลแวร์การพิสูจน์ตัวตนกรองคำร้องทุกเม็ด ความคิดหลักง่าย: ทุก SELECT, UPDATE, และ DELETE จะถูกกรองโดยนโยบายอัตโนมัติ

ส่วนคำว่า “แถว” สำคัญ RLS ไม่ได้ปกป้องทุกอย่าง:

  • มันไม่ปกปิดคอลัมน์เฉพาะด้วยตัวเอง (ใช้ views หรือสิทธิ์คอลัมน์)
  • มันไม่ทำให้ฟังก์ชันที่ไม่ปลอดภัยเป็นของปลอดภัย (ฟังก์ชันยังสามารถรั่วข้อมูลได้หากรันด้วยสิทธิ์ยกระดับ)
  • มันไม่ตรวจสอบกฎธุรกิจทั้งหมด (เช่น “เฉพาะเจ้าของเท่านั้นที่เปลี่ยนการตั้งค่าการเรียกเก็บเงินได้”)

ตัวอย่างชัดเจน: คุณเพิ่ม endpoint ที่แสดงรายการ projects พร้อม join กับ invoices เพื่อแดชบอร์ด ฝั่งแอปอย่างเดียว อาจกรอง projects ตาม tenant แต่ลืมกรอง invoices หรือ join บนคีย์ที่ข้าม tenant ได้ง่าย ๆ ด้วย RLS ตารางทั้งสองสามารถบังคับการแยก tenant ได้ ดังนั้นคำสั่งจะล้มอย่างปลอดภัยแทนการรั่วข้อมูล

การแลกเปลี่ยนคือนิสัย: คุณเขียนโค้ดตรวจสอบน้อยลงและลดจำนวนจุดที่อาจรั่ว แต่คุณต้องทำงานใหม่: ออกแบบนโยบายอย่างระมัดระวัง ทดสอบตั้งแต่ต้น และยอมรับว่านโยบายอาจบล็อกคำสั่งที่คุณคิดว่าจะทำงานได้

เมื่อ RLS ช่วยลดความยุ่งยาก (และเมื่อมันเพิ่มความเจ็บปวด)

RLS อาจรู้สึกเป็นงานเพิ่มจนกว่าแอปของคุณจะมี endpoint มากกว่าจำนวนไม่กี่ตัว ถ้าคุณมีขอบเขต tenant ที่ชัดเจนและเส้นทางการดึงข้อมูลหลายแบบ (หน้ารายการ, ค้นหา, การส่งออก, เครื่องมือแอดมิน) การใส่กฎในฐานข้อมูลหมายความว่าคุณไม่ต้องจำเพิ่มตัวกรองเดิมในทุกที่

RLS เหมาะอย่างยิ่งเมื่อกฎน่าเบื่อและเป็นสากล: “ผู้ใช้เห็นได้เฉพาะแถวของ tenant ของตัวเอง” หรือ “ผู้ใช้เห็นเฉพาะโปรเจกต์ที่สมาชิกของเขาเป็นส่วนหนึ่ง” ในกรณีเหล่านั้น นโยบายช่วยลดความผิดพลาดเพราะทุก SELECT, UPDATE, และ DELETE จะผ่านเกตเดียวกัน แม้เมื่อเพิ่มคำสั่ง SQL ใหม่ทีหลัง

มันยังช่วยในแอปที่อ่านหนักเมื่อโลจิกการกรองคงที่ หาก API ของคุณมีวิธีโหลด invoices 15 แบบ (ตามสถานะ, ตามวันที่, ตามลูกค้า, ตามการค้นหา) RLS ช่วยให้คุณหยุดเขียนการกรอง tenant ซ้ำ ๆ ในทุกคำสั่งและโฟกัสที่ฟีเจอร์แทน

RLS จะเพิ่มความเจ็บปวดเมื่อกฎไม่ได้อยู่ในระดับแถว กฎต่อฟิลด์เช่น “คุณเห็นเงินเดือนแต่ไม่เห็นโบนัส” หรือ “ปกปิดคอลัมน์นี้เว้นแต่คุณเป็น HR” มักกลายเป็น SQL ที่อึดอัดและข้อยกเว้นที่ยากดูแล

มันยังไม่เหมาะกับการรายงานหนักที่ต้องการการเข้าถึงกว้างจริง ๆ ทีมมักสร้าง role ข้ามไปเพื่อ “งานนี้งานเดียว” และตรงนั้นแหละที่ความผิดพลาดสะสม

ก่อนตัดสินใจ ให้คิดว่าคุณต้องการให้ฐานข้อมูลเป็นเกตสุดท้ายหรือไม่ ถ้าตอบว่าใช่ วางแผนวินัย: ทดสอบพฤติกรรมฐานข้อมูล (ไม่ใช่แค่ผล API), ถือมิเกรชันเป็นการเปลี่ยนแปลงความปลอดภัย, หลีกเลี่ยงการบายพาสแบบด่วน, ตัดสินใจว่าหน้าที่งานแบ็คกราวด์ยืนยันตัวตนอย่างไร และรักษานโยบายให้เล็กและทำซ้ำได้

ถ้าคุณใช้เครื่องมือที่สร้าง backend อัตโนมัติ มันช่วยเร่งการส่งมอบได้ แต่ไม่ทำให้ความต้องการบทบาทที่ชัดเจน เทสต์ และโมเดล tenant เรียบง่ายหายไป (ตัวอย่างเช่น Koder.ai ใช้ Go และ PostgreSQL สำหรับ backend ที่สร้างโดยเครื่องมือ คุณยังต้องออกแบบ RLS อย่างตั้งใจ แทนที่จะ “โรยมันทีหลัง”)

เบื้องต้นของโมเดลข้อมูลที่ทำให้นโยบาย RLS จัดการได้ง่าย

RLS ง่ายที่สุดเมื่อสคีมาของคุณบอกชัดว่าใครเป็นเจ้าของอะไร หากคุณเริ่มด้วยโมเดลที่ไม่ชัดและพยายาม “แก้ด้วยนโยบาย” มักได้คำสั่งช้าและบั๊กที่สับสน

ใส่คีย์ tenant ในทุกที่ที่ควรมี

เลือกคีย์ tenant ตัวเดียว (เช่น org_id) และใช้ให้สม่ำเสมอ ตารางที่เป็นของ tenant ควรมีคอลัมน์นี้ แม้จะอ้างถึงตารางอื่นที่มีคีย์เดียวกันด้วยก็ตาม สิ่งนี้หลีกเลี่ยงการ join ภายในนโยบายและทำให้การตรวจสอบ USING ง่าย

กฎปฏิบัติ: ถ้าแถวควรหายไปเมื่อผู้ใช้ยกเลิก ลูกค้า ควรมี org_id

จำลองการเป็นสมาชิกอย่างชัดเจน

นโยบาย RLS มักตอบคำถามเดียว: “ผู้ใช้นี้เป็นสมาชิกของ org นี้หรือไม่ และทำอะไรได้บ้าง?” ยากที่จะอนุมานจากคอลัมน์กระจัดกระจาย

เก็บตารางหลักให้เล็กและธรรมดา:

  • users (หนึ่งแถวต่อคน)
  • orgs (หนึ่งแถวต่อ tenant)
  • org_memberships (user_id, org_id, role, status)
  • ทางเลือก: project_memberships สำหรับการเข้าถึงระดับโปรเจกต์

เมื่อมีสิ่งนี้ นโยบายของคุณสามารถเช็กสมาชิกด้วยการค้นหาที่มีดัชนีได้ครั้งเดียว

แยกข้อมูลอ้างอิงที่แชร์จากข้อมูลที่เป็นของ tenant

ไม่ใช่ทุกอย่างต้องมี org_id ตารางอ้างอิงเช่น ประเทศ ประเภทสินค้า หรือประเภทแผนมักจะแชร์ระหว่าง tenant ทำเป็นอ่านอย่างเดียวสำหรับบทบาทส่วนใหญ่ และอย่าผูกมันกับ org เดียว

ข้อมูลที่เป็นของ tenant (projects, invoices, tickets) ควรหลีกเลี่ยงการดึงรายละเอียดเฉพาะ tenant ผ่านตารางที่แชร์ เก็บตารางแชร์ให้น้อยและเสถียร

FK, cascades และดัชนี

Foreign keys ยังคงทำงานกับ RLS แต่การลบอาจเซอร์ไพรส์ถ้าบทบาทที่ลบไม่สามารถ “เห็น” แถวที่ขึ้นต่อกันได้ วางแผน cascades ให้ดีและทดสอบการลบจริง

เพิ่มดัชนีคอลัมน์ที่นโยบายกรองบ่อย โดยเฉพาะ org_id และคีย์การเป็นสมาชิก นโยบายที่อ่านว่า WHERE org_id = ... ไม่ควรกลายเป็นการสแกนทั้งตารางเมื่อขนาดตารางเป็นล้านแถว

วิธีที่นโยบาย RLS ทำงาน โดยไม่ต้องกลัว

RLS เป็นสวิตช์ต่อ-ตาราง เมื่อติดตั้งแล้ว PostgreSQL จะไม่ไว้ใจโค้ดแอปให้จำตัวกรอง tenant อีกต่อไป ทุก SELECT, UPDATE, และ DELETE จะถูกกรองด้วยนโยบาย และทุก INSERT และ UPDATE จะถูกตรวจสอบโดยนโยบาย

การเปลี่ยนความคิดครั้งใหญ่: เมื่อเปิด RLS แล้ว คำสั่งที่เคยคืนข้อมูลอาจเริ่มคืนเป็นศูนย์แถวโดยไม่เกิดข้อผิดพลาด นั่นคือตัวควบคุมการเข้าถึงของ PostgreSQL

นโยบายทำอะไรจริง ๆ

นโยบายเป็นกฎเล็ก ๆ แนบกับตาราง พวกมันใช้การตรวจสองแบบ:

  • USING เป็นตัวกรองอ่าน ถ้าแถวไม่ตรง USING มันจะมองไม่เห็นสำหรับ SELECT และไม่สามารถเป็นเป้าของ UPDATE หรือ DELETE
  • WITH CHECK เป็นประตูเขียน ตัดสินว่าแถวใหม่หรือแถวที่เปลี่ยนแล้วอนุญาตสำหรับ INSERT หรือ UPDATE หรือไม่

แพตเทิร์น SaaS ทั่วไป: USING รับรองว่าคุณเห็นเฉพาะแถวจาก tenant ของคุณ และ WITH CHECK รับรองว่าคุณไม่สามารถแทรกแถวเข้า tenant อื่นโดยเดา tenant_id

PERMISSIVE vs RESTRICTIVE ในประโยคเดียว

เมื่อคุณเพิ่มนโยบายทีหลัง เรื่องนี้สำคัญ:

  • PERMISSIVE (ค่าดีฟอลต์): แถวอนุญาตถ้านโยบายใดนโยบายหนึ่งอนุญาต
  • RESTRICTIVE: แถวจะอนุญาตก็ต่อเมื่อทุกนโยบายแบบ restrictive อนุญาตมัน (ทับพฤติกรรม permissive)

ถ้าคุณวางแผนจะเรียงกฎเช่นการจับคู่ tenant บวกการเช็กบทบาทบวกการเป็นสมาชิกโปรเจกต์ นโยบาย restrictive อาจทำให้เจตนาแน่ชัด แต่ก็ทำให้ล็อกตัวเองออกได้ง่ายถ้าลืมเงื่อนไขตัวใดตัวหนึ่ง

Postgres รู้ได้อย่างไรว่าใครเป็นผู้เรียก

RLS ต้องการค่าตัวตนที่เชื่อถือได้ ตัวเลือกทั่วไป:

  • ตัวแปรเซสชันตั้งต่อคำร้อง (เช่น app.user_id และ app.tenant_id)
  • ข้อความ JWT แมปเข้าเป็น session settings โดยชั้น API
  • การสลับ role (SET ROLE ... ต่อคำร้อง) ซึ่งทำงานได้แต่เพิ่มภาระการปฏิบัติการ

เลือกวิธีหนึ่งแล้วใช้ให้ทั่ว อย่าผสมแหล่งตัวตนข้ามบริการเป็นเส้นทางด่วนสู่บั๊กที่สับสน

การตั้งชื่อนโยบายเพื่อให้ง่ายต่อการดีบักทีหลัง

ใช้คอนเวนชันที่คาดเดาได้เพื่อให้การ dump สคีมาและโลกรอ่านง่าย เช่น: {table}__{action}__{rule} เช่น projects__select__tenant_match

ทีละขั้น: เพิ่ม RLS ให้ตารางหนึ่งและพิสูจน์ว่ามันทำงาน

วางรากฐาน multi-tenant ให้เร็วขึ้น
เปลี่ยนโมเดล tenant ของคุณให้เป็นตาราง บทบาท และ endpoints จริงโดยไม่ต้องเริ่มจากศูนย์
ลอง Koder.ai

ถ้าคุณใหม่กับ RLS ให้เริ่มจากตารางเดียวและทำ proof ขนาดเล็ก เป้าหมายไม่ใช่ครอบคลุมทั้งหมด เป้าหมายคือให้ฐานข้อมูลปฏิเสธการเข้าถึงข้าม tenant แม้จะมีบั๊กในแอป

ตารางเล็ก ๆ ให้ฝึก

สมมติ projects ง่าย ๆ ก่อน เพิ่ม tenant_id ในทางที่ไม่ทำให้เขียนล้ม

ALTER TABLE projects ADD COLUMN tenant_id uuid;

-- Backfill existing rows (example: everyone belongs to a default tenant)
UPDATE projects SET tenant_id = '11111111-1111-1111-1111-111111111111'::uuid
WHERE tenant_id IS NULL;

ALTER TABLE projects ALTER COLUMN tenant_id SET NOT NULL;

ถัดไป แยกความเป็นเจ้าของจากการเข้าถึง แพตเทิร์นทั่วไปคือ: role หนึ่งเป็นเจ้าของตาราง (app_owner), อีก role ใช้โดย API (app_user) บทบาท API ไม่ควรเป็นเจ้าของตาราง มิฉะนั้นมันจะบายพาสนโยบายได้

ALTER TABLE projects OWNER TO app_owner;
REVOKE ALL ON projects FROM PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON projects TO app_user;

ตอนนี้ตัดสินใจว่าคำร้องบอก Postgres ว่ากำลังให้บริการ tenant ไหนอย่างไร วิธีง่าย ๆ คือการตั้งค่าที่มีขอบเขตคำร้อง แอปของคุณตั้งค่านั้นทันทีหลังเปิดทรานแซกชัน

-- inside the same transaction as the request
SELECT set_config('app.current_tenant', '22222222-2222-2222-2222-222222222222', true);

เปิด RLS และเริ่มด้วยการเข้าถึงแบบอ่าน

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY projects_tenant_select
ON projects
FOR SELECT
TO app_user
USING (tenant_id = current_setting('app.current_tenant')::uuid);

พิสูจน์ว่ามันทำงานโดยลองสอง tenant ต่างกันและตรวจดูการนับแถว

เพิ่มกฎเขียน (WITH CHECK)

นโยบายอ่านไม่ปกป้องการเขียน เพิ่ม WITH CHECK เพื่อไม่ให้ insert และ update ลอบป้อนแถวไป tenant ผิด

CREATE POLICY projects_tenant_write
ON projects
FOR INSERT, UPDATE
TO app_user
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);

วิธีรวดเร็วในการยืนยันพฤติกรรม (รวมถึงความล้มเหลว) คือเก็บสคริปต์ SQL เล็ก ๆ ที่คุณรันซ้ำหลังมิเกรชันทุกครั้ง:

  • BEGIN; SET LOCAL ROLE app_user;
  • SELECT set_config('app.current_tenant', '\u003ctenant A\u003e', true); SELECT count(*) FROM projects;
  • INSERT INTO projects(id, tenant_id, name) VALUES (gen_random_uuid(), '\u003ctenant B\u003e', 'bad'); (จะต้องล้ม)
  • UPDATE projects SET tenant_id = '\u003ctenant B\u003e' WHERE ...; (จะต้องล้ม)
  • ROLLBACK;

ถ้าคุณรันสคริปต์นั้นแล้วได้ผลลัพธ์เดิมทุกครั้ง คุณมีเบสไลน์ที่เชื่อถือได้ก่อนขยาย RLS ไปยังตารางอื่น

แพตเทิร์นนโยบายที่คุณจะใช้ซ้ำในแอป SaaS ส่วนใหญ่

ทีมส่วนใหญ่หันมาใช้ RLS หลังจากเบื่อการเขียนการเช็กสิทธิ์ซ้ำ ๆ ในทุกคำสั่ง ข่าวดีคือรูปทรงนโยบายที่คุณต้องการมักคงที่

แถวที่เป็นของเจ้าของ vs แถวที่เป็นของ tenant โดยสมาชิก

บางตารางเป็นของผู้ใช้คนเดียวตามธรรมชาติ (notes, API tokens) ขณะที่อื่น ๆ เป็นของ tenant ที่การเข้าถึงขึ้นกับการเป็นสมาชิก ปฏิบัติต่อสองกรณีนี้ต่างกัน

สำหรับข้อมูลที่เป็นของเจ้าของ นโยบายมักเช็ก created_by = app_user_id() สำหรับข้อมูลของ tenant นโยบายมักเช็กว่าผู้ใช้มีแถวสมาชิกสำหรับ org หรือไม่

วิธีปฏิบัติที่อ่านง่ายคือรวมตัวตนไว้ใน helper SQL เล็ก ๆ แล้วนำกลับมาใช้ใหม่:

-- Example helpers
create function app_user_id() returns uuid
language sql stable as $$
  select current_setting('app.user_id', true)::uuid
$$;

create function app_is_admin() returns boolean
language sql stable as $$
  select current_setting('app.is_admin', true) = 'true'
$$;

แยกกฎอ่านจากกฎเขียน

การอ่านมักกว้างกว่าการเขียน ตัวอย่าง: สมาชิกทุกคนของ org อาจ SELECT projects ได้ แต่เฉพาะ editor เท่านั้นที่ UPDATE และเฉพาะ owner เท่านั้นที่ DELETE

ทำให้ชัดเจน: นโยบายหนึ่งสำหรับ SELECT (membership), นโยบายหนึ่งสำหรับ INSERT/UPDATE พร้อม WITH CHECK (ตามบทบาท), และนโยบายหนึ่งสำหรับ DELETE (มักเข้มงวดกว่าการอัปเดต)

ทางออกสำหรับ admin โดยไม่ปิด RLS

หลีกเลี่ยงการ “ปิด RLS สำหรับแอดมิน” แทนที่จะทำแบบนั้น ให้เพิ่มทางหนีภายในนโยบาย เช่น app_is_admin() เพื่อไม่ให้คุณเผลอให้สิทธิ์กว้างกับ role บริการร่วม

การลบแบบนุ่ม (soft deletes) และสถานะ

ถ้าคุณใช้ deleted_at หรือ status ให้ใส่มันเข้าในนโยบาย SELECT (deleted_at is null) มิฉะนั้นคนจะสามารถ “ฟื้น” แถวโดยพลิกค่าสถานะที่แอปคิดว่าเป็นขั้นสุดท้ายได้

UPSERT: ทำให้ WITH CHECK เป็นมิตร

INSERT ... ON CONFLICT DO UPDATE ต้องผ่าน WITH CHECK สำหรับแถวหลังการเขียน หากนโยบายของคุณต้องการ created_by = app_user_id() ให้แน่ใจว่า upsert ตั้ง created_by เมื่อ insert และไม่เขียนทับเมื่อ update

ถ้าคุณสร้างโค้ด backend อัตโนมัติ แพตเทิร์นเหล่านี้ควรเป็นเทมเพลตภายในเพื่อให้ตารางใหม่เริ่มด้วยการตั้งค่าปลอดภัยแทนสเตตเปิดเปล่า

กับดัก RLS ที่พบบ่อยซึ่งทำให้การดีบักเจ็บปวด

RLS ดีจนกว่าจะมีรายละเอียดเล็ก ๆ ที่ทำให้ดูเหมือน PostgreSQL "ซ่อนหรือโชว์ข้อมูลแบบสุ่ม" ข้อผิดพลาดด้านล่างคือสิ่งที่เสียเวลามากที่สุด

กับดักที่ทำให้การรั่วของ tenant เกิดขึ้นอย่างเงียบ ๆ

กับดักแรกคือการลืม WITH CHECK บน insert และ update USING ควบคุมสิ่งที่คุณมองเห็น ไม่ใช่สิ่งที่คุณอนุญาตให้สร้าง หากไม่มี WITH CHECK บั๊กในแอปอาจเขียนแถวเข้า tenant ผิด และคุณอาจไม่สังเกตเพราะผู้ใช้เดียวกันอ่านมันไม่ได้

กับดักรั่วทั่วไปอีกอย่างคือ “leaky join” คุณกรอง projects ถูกต้อง แล้ว join กับ invoices, notes, หรือ files ที่ไม่ได้ป้องกันแบบเดียวกัน การแก้ไขเข้มงวดแต่เรียบง่าย: ทุกตารางที่อาจเปิดเผยข้อมูล tenant ต้องมีนโยบายของตัวเอง และ view ไม่ควรพึ่งพาตารางเดียวที่ปลอดภัยเท่านั้น

รูปแบบความล้มเหลวยอดนิยมปรากฏเร็ว:

  • มีนโยบายอ่าน แต่กฎเขียนขาด WITH CHECK
  • เงื่อนไขนโยบายใช้ join ไปยังตารางอื่นที่ไม่ได้ป้องกัน
  • การบังคับใช้อยู่ใน view แต่ตารางพื้นฐานยังเปิดอยู่
  • คุณพึ่งพา “แอปตั้ง tenant_id เสมอ” และงานแบ็คกราวด์ตัวหนึ่งลืม
  • คุณทดสอบด้วย role superuser จึงไม่เห็นพฤติกรรมจริง

กับดักที่ทำให้พฤติกรรมสับสน

นโยบายที่อ้างถึงตารางเดียวกัน (โดยตรงหรือผ่าน view) อาจสร้างการเรียกซ้ำที่น่าประหลาดใจ นโยบายอาจเช็กสมาชิกโดยการคิวรี view ที่อ่านตารางที่ป้องกันอีกครั้ง ซึ่งนำไปสู่ข้อผิดพลาด คำสั่งช้า หรือแม้แต่นโยบายที่ไม่เคยแมตช์

การตั้งค่า role เป็นอีกแหล่งความสับสน เจ้าของตารางและ role ที่ยกระดับสิทธิ์สามารถบายพาส RLS ทำให้เทสต์ของคุณผ่านแต่ผู้ใช้จริงล้ม (หรือกลับกัน) ทดสอบด้วย role ที่มีสิทธิ์ต่ำเดียวกับที่แอปใช้เสมอ

ระวัง SECURITY DEFINER ฟังก์ชัน พวกมันรันด้วยสิทธิ์ของเจ้าของฟังก์ชัน ดังนั้น helper อย่าง current_tenant_id() อาจโอเค แต่ฟังก์ชัน “สะดวก” ที่อ่านข้อมูลอาจอ่านข้าม tenant โดยไม่ตั้งใจ เว้นวางการตั้งค่า search_path ที่ปลอดภัยภายในฟังก์ชัน security definer ด้วย มิฉะนั้นฟังก์ชันอาจใช้วัตถุชื่อเดียวกันจาก schema อื่นและตรรกะนโยบายของคุณอาจชี้ไปยังสิ่งที่ผิดตามสถานะเซสชัน

การดีบัก RLS: วิธีปฏิบัติที่เห็นผล

ฝึก RLS บนแอปจริง
โปรโตไทป์สคีมา projects และ memberships และทดสอบการแยก tenant แบบครบวงจร
สร้างโปรเจกต์

บั๊ก RLS มักเกิดจากบริบทที่ขาดหาย ไม่ใช่ "SQL ผิด" นโยบายอาจถูกต้องในทางทฤษฎีแต่ล้มเพราะ role เซสชันต่างจากที่คุณคิด หรือเพราะคำร้องไม่เคยตั้งค่า tenant และ user ที่นโยบายพึ่งพา

วิธีที่เชื่อถือได้ในการทำสำเนารายงานโปรดักชันคือจำลองการตั้งค่าเซสชันเดียวกันในเครื่องและรันคำสั่ง SQL เดียวกัน นั่นมักหมายถึง:

  • SET ROLE app_user; (หรือ role API จริง)
  • SELECT set_config('app.tenant_id', 't_123', true); และ SELECT set_config('app.user_id', 'u_456', true);
  • รัน SQL เดิมที่แอปรัน (รวมพารามิเตอร์)
  • ยืนยันสิ่งที่ Postgres เห็น: SELECT current_user, current_setting('app.tenant_id', true), current_setting('app.user_id', true);

เมื่อไม่แน่ใจว่านโยบายใดถูกนำมาใช้ ให้ตรวจสอบแค็ตตาล็อกแทนการเดา pg_policies แสดงแต่ละนโยบาย คำสั่ง และนิพจน์ USING กับ WITH CHECK จับคู่กับ pg_class เพื่อตรวจว่า RLS เปิดอยู่บนตารางและไม่ได้ถูกบายพาส

ปัญหาด้านประสิทธิภาพสามารถดูเหมือนปัญหา auth ได้ นโยบายที่ join ตารางสมาชิกหรือเรียกฟังก์ชันอาจถูกต้องแต่ช้าเมื่อโตขึ้น ใช้ EXPLAIN (ANALYZE, BUFFERS) บนคำสั่งที่ทำซ้ำและมองหา sequential scans, nested loops ที่คาดไม่ถึง หรือกรองที่ถูกใช้ช้า ดัชนีที่หายไปบน (tenant_id, user_id) และตารางสมาชิกเป็นสาเหตุทั่วไป

ยังช่วยได้ถ้าล็อกค่าสามอย่างต่อคำร้องที่ชั้นแอป: tenant ID, user ID, และ role ฐานข้อมูลที่ใช้สำหรับคำร้อง เมื่อค่าพวกนี้ไม่ตรงกับที่คุณคิด RLS จะทำงาน “ผิด” เพราะอินพุตผิด

สำหรับเทสต์ ให้เก็บ tenant ตัวอย่างไม่กี่ตัวและทำให้ความล้มเหลวชัดเจน ชุดเทสต์เล็ก ๆ ควรรวม: “Tenant A ไม่สามารถอ่าน Tenant B”, “ผู้ใช้ที่ไม่เป็นสมาชิกไม่เห็นโปรเจกต์”, “owner อัปเดตได้ viewer ไม่ได้”, “insert ถูกบล็อกถ้า tenant_id ไม่ตรงบริบท”, และ “admin override ใช้ได้เฉพาะที่ตั้งใจไว้”

เช็คลิสต์ก่อนปล่อย RLS สู่โปรดักชัน

ถือ RLS เสมือนเข็มขัดนิรภัย ไม่ใช่สวิตช์ฟีเจอร์ พลาดเล็กน้อยกลายเป็น “ทุกคนเห็นข้อมูลทุกคน” หรือ “ทุกอย่างคืน 0 แถว”

รูปแบบโมเดลข้อมูลและนโยบาย

ตรวจสอบให้แน่ใจว่าการออกแบบตารางและกฎนโยบายสอดคล้องกับโมเดล tenant ของคุณ

  • ทุกตารางที่เป็นของ tenant ควรมีคีย์ tenant ชัดเจน (ปกติคือ tenant_id) ถ้าไม่มี ให้จดเหตุผลไว้ (เช่น ตารางอ้างอิงระดับโลก)
  • เปิด RLS บนทุกตารางที่เป็นของ tenant ไม่ใช่แค่ตาราง “หลัก” ถ้ามีเส้นทางบางอย่างต้องไม่บายพาส ให้พิจารณา FORCE ROW LEVEL SECURITY บนตารางเหล่านั้น
  • แยกกฎอ่านและเขียน Reads ใช้ USING Writes ต้องมี WITH CHECK เพื่อไม่ให้ insert และ update ย้ายแถวไป tenant อื่นได้
  • เก็บเงื่อนไขนโยบายให้เป็นมิตรกับดัชนี ถ้านโยบายกรองด้วย tenant_id หรือ join ผ่านตารางสมาชิก ให้เพิ่มดัชนีที่ตรงกัน

สถานการณ์ความสมเหตุสมผลง่าย ๆ: ผู้ใช้ tenant A อ่าน invoice ของตัวเองได้, สามารถ insert invoice ได้เฉพาะสำหรับ tenant A, และไม่สามารถอัปเดต invoice เพื่อเปลี่ยน tenant_id

บทบาท, ประสิทธิภาพ, และเทสต์

RLS แข็งแรงเท่ากับบทบาทที่แอปใช้

  • ยืนยันว่านโยบายแอปไม่เชื่อมต่อด้วย superuser, เจ้าของตาราง, หรือ role ที่มี bypassrls
  • รันคำสั่งจริงบางรายการด้วยข้อมูลขนาดใกล้เคียงโปรดักชันและตรวจแผนการคิวรี
  • เพิ่มเทสต์เชิงลบอัตโนมัติที่ยืนยันว่าการเข้าถึงข้าม tenant ล้มเหลว

ตัวอย่าง: แอปโปรเจกต์แบบ multi-tenant ที่มีการเข้าถึงตามการเป็นสมาชิก

นำ RLS ไปใช้กับข้อมูล SaaS ทั่วไป
สร้าง CRM แบบ tenant-based และนำแพตเทิร์น RLS เดียวกันไปใช้กับทุกตาราง
เริ่ม CRM

จินตนาการแอป B2B ที่บริษัท (orgs) มี projects และ projects มี tasks ผู้ใช้สามารถเป็นสมาชิกหลาย org และผู้ใช้อาจเป็นสมาชิกของบางโปรเจกต์เท่านั้น นี่เป็นกรณีที่เหมาะกับ RLS เพราะฐานข้อมูลสามารถบังคับการแยก tenant แม้ endpoint ลืมตัวกรอง

โมเดลง่าย ๆ คือ: orgs, users, org_memberships (org_id, user_id, role), projects (id, org_id), project_memberships (project_id, user_id), tasks (id, project_id, org_id, ...) org_id บน tasks นั้นตั้งใจให้มี มันทำให้นโยบายเรียบง่ายและลดความประหลาดใจขณะ join

การรั่วคลาสสิกเกิดเมื่อ tasks มีแค่ project_id และนโยบายเช็กการเข้าถึงผ่าน join ไปยัง projects ความผิดพลาดหนึ่งครั้ง (นโยบาย permissive บน projects, join ที่ลดเงื่อนไข, หรือ view ที่เปลี่ยนบริบท) อาจเปิดเผย tasks ของ org อื่น

เส้นทางการมิเกรชที่ปลอดภัยหลีกเลี่ยงการทำลายทราฟฟิกโปรดักชัน:

  • ปล่อยการเปลี่ยนแปลงสคีมาก่อน (เพิ่ม org_id ให้ tasks, เพิ่มตารางสมาชิก)
  • backfill tasks.org_id จาก projects.org_id, แล้วเพิ่ม NOT NULL
  • เพิ่มนโยบายแต่ยังไม่เปิด RLS ขณะทดสอบในสเตจ
  • เปิด RLS แล้วบังคับมัน แล้วค่อยลบตัวกรองฝั่งแอปเดิม

การสนับสนุนควรจัดการด้วย role แคบ ๆ สำหรับฉุกเฉิน ไม่ใช่การปิด RLS เก็บแยกจากบัญชีสนับสนุนปกติและบันทึกชัดเมื่อมันถูกใช้

จดบันทึกกฎเพื่อไม่ให้นโยบายไหลเบี้ยว: ตัวแปรเซสชันใดต้องตั้ง (user_id, org_id), ตารางใดต้องมี org_id, คำว่า “member” หมายถึงอะไร, และตัวอย่าง SQL สั้น ๆ ที่ควรคืน 0 แถวเมื่อรันด้วย org ที่ไม่ถูกต้อง

ขั้นตอนถัดไป: นำ RLS ขึ้นอย่างปลอดภัยและรักษามันให้ง่ายต่อการดูแล

RLS ใช้ง่ายเมื่อคุณปฏิบัติต่อมันเหมือนการเปลี่ยนแปลงผลิตภัณฑ์ ปล่อยเป็นชิ้นเล็ก ๆ พิสูจน์พฤติกรรมด้วยเทสต์ และเก็บบันทึกเหตุผลว่าทำไมนโยบายแต่ละข้อถึงมีอยู่

แผนการนำขึ้นที่มักได้ผล:

  • เริ่มจากตารางเดียวที่มีความเป็นเจ้าของ tenant ชัดเจน (เช่น projects) และล็อกมัน
  • เพิ่มเทสต์ที่ครอบคลุมการอ่านและการเขียนที่อนุญาตและถูกบล็อกสำหรับบทบาทไม่กี่แบบ (owner, member, outsider)
  • ขยายเป็นชุด (หนึ่งพื้นที่ฟีเจอร์ในแต่ละครั้ง) ที่คุณสามารถดีบักได้ในเซสชันเดียว
  • เฝ้าดูข้อผิดพลาดสิทธิ์ระหว่างการนำขึ้นและ deploy ในช่วงความเสี่ยงต่ำ

หลังจากตารางแรกเสถียร ทำให้การเปลี่ยนนโยบายเป็นเรื่องจงใจ เพิ่มขั้นตอนตรวจนโยบายในการมิเกรชัน และใส่บันทึกสั้น ๆ เกี่ยวกับเจตนา (ใครควรเข้าถึงอะไรและทำไม) พร้อมกับการอัปเดตเทสต์ที่สอดคล้อง สิ่งนี้ป้องกันการเพิ่ม OR แบบไม่คิดจนสุดท้ายกลายเป็นรูรั่ว

ถ้าคุณเร่งรีบ เครื่องมืออย่าง Koder.ai (koder.ai) สามารถช่วยสร้างจุดเริ่มต้น Go + PostgreSQL ผ่านการคุย แล้วคุณค่อยวางนโยบาย RLS และเทสต์ด้วยวินัยเดียวกับ backend ที่เขียนมือ

สุดท้าย ให้เก็บราวนิรภัยช่วง rollout: ถ่าย snapshot ก่อนมิเกรชันนโยบาย ฝึกการย้อนกลับจนชิน และมีทางฉุกเฉินสำหรับการสนับสนุนที่ไม่ปิด RLS ทั้งระบบ

คำถามที่พบบ่อย

RLS แก้ปัญหาด้านความปลอดภัยอะไรในแอป SaaS จริง ๆ?

RLS ทำให้ PostgreSQL บังคับว่าแถวใดมองเห็นหรือแก้ไขได้สำหรับคำร้องหนึ่ง ๆ ดังนั้นการแยก tenant ไม่ต้องพึ่งพาว่า endpoint ทุกตัวจะจำต้องมีเงื่อนไข WHERE tenant_id = ... ชัยชนะหลักคือการลดข้อผิดพลาดแบบ “ลืมใส่เงื่อนไข” เมื่อแอปของคุณโตและคำสั่ง SQL เพิ่มขึ้น

เมื่อไหร่ RLS คุ้มกับความซับซ้อนที่เพิ่มขึ้น?

คุ้มเมื่อกฎการเข้าถึงสม่ำเสมอและอิงแถว เช่น การแยก tenant หรือการเข้าถึงตามสมาชิก และเมื่อคุณมีหลายเส้นทางการดึงข้อมูล (การค้นหา, การส่งออก, หน้าผู้ดูแล, งานแบ็คกราวด์) มันมักไม่คุ้มถ้ากฎส่วนใหญ่เป็นระดับคอลัมน์ มีข้อยกเว้นมาก หรือเน้นการรายงานข้าม tenant ขนาดใหญ่

RLS ปกป้องฉันจากอะไรไม่ได้บ้าง?

ใช้ RLS สำหรับการมองเห็นแถวและการควบคุมการเขียนขั้นพื้นฐาน ส่วนความเป็นส่วนตัวของคอลัมน์มักต้องใช้ views และสิทธิ์คอลัมน์ และกฎทางธุรกิจที่ซับซ้อน (เช่น ความเป็นเจ้าของการเรียกเก็บเงินหรือกระบวนการอนุมัติ) ยังคงเหมาะกับตรรกะในแอปหรือข้อจำกัดในฐานข้อมูลที่ออกแบบมาอย่างระมัดระวัง

วิธีที่ปลอดภัยที่สุดในการเริ่มใช้ RLS หากฉันยังใหม่คืออะไร?

สร้าง role ที่มีสิทธิ์น้อยสำหรับ API (ไม่ใช่เจ้าของตาราง) เปิด RLS แล้วเพิ่มนโยบาย SELECT และนโยบาย INSERT/UPDATE ที่มี WITH CHECK ตั้งค่าสถานะในเซสชันต่อคำร้อง (เช่น app.current_tenant) แล้วยืนยันว่าการสลับค่านั้นเปลี่ยนแถวที่คุณเห็นและเขียนได้

แอปของฉันควรบอก Postgres อย่างไรว่า tenant และ user ใดกำลังทำคำร้อง?

วิธีที่ใช้บ่อยคือเก็บค่าในตัวแปรเซสชันต่อคำร้อง ตั้งค่าตอนเริ่มทรานแซกชัน เช่น app.tenant_id และ app.user_id กุญแจคือความสม่ำเสมอ: ทุกเส้นทางโค้ด (เว็บ, งานแบ็คกราวด์, สคริปต์) ต้องตั้งค่าสิ่งเดียวกันที่นโยบายคาดหวัง มิฉะนั้นคุณจะเจอพฤติกรรม “คืนค่าเป็น 0 แถว” ที่สับสน

ความแตกต่างระหว่าง USING และ WITH CHECK ในนโยบาย RLS คืออะไร?

USING ควบคุมตัวกรองอ่าน: แถวที่ไม่ตรงเงื่อนไขจะมองไม่เห็นสำหรับ SELECT และไม่สามารถเป็นเป้าหมายของ UPDATE/DELETE ได้ ส่วน WITH CHECK เป็นประตูเขียน: กำหนดว่าแถวใหม่หรือแถวที่เปลี่ยนแล้วอนุญาตสำหรับ INSERT หรือ UPDATE หรือไม่

ทำไมคนถึงย้ำว่า “อย่าลืม WITH CHECK”?

เพราะถ้าคุณเพิ่มแค่นโยบาย USING จุดบกพร่องของ endpoint ยังอาจแทรกหรืออัปเดตแถวเข้า tenant ผิดได้ และผู้ใช้เดียวกันอาจอ่านแถวผิดนั้นไม่ได้เลยจนไม่สังเกต ดังนั้นให้จับคู่กฎอ่านกับ WITH CHECK สำหรับการเขียนเสมอเพื่อป้องกันการสร้างข้อมูลผิดตั้งแต่ต้น

ฉันควรจัดโครงสร้างสคีมาอย่างไรเพื่อให้นโยบาย RLS ง่ายและเร็ว?

หลีกเลี่ยงการ join ภายในนโยบายโดยใส่คีย์ tenant (เช่น org_id) ลงในตารางที่เป็นของ tenant โดยตรง แม้มันจะอ้างถึงตารางอื่นที่มี org_id ก็ตาม เพิ่มตารางสมาชิก (org_memberships, และถ้าจำเป็น project_memberships) เพื่อให้นโยบายสามารถตรวจสอบด้วยการค้นหาที่มีดัชนีเดียวแทนการอนุมานซับซ้อน

จะดีบักปัญหา “RLS ซ่อนข้อมูลของฉัน” โดยไม่ต้องเดาอย่างไร?

ก่อนอื่นจำลองสภาพแวดล้อมเซสชันเดียวกับที่แอปใช้ โดยตั้ง role และตัวแปรเซสชันเดียวกัน แล้วรันคำสั่ง SQL เดิมที่แอปรัน ต่อมายืนยันว่า RLS ถูกเปิดและตรวจดู pg_policies เพื่อดู USING และ WITH CHECK ที่ใช้งาน เพราะข้อผิดพลาดของ RLS มักเกิดจากบริบทตัวตนที่ขาดหาย มากกว่าที่จะเป็น “SQL ผิด”

ถ้าฉันใช้เครื่องมือสร้าง backend (เช่น Koder.ai) ฉันยังต้องออกแบบ RLS อย่างรอบคอบไหม?

ใช่ แต่ถือโค้ดที่สร้างขึ้นเป็นจุดเริ่มต้น ไม่ใช่ระบบความปลอดภัยสำเร็จรูป ถ้าคุณใช้ Koder.ai เพื่อสร้าง Go + PostgreSQL backend คุณยังต้องกำหนดโมเดล tenant ตั้งค่าตัวตนในเซสชันอย่างสม่ำเสมอ และเพิ่มนโยบายและเทสต์อย่างตั้งใจ เพื่อไม่ให้ตารางใหม่ถูกส่งขึ้นโดยไม่มีการป้องกันที่เหมาะสม

สารบัญ
ปัญหาจริงที่ RLS พยายามแก้ในแอป SaaSเมื่อ RLS ช่วยลดความยุ่งยาก (และเมื่อมันเพิ่มความเจ็บปวด)เบื้องต้นของโมเดลข้อมูลที่ทำให้นโยบาย RLS จัดการได้ง่ายวิธีที่นโยบาย RLS ทำงาน โดยไม่ต้องกลัวทีละขั้น: เพิ่ม RLS ให้ตารางหนึ่งและพิสูจน์ว่ามันทำงานแพตเทิร์นนโยบายที่คุณจะใช้ซ้ำในแอป SaaS ส่วนใหญ่กับดัก RLS ที่พบบ่อยซึ่งทำให้การดีบักเจ็บปวดการดีบัก RLS: วิธีปฏิบัติที่เห็นผลเช็คลิสต์ก่อนปล่อย RLS สู่โปรดักชันตัวอย่าง: แอปโปรเจกต์แบบ multi-tenant ที่มีการเข้าถึงตามการเป็นสมาชิกขั้นตอนถัดไป: นำ RLS ขึ้นอย่างปลอดภัยและรักษามันให้ง่ายต่อการดูแลคำถามที่พบบ่อย
แชร์
Koder.ai
Build your own app with Koder today!

The best way to understand the power of Koder is to see it for yourself.

Start FreeBook a Demo