2 นาที

เมนูนำทางที่รับรู้สิทธิ์ แต่ยังคงบังคับการเข้าถึง

เมนูนำทางที่รับรู้สิทธิ์ช่วยเพิ่มความชัดเจน แต่ความปลอดภัยต้องอยู่ที่ฝั่งเซิร์ฟเวอร์ ดูรูปแบบง่าย ๆ สำหรับบทบาท นโยบาย และการซ่อน UI อย่างปลอดภัย

เมนูนำทางที่รับรู้สิทธิ์ แต่ยังคงบังคับการเข้าถึง

ปัญหาที่เมนูตามสิทธิ์แก้ได้จริง ๆ

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

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

การซ่อน UI ไม่ใช่ความปลอดภัย มันคือความชัดเจน

เพื่อนร่วมงานที่อยากรู้อยากเห็นก็ยังสามารถ:

  • พิมพ์ deep link ไปยังหน้าที่ถูกซ่อนได้ถ้ารู้ URL
  • เปิดบุ๊กมาร์กเก่าไปยังหน้าผู้ดูแล
  • เรียก API โดยตรงด้วยสคริปต์หรือเครื่องมือ
  • แก้ไขคำขอจากเบราว์เซอร์แล้วลอง ID ต่าง ๆ

ดังนั้นสิ่งที่เมนูตามสิทธิ์แก้จริง ๆ คือการชี้แนะแบบตรงไปตรงมา มันทำให้อินเทอร์เฟซสอดคล้องกับงาน บทบาท และบริบทของผู้ใช้ ในขณะที่ทำให้เห็นชัดเมื่อบางสิ่งไม่สามารถใช้ได้

สถานะที่ดีคือ:

  • ผู้ใช้เห็นการกระทำที่เหมาะสมกับตนบ่อยครั้ง และไม่ค่อยเจอข้อผิดพลาดอย่างไม่คาดคิด
  • นักพัฒนามีแหล่งความจริงเดียวสำหรับกฎการเข้าถึง แทนที่จะกระจัดกระจาย
  • การเปลี่ยนแปลงของผลิตภัณฑ์ปลอดภัยขึ้น เพราะเมนูใหม่ทำให้คุณต้องตัดสินใจทันทีว่าต้องการสิทธิ์ใด
  • ความปลอดภัยถูกบังคับใช้ในที่ที่สำคัญ: ฝั่งเซิร์ฟเวอร์ปฎิเสธการกระทำที่ถูกห้ามเสมอ แม้ UI จะแสดงโดยผิดพลาด

ตัวอย่าง: ใน CRM ขนาดเล็ก Sales Rep ควรเห็น Leads และ Tasks แต่ไม่ควรเห็นการจัดการผู้ใช้ ถ้าพวกเขาวาง URL ของหน้าจัดการผู้ใช้หน้าเว็บจะต้องปิดการเข้าถึง และเซิร์ฟเวอร์ต้องบล็อกความพยายามในการเรียกดูรายชื่อผู้ใช้หรือเปลี่ยนบทบาทด้วย

การมองเห็นไม่ใช่การอนุญาต

การมองเห็นคือสิ่งที่อินเทอร์เฟซเลือกจะแสดง การอนุญาตคือสิ่งที่ระบบจะอนุญาตเมื่อคำขอไปถึงเซิร์ฟเวอร์

เมนูตามสิทธิ์ลดความสับสน หากใครบางคนจะไม่ถูกอนุญาตให้เห็น Billing หรือ Admin เลย การซ่อนรายการเหล่านั้นทำให้แอปสะอาดขึ้นและลดคำถามจากซัพพอร์ต แต่การซ่อนปุ่มไม่ใช่ประตูล็อก คนยังคงพยายามเรียก endpoint ที่อยู่เบื้องหลังได้ด้วย dev tools บุ๊กมาร์กเก่า หรือคำขอที่คัดลอกมา

กฎปฏิบัติ: ตัดสินใจว่าคุณอยากให้ประสบการณ์เป็นแบบไหน แล้วบังคับใช้กฎที่ฝั่งเซิร์ฟเวอร์ไม่ว่า UI จะทำอะไร

เมื่อคุณตัดสินใจจะแสดงการกระทำ มีสามรูปแบบที่ครอบคลุมส่วนใหญ่:

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

“ดูได้แต่แก้ไขไม่ได้” เป็นกรณีที่พบบ่อยและควรออกแบบอย่างชัดเจน แยกสิทธิ์เป็นสองอย่าง: หนึ่งสำหรับการอ่านข้อมูล และหนึ่งสำหรับการแก้ไข ในเมนูคุณอาจแสดงรายละเอียดลูกค้าให้ทุกคนที่มีสิทธิ์อ่าน แต่แสดงปุ่ม Edit customer เฉพาะผู้ที่มีสิทธิ์เขียน ในหน้าเพจ เรนเดอร์ฟิลด์เป็นแบบอ่านอย่างเดียวและปิดกั้นตัวควบคุมแก้ไข ในขณะที่ยังอนุญาตให้โหลดหน้าได้

สำคัญที่สุดคือ ฝั่งเซิร์ฟเวอร์ตัดสินผลสุดท้าย แม้ UI จะซ่อนการกระทำทั้งหมดของผู้ดูแล เซิร์ฟเวอร์ยังคงต้องตรวจสอบสิทธิ์ในทุกคำขอที่ละเอียดอ่อนและส่งกลับการตอบสนองว่า “ไม่อนุญาต” เมื่อมีการพยายาม

เลือกรูปแบบสิทธิ์ที่ดูแลรักษาได้

วิธีที่เร็วที่สุดในการส่งมอบเมนูตามสิทธิ์คือเริ่มด้วยโมเดลที่ทีมอธิบายได้ด้วยประโยคเดียว ถ้าคุณอธิบายไม่ได้ คุณจะรักษาความถูกต้องไม่ได้

ใช้บทบาทเพื่อจัดกลุ่ม ไม่ใช่เพื่อความหมาย Admin และ Support เป็นถังที่มีประโยชน์ แต่เมื่อบทบาทเริ่มเพิ่มขึ้น (Admin-West-Coast-ReadOnly) UI จะกลายเป็นเขาวงกตและฝั่งเซิร์ฟเวอร์จะกลายเป็นการคาดเดา

ให้สิทธิ์เป็นแหล่งความจริงสำหรับสิ่งที่ใครสักคนสามารถทำได้ เก็บให้เล็กและอิงการกระทำ เช่น invoice.create หรือ customer.export วิธีนี้ขยายได้ดีกว่าการเพิ่มบทบาท เพราะฟีเจอร์ใหม่มักเพิ่มการกระทำใหม่ ไม่ใช่ชื่อตำแหน่งงานใหม่

แล้วเพิ่มนโยบาย (policies) เพื่อบริบท นี่คือที่ที่คุณจัดการ “แก้ไขได้เฉพาะเรคคอร์ดของตัวเอง” หรือ “อนุมัติใบแจ้งหนี้ได้เฉพาะต่ำกว่า $5,000” นโยบายช่วยป้องกันการสร้างสิทธิ์ที่ซ้ำซ้อนหลายสิบรายการที่ต่างกันแค่เงื่อนไข

ชั้นการดูแลรักษาที่ควรมี:

  • Roles จัดกลุ่มสิทธิ์ (หน้าที่งาน)
  • Permissions อธิบายการกระทำ (ทำอะไร)
  • Policies เพิ่มบริบท (เมื่อไร, เรคคอร์ดไหน)

การตั้งชื่อสำคัญกว่าที่คนคิด หาก UI บอกว่า Export Customers แต่ API ใช้ download_all_clients_v2 คุณจะซ่อนสิ่งที่ผิดหรือบล็อกสิ่งที่ถูกในท้ายที่สุด จงใช้ชื่อตามคนอ่าน เข้าใจง่าย และใช้ร่วมกันระหว่าง frontend กับ backend:

  • ใช้รูปแบบ noun.verb (หรือ resource.action) อย่างสม่ำเสมอ
  • จับคู่ป้าย UI กับเจตนาของสิทธิ์ ไม่ใช่ชื่อโค้ดภายใน
  • ให้ชื่อสิทธิ์เก่ายังคงทำงานเมื่อคุณเปลี่ยนชื่อตัวฟีเจอร์

ตัวอย่าง: ใน CRM บทบาท Sales อาจมี lead.create และ lead.update แต่มีนโยบายจำกัดการแก้ไขให้เฉพาะ lead ที่ผู้ใช้เป็นเจ้าของ นั่นทำให้เมนูชัดเจน ในขณะที่ฝั่งเซิร์ฟเวอร์ยังเข้มงวด

ขั้นตอนทีละก้าว: นำบทบาทและสิทธิ์มาใช้งานตั้งแต่ต้นจนจบ

เมนูที่รับรู้สิทธิ์ให้ความรู้สึกดีเพราะลดความรกและป้องกันการคลิกผิด แต่จะช่วยได้ก็ต่อเมื่อฝั่งเซิร์ฟเวอร์ยังเป็นผู้ควบคุม คิดว่า UI เป็นคำแนะนำและเซิร์ฟเวอร์เป็นผู้ตัดสิน

เริ่มจากการเขียนสิ่งที่คุณปกป้อง ไม่ใช่หน้าเพจ แต่เป็นการกระทำ View customer list แตกต่างจาก export customers และ delete customer นี่คือกระดูกสันหลังของเมนูที่ไม่กลายเป็นการแสดงความปลอดภัย

สูตรปฏิบัติแบบเอนด์ทูเอนด์

  1. สำรวจการกระทำทั้ง UI และ API. สำหรับแต่ละฟีเจอร์ ให้ลงรายการการปฏิบัติจริง: read, create, update, delete, export, invite, change billing, run admin reports
  2. กำหนดสิทธิ์บนเซิร์ฟเวอร์เป็นแหล่งความจริง. เก็บการแมป role-to-permission ในฐานข้อมูล (หรือตั้งค่า) และทำให้ handler ของ API ทุกตัวถามว่า “ผู้ใช้คนนี้มีสิทธิ์ X บน resource Y หรือไม่?”
  3. ส่ง payload ความสามารถขนาดเล็กให้ UI. หลังจากล็อกอิน (หรือรีเฟรชเซสชัน) ส่ง boolean เช่น canEditCustomers, canDeleteCustomers, canExport หรือรายการสั้น ๆ ของสตริงสิทธิ์ เก็บให้เรียบง่าย
  4. บังคับเช็คในการเขียนทุกครั้ง รวมทั้งการอ่านที่ละเอียดอ่อน. ทุกการเปลี่ยนแปลงต้องตรวจสอบสิทธิ์ บางการอ่านก็ควรตรวจสอบด้วย (เช่น เงินเดือน, audit logs, exports หรือ endpoint ที่ข้ามการกรองปกติ)
  5. เพิ่มเทสที่มุ่งไปที่บทบาทสำคัญ. เลือก 2-3 บทบาทหลักและทดสอบการกระทำที่เสี่ยงที่สุด เช่น Sales สร้างดีลได้แต่ไม่สามารถ export customers ได้ และ Admin สามารถเชิญผู้ใช้ได้

กฎสำคัญเล็กน้อย: อย่าไว้ใจ flag ของบทบาทหรือสิทธิ์ที่มาจากไคลเอนต์ UI ได้ UI อาจซ่อนปุ่มตามความสามารถ แต่ API ต้องปฏิเสธคำขอที่ไม่ได้รับอนุญาต

รูปแบบ frontend ที่ยังตรงไปตรงมา

Turn learnings into credits
แชร์สิ่งที่คุณสร้างกับ Koder.ai และรับเครดิตสำหรับโปรเจกต์ต่อไป

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

สร้างเมนูจากแหล่งความจริงเดียว

แทนที่จะกระจายการตรวจสอบสิทธิ์ในทุกปุ่ม ให้กำหนดการนำทางจากคอนฟิกหนึ่งชุดที่รวมสิทธิ์ที่ต้องการสำหรับแต่ละรายการ แล้วเรนเดอร์จากคอนฟิกนั้น วิธีนี้ทำให้กฎอ่านง่ายและหลีกเลี่ยงการลืมเช็คในมุมแปลก ๆ ของ UI

รูปแบบเรียบง่ายเช่นนี้:

const menu = [
  { label: "Contacts", path: "/contacts", requires: "contacts.read" },
  { label: "Export", action: "contacts.export", requires: "contacts.export" },
  { label: "Admin", path: "/admin", requires: "admin.access" },
];

const visibleMenu = menu.filter(item => userPerms.includes(item.requires));

เลือกซ่อนทั้งส่วน (เช่น Admin) มากกว่าการกระจายเช็คบนทุกลิงก์ของหน้าผู้ดูแล นั่นคือจุดที่มีโอกาสผิดพลาดน้อยกว่า

สถานะที่ตรงไปตรงมา: ซ่อน vs ปิดใช้งาน

ซ่อนรายการเมื่อผู้ใช้จะไม่มีสิทธิ์ใช้งานเลย ปิดใช้งานเมื่อผู้ใช้มีสิทธิ์แต่ขาดบริบทในตอนนี้

ตัวอย่าง: ปุ่ม Delete contact ควรปิดใช้งานจนกว่าจะมีการเลือก contact เดียวกัน สิทธิ์เดียวกัน แต่ขาดบริบท เมื่อปิดใช้งานให้เพิ่มข้อความสั้น ๆ อธิบาย (tooltip ข้อความช่วยเหลือ หรือบันทึกในบรรทัด): เลือก contact เพื่อจะลบ

ชุดกฎที่ใช้งานได้จริง:

  • ซ่อนเมื่อผู้ใช้ไม่มีสิทธิ์
  • ปิดใช้งานเมื่อผู้ใช้มีสิทธิ์ แต่ยังไม่เลือกรายการ ฟอร์มไม่ถูกต้อง หรือข้อมูลยังโหลดอยู่
  • อย่าใช้ local storage เป็นความจริงสำหรับสิทธิ์ มันเป็นเพียงแคช
  • ตรวจสอบสิทธิ์อีกครั้งหลังล็อกอิน เปลี่ยนบทบาท หรือสลับบัญชี

การบังคับที่ฝั่งเซิร์ฟเวอร์ซึ่งเลี่ยงไม่ได้

การซ่อนเมนูช่วยให้ผู้ใช้โฟกัส แต่ไม่ได้ปกป้องอะไร ฝั่งเซิร์ฟเวอร์ต้องเป็นผู้ตัดสินสุดท้ายเพราะคำขอสามารถถูกเล่นซ้ำ แก้ไข หรือถูกเริ่มจากนอก UI ของคุณได้

กฎที่ดี: ทุกการกระทำที่เปลี่ยนข้อมูลต้องมีการตรวจสอบอนุญาตครั้งหนึ่ง ในที่เดียว ที่คำขอทุกคำขอผ่านได้ นั่นอาจเป็น middleware wrapper หรือเลเยอร์นโยบายขนาดเล็กที่คุณเรียกที่จุดเริ่มต้นของแต่ละ endpoint เลือกวิธีหนึ่งแล้วยึดตามมัน มิฉะนั้นคุณจะพลาดเส้นทาง

วางการเช็คบนเส้นทางคำขอ

แยกการอนุญาตออกจากการตรวจสอบอินพุตก่อน ตัดสินก่อนว่า “ผู้ใช้นี้อนุญาตหรือไม่?” แล้วค่อย validate payload หาก validate ก่อน คุณอาจเปิดเผยรายละเอียด (เช่น ID ของเรคคอร์ดที่มีอยู่) ให้กับคนที่ไม่ควรรู้ว่าการกระทำนั้นเป็นไปได้

รูปแบบที่ขยายได้:

  • ยืนยันผู้เรียกและสร้างตัวตนที่ชัดเจน (user id, org id, roles, permissions)
  • โหลดบริบทของ resource ที่ต้องการสำหรับการอนุญาต (มักเป็น owner id หรือ org id)
  • เรียกฟังก์ชันนโยบายเดียว (เช่น: Can(user, "invoice.delete", invoice))
  • หากปฏิเสธ หยุดทันทีและส่งสถานะที่เหมาะสม
  • แล้วจึง validate และทำธุรกิจโลจิก

ส่งคืนการตอบสนองที่ชัดเจนและสม่ำเสมอ

ใช้รหัสสถานะที่ช่วยทั้ง frontend และบันทึกของคุณ:

  • 401 Unauthorized เมื่อผู้เรียกยังไม่ได้ล็อกอิน
  • 403 Forbidden เมื่อล็อกอินแล้วแต่ไม่อนุญาต

ระวังการใช้ 404 Not Found เป็นการพราง มันมีประโยชน์เพื่อไม่บอกว่า resource มีอยู่ แต่ถ้าผสมแบบสุ่มการดีบักจะยากมาก เลือกกฎที่สอดคล้องกันต่อประเภท resource

ให้แน่ใจว่านโยบายการอนุญาตเดียวกันรันไม่ว่าแอคชันมาจากการคลิกปุ่ม แอปมือถือ สคริปต์ หรือการเรียก API โดยตรง

สุดท้าย บันทึกการพยายามที่ถูกปฏิเสธเพื่อการดีบักและการตรวจสอบ แต่เก็บบันทึกให้ปลอดภัย บันทึกว่าใคร ทำอะไร และประเภท resource ระดับสูง หลีกเลี่ยงฟิลด์ที่ละเอียดอ่อน payload เต็ม หรือความลับ

กรณีขอบที่ทำให้การออกแบบสำเร็จหรือพัง

บั๊กของสิทธิ์ส่วนใหญ่เกิดเมื่อผู้ใช้ทำสิ่งที่เมนูไม่ได้คาดคิด นั่นเป็นเหตุผลที่เมนูตามสิทธิ์มีประโยชน์ แต่ต้องออกแบบให้รองรับเส้นทางที่ข้ามเมนูด้วย

ถ้าเมนูซ่อน Billing สำหรับบทบาทหนึ่ง ผู้ใช้อาจวาง URL ที่บันทึกไว้หรือเปิดจากประวัติเบราว์เซอร์ จงปฏิบัติต่อการโหลดหน้าแต่ละครั้งเหมือนคำขอใหม่: ดึงสิทธิ์ปัจจุบันของผู้ใช้ และให้หน้าปฏิเสธการโหลดข้อมูลปกป้องเมื่อขาดสิทธิ์ ข้อความเป็นมิตรว่า “คุณไม่มีสิทธิ์เข้าถึง” ก็ใช้ได้ แต่การป้องกันจริงคือเซิร์ฟเวอร์ต้องส่งคืนข้อมูลว่าง

การเรียก API โดยตรงและ endpoint ที่เป็นกลุ่ม

ใคร ๆ ก็เรียก API ของคุณได้จาก dev tools สคริปต์ หรือไคลเอนต์อื่น ดังนั้นตรวจสอบสิทธิ์ในทุก endpoint ไม่ใช่แค่หน้าผู้ดูแล ความเสี่ยงที่มักพลาดคือ bulk actions: /items/bulk-update เดียวอาจทำให้ผู้ไม่ใช่แอดมินเปลี่ยนฟิลด์ที่พวกเขาไม่เห็นใน UI

บทบาทอาจเปลี่ยนระหว่างเซสชัน หากแอดมินถอดสิทธิ์ ผู้ใช้อาจยังมีโทเคนเก่าหรือสถานะเมนูที่แคชไว้ ใช้โทเคนอายุสั้นหรือการค้นหาสิทธิ์ฝั่งเซิร์ฟเวอร์ และจัดการกับการตอบกลับ 401/403 โดยการรีเฟรชสิทธิ์และอัปเดต UI

เครื่องที่ใช้ร่วมกันสร้างกับดักอีกแบบ: สถานะเมนูที่แคชอาจรั่วข้ามบัญชี เก็บสถานะการมองเห็นเมนูโดยมีคีย์เป็น user ID หรือหลีกเลี่ยงการเก็บถาวร

ห้าการทดสอบที่ควรทำก่อนออก:

  • เปิด deep link ไปยังหน้าที่ถูกซ่อนในขณะที่ล็อกอินเป็นบทบาทจำกัด
  • เรียก API ป้องกันด้วยคำขอที่คัดลอกมาจาก dev tools
  • เปลี่ยนบทบาทของผู้ใช้ขณะใช้งาน แล้วลองทำการกระทำซ้ำ
  • ออกจากระบบ ล็อกอินเป็นผู้ใช้อีกคน แล้วตรวจสอบว่าเมนูรีเซ็ต
  • ลอง endpoint แบบกลุ่มที่มีฟิลด์ผสมทั้งที่อนุญาตและไม่อนุญาต

ตัวอย่างสถานการณ์: เมนู CRM ง่าย ๆ ที่มีการบังคับจริง

Ship honest UI states
สร้าง endpoint ความสามารถและเชื่อม UI ของคุณกับมันโดยใช้ Koder.ai

จินตนาการ CRM ภายในที่มีสามบทบาท: Sales, Support, และ Admin ทุกคนล็อกอินและแอปแสดงเมนูด้านซ้าย แต่เมนูเป็นเครื่องอำนวยความสะดวก ความปลอดภัยจริง ๆ คือสิ่งที่เซิร์ฟเวอร์อนุญาต

นี่คือชุดสิทธิ์ง่าย ๆ ที่อ่านได้:

  • Leads: view, create, edit, delete, export
  • Tickets: view, create, edit, assign
  • Billing: view, edit
  • Users: view, manage

UI เริ่มจากขอการกระทำที่ผู้ใช้ปัจจุบันอนุญาตจาก backend (มักเป็นรายการสตริงของสิทธิ์) พร้อมบริบทพื้นฐานเช่น user id และทีม เมนูถูกสร้างจากนั้น หากคุณไม่มี billing.view คุณจะไม่เห็น Billing ถ้าคุณมี leads.export คุณจะเห็นปุ่ม Export ในหน้ารายชื่อ Leads ถ้าคุณแก้ไขได้เฉพาะ lead ของตัวเอง ปุ่ม Edit อาจยังปรากฏ แต่ควรปิดใช้งานหรือแสดงข้อความชัดเจนเมื่อ lead ไม่ใช่ของคุณ

ตอนนี้ส่วนสำคัญ: ทุก endpoint ของการกระทำบังคับกฎเดียวกัน

ตัวอย่าง: Sales สร้าง lead และแก้ไข lead ที่เป็นของตัวเองได้ Support ดู ticket และมอบหมายได้ แต่แตะ billing ไม่ได้ Admin จัดการผู้ใช้และ billing ได้

เมื่อใครสักคนพยายามลบ lead ฝั่งเซิร์ฟเวอร์ตรวจสอบ:

  1. ผู้ใช้มี leads.delete หรือไม่?
  2. ถ้ากฎเป็น own-only, lead.owner_id == user.id หรือไม่?
  3. ถ้า lead ถูกล็อก (เช่น ถูกออกใบแจ้งหนี้แล้ว) การลบถูกบล็อกสำหรับทุกคนยกเว้น Admin หรือเปล่า?

แม้ Support จะเรียก endpoint ลบด้วยตนเอง เขาก็จะได้การตอบกลับ forbidden เมนูที่ถูกซ่อนไม่เคยเป็นการป้องกัน การตัดสินใจของฝั่งเซิร์ฟเวอร์ต่างหาก

ข้อผิดพลาดและกับดักที่พบบ่อย

กับดักใหญ่ที่สุดคือคิดว่าจบงานเมื่อเมนูดูถูกต้อง การซ่อนปุ่มลดความสับสน แต่ไม่ลดความเสี่ยง

ข้อผิดพลาดที่พบบ่อย:

  • “ไม่เห็น” กลายเป็น “ทำไม่ได้.” ใครบางคนยังสามารถเรียก API โดยตรง ใช้ URL เก่า หรือล่อการกระทำผ่าน dev tools อย่าถือว่าการซ่อนคือเกต
  • ตรวจสอบสิทธิ์เฉพาะที่ UI. ถ้า frontend ตัดสินว่าใครลบเรคคอร์ดได้ คุณก็แพ้แล้ว ฝั่งเซิร์ฟเวอร์ต้องเป็นผู้ตัดสินสุดท้ายสำหรับการกระทำที่ป้องกัน
  • ใช้ flag isAdmin เดียวกับทุกอย่าง. มันดูเร็ว แต่ใช้ไปสักพักจะกลายเป็นข้อยกเว้นจนไม่มีใครอธิบายกฎการเข้าถึงได้
  • ไว้ใจไคลเอนต์เรื่องบทบาท. อย่าเชื่อ role, isAdmin, หรือ permissions ที่มาจากเบราว์เซอร์ ให้ได้มาจากเซสชันหรือโทเคนของคุณแล้วค้นหาบทบาทและสิทธิ์ฝั่งเซิร์ฟเวอร์
  • ลืมสิทธิ์การอ่าน. ทีมมักสนใจการเขียน แต่ข้อมูลรั่วมักมาจากการอ่าน การเรียกดูลูกค้า ดูใบแจ้งหนี้ การส่งออก CSV และการค้นหามักต้องมีการตรวจสอบด้วย

ตัวอย่างชัดเจน: คุณซ่อนเมนู Export leads สำหรับคนที่ไม่ใช่ผู้จัดการ ถ้า endpoint export ไม่ได้เช็คสิทธิ์ ผู้ใช้ที่เดาคำขอได้หรือคัดลอกจากเพื่อนร่วมงานก็ยังดาวน์โหลดไฟล์ได้

เช็คลิสต์ก่อนปล่อย

Prototype RBAC in minutes
สร้างแอปที่รับรู้สิทธิ์ใน Koder.ai จากพรอมต์แชทและพัฒนาอย่างปลอดภัย

ก่อนปล่อยเมนูตามสิทธิ์ ให้ตรวจสอบสุดท้ายโดยมุ่งที่สิ่งที่ผู้ใช้ทำได้จริง ไม่ใช่สิ่งที่เห็น

ไล่แอปของคุณเป็นแต่ละบทบาทและลองการกระทำเดียวกันทั้งใน UI และโดยการเรียก endpoint โดยตรง (หรือใช้ dev tools) เพื่อให้แน่ใจว่าเซิร์ฟเวอร์คือแหล่งความจริง

เช็คลิสต์:

  • สำหรับการกระทำที่ป้องกันแต่ละรายการ ให้ยืนยันว่ามีการเช็คสิทธิ์ฝั่งเซิร์ฟเวอร์ที่จุดการดำเนินการจริง (ไม่ใช่แค่ตอนสร้างเมนู)
  • ให้ UI เรนเดอร์การกระทำจากความสามารถที่เซิร์ฟเวอร์ส่งมา (ตัวอย่าง: can-list จากเซสชันหรือ endpoint ของสิทธิ์) แทนการเดาจากชื่อบทบาท
  • ตรวจสอบว่า UI จัดการการตอบกลับห้ามด้วยความปกติ: แสดงข้อความชัดเจน เก็บหน้าให้เสถียร และหลีกเลี่ยงการทิ้งผู้ใช้ในสถานะพัง
  • ทดสอบการเปลี่ยนบทบาทและสิทธิ์ เมื่อแอดมินอัปเดตการเข้าถึง ควรมีผลเร็วและคาดเดาได้ (รีเฟรชโทเคน ยกเลิกเซสชัน หรือการเวอร์ชันสิทธิ์)
  • รันเทสที่มีมูลค่าสูงครอบคลุมบทบาทและการกระทำที่เกี่ยวกับเงิน การส่งออก การลบ และการตั้งค่าแอดมิน

วิธีง่าย ๆ ในการหาช่องว่าง: เลือกปุ่ม “อันตราย” หนึ่งอัน (ลบผู้ใช้ ส่งออก CSV เปลี่ยน billing) แล้วติดตามจากต้นจนจบ เมนูควรถูกซ่อนเมื่อเหมาะสม endpoint ควรปฏิเสธการเรียกที่ไม่ได้รับอนุญาต และ UI ควรคืนสภาพได้เมื่อได้รับ 403

ขั้นตอนต่อไป: ปล่อยอย่างปลอดภัยโดยไม่ชะลอทีม

เริ่มเล็ก ๆ คุณไม่ต้องมีเมทริกซ์การเข้าถึงสมบูรณ์ในวันแรก เลือกการกระทำหลักไม่กี่รายการ (view, create, edit, delete, export, manage users) แมปกับบทบาทที่มี แล้วเดินหน้าต่อ เมื่อมีฟีเจอร์ใหม่ เพิ่มเฉพาะการกระทำใหม่ที่มันนำมา

ก่อนสร้างหน้าจอ ทำการวางแผนสั้น ๆ ที่ระบุรายการการกระทำ ไม่ใช่หน้าเดียว ปุ่ม Invoices ในเมนูอาจซ่อนการกระทำหลายอย่าง: ดูรายการ ดูรายละเอียด สร้าง คืนเงิน ส่งออก การเขียนรายการเหล่านี้ก่อนทำให้ทั้ง UI และกฎฝั่งเซิร์ฟเวอร์ชัดเจน และป้องกันการทำผิดพลาดที่มักเกิดจากการปิดกั้นทั้งหน้าแต่ปล่อย endpoint เสี่ยงไว้

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

รูทีนการปล่อยง่าย ๆ ช่วยทีมเคลื่อนไหวเร็วโดยไม่เดา:

  • เขียนรายการการกระทำสำหรับฟีเจอร์และตั้งชื่อสิทธิ์
  • เพิ่มเช็คฝั่งเซิร์ฟเวอร์ก่อน แล้วเชื่อม UI เข้ากับสิทธิ์เดียวกัน
  • ทดสอบหนึ่งบทบาทที่ได้รับอนุญาตและหนึ่งที่ถูกปฏิเสธสำหรับการกระทำเสี่ยงแต่ละรายการ
  • บันทึกการพยายามที่ถูกปฏิเสธ (ไม่เก็บข้อมูลละเอียดอ่อน) เพื่อหาช่องว่าง
  • สแนปชอตก่อนการเปิดตัวเพื่อย้อนกลับได้รวดเร็วถ้าจำเป็น

ถ้าคุณสร้างกับแพลตฟอร์มแชทอย่าง Koder.ai (koder.ai) โครงสร้างเดียวกันนี้ยังใช้ได้: เก็บ permissions และ policies ครั้งเดียว ให้ UI อ่านความสามารถจากเซิร์ฟเวอร์ และทำให้การเช็คฝั่งเซิร์ฟเวอร์เป็นสิ่งที่ไม่เลือกได้ในทุก handler.

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

What do permission-aware menus actually solve?

Permission-aware menus mostly solve clarity, not security. They help users focus on what they can actually do, reduce dead-end clicks, and cut “why am I seeing this?” support questions.

Security still has to be enforced on the backend, because anyone can try deep links, old bookmarks, or direct API calls regardless of what the UI shows.

When should I hide a menu item vs disable it?

Hide when a feature should be effectively undiscoverable for a role and there’s no expected path for them to use it.

Disable when the user might have access but is missing context right now, like no record selected, invalid form state, or data still loading. If you disable, add a short explanation so it doesn’t look broken.

Why is “hiding the button” not security?

Because visibility is not authorization. A user can paste a URL, reuse a bookmarked admin screen, or call your API outside your UI.

Treat the UI as guidance. Treat the backend as the final decision-maker for every sensitive request.

How should the frontend know what the user is allowed to do?

Your server should return a small “capabilities” response after login or session refresh, based on server-side permission checks. The UI then renders menus and buttons from that.

Do not trust client-provided flags like isAdmin coming back from the browser; compute permissions from the authenticated identity on the server.

What’s the simplest end-to-end way to implement permissions?

Start by inventorying actions, not pages. For each feature, separate things like read, create, update, delete, export, invite, and billing changes.

Then enforce each action in the backend handler (or middleware/wrapper) before doing any work. Wire the menu to the same permission names so UI and API stay aligned.

Should I use roles or permissions as the main model?

A practical default is: roles are buckets, permissions are the source of truth. Keep permissions small and action-based (for example, invoice.create), and attach them to roles.

If roles start multiplying to encode conditions (like region or ownership), move those conditions into policies instead of creating endless role variants.

How do I handle “can view but not edit” and other conditional access?

Use policies for contextual rules like “can edit only your own record” or “can approve invoices under a limit.” That keeps your permission list stable while still expressing real-world constraints.

The backend should evaluate the policy using resource context (like owner ID or org ID), not assumptions from the UI.

Do I really need permission checks on read endpoints too?

Not always. Reads that expose sensitive data or bypass normal filtering should be gated too, such as exports, audit logs, salary data, admin user lists, or any endpoint that returns more than the UI normally shows.

A good baseline is: all writes must be checked, and sensitive reads must be checked as well.

How do I avoid security holes with bulk actions and “power” endpoints?

Bulk endpoints are easy to miss because they can change many records or fields in one request. A user might be blocked in the UI but still hit /bulk-update directly.

Check permissions for the bulk action itself, and also validate which fields are allowed to be changed for that role, otherwise you can accidentally allow hidden fields to be edited.

What should happen when roles change mid-session or the UI cache is stale?

Assume permissions can change while someone is logged in. When the API returns 401 or 403, the UI should handle it as a normal state: refresh capabilities, update the menu, and show a clear message.

Also avoid persisting menu visibility in a way that can leak across accounts on shared devices; if you cache it, key it by user identity or don’t persist it at all.

How can I validate my implementation before rolling out?

Permission-aware menus are useful, but keep testing the actions behind them. When a new feature arrives, name each action and add backend checks first. Then wire the UI and run role-based tests.

Log denied attempts (without sensitive data) and snapshot behavior before rollout so you can roll back if a role breaks.

Related posts