Menu điều hướng nhận biết quyền giúp tăng tính rõ ràng, nhưng bảo mật phải được thực thi ở backend. Xem các mẫu đơn giản cho vai trò, chính sách và cách ẩn UI an toàn.

Khi người ta nói “ẩn nút”, họ thường có một trong hai ý: giảm rối cho người dùng không dùng được tính năng, hoặc ngăn lạm dụng. Chỉ mục đầu tiên là điều có thể làm được ở frontend.
Menu điều hướng nhận biết quyền chủ yếu là công cụ UX. Chúng giúp ai đó mở app và ngay lập tức thấy họ có thể làm gì, thay vì chạm phải màn hình “Truy cập bị từ chối” sau mỗi vài lần nhấp. Chúng cũng giảm chi phí hỗ trợ bằng cách tránh các câu hỏi như “Tôi phê duyệt hoá đơn ở đâu?” hoặc “Tại sao trang này bị lỗi?”.
Ẩn giao diện không phải là bảo mật. Đó là sự rõ ràng.
Ngay cả một đồng nghiệp tò mò vẫn có thể:
Vì vậy vấn đề thật sự mà menu nhận biết quyền giải quyết là hướng dẫn trung thực. Chúng giữ giao diện phù hợp với công việc, vai trò và ngữ cảnh của người dùng, đồng thời làm rõ khi nào một thứ gì đó không khả dụng.
Một trạng thái tốt sẽ trông như sau:
Ví dụ: trong một CRM nhỏ, một Sales Rep nên thấy Leads và Tasks, nhưng không thấy User Management. Nếu họ dán URL quản lý người dùng thì trang phải fail-closed, và server vẫn chặn mọi cố gắng liệt kê người dùng hoặc thay đổi vai trò.
Hiển thị là những gì giao diện chọn hiển thị. Phân quyền là những gì hệ thống thực sự cho phép khi một request đến server.
Menu nhận biết quyền giảm bớt sự nhầm lẫn. Nếu ai đó sẽ không bao giờ được phép thấy Billing hoặc Admin, ẩn những mục đó giữ app gọn và giảm ticket hỗ trợ. Nhưng ẩn nút không phải là khoá. Người ta vẫn có thể thử endpoint bên dưới bằng dev tools, bookmark cũ hoặc request sao chép.
Một quy tắc thực tế: quyết định trải nghiệm bạn muốn, rồi thực thi quy tắc đó ở backend dù UI làm gì.
Khi bạn quyết định cách trình bày một hành động, ba mẫu sau bao phủ hầu hết trường hợp:
“Bạn có thể xem nhưng không sửa” là tình huống phổ biến và đáng thiết kế rõ ràng. Xử lý nó như hai permission: một cho đọc dữ liệu và một cho thay đổi. Trong menu, bạn có thể hiển thị Chi tiết Khách hàng cho mọi người có quyền đọc, nhưng chỉ hiển thị Chỉnh sửa khách hàng cho những ai có quyền ghi. Trên trang, render các trường ở chế độ chỉ đọc và khoá các điều khiển chỉnh sửa, trong khi vẫn cho phép trang tải.
Quan trọng nhất, backend quyết định kết quả cuối cùng. Ngay cả khi UI ẩn mọi hành động admin, server vẫn cần kiểm tra permission trên mọi request nhạy cảm và trả về phản hồi “không được phép” rõ ràng khi ai đó thử.
Cách nhanh nhất để triển khai menu nhận biết quyền là bắt đầu với một mô hình mà đội bạn có thể giải thích trong một câu. Nếu bạn không giải thích được, bạn sẽ không giữ được nó đúng.
Dùng roles để gom nhóm, không dùng roles để đặt nghĩa. Admin và Support là thùng hữu ích. Nhưng khi roles bắt đầu nhân lên (Admin-West-Coast-ReadOnly), UI trở thành mê cung và backend trở nên đoán mò.
Ưu tiên permissions làm nguồn chân lý cho những gì ai đó có thể làm. Giữ chúng nhỏ và theo hành động, như invoice.create hoặc customer.export. Điều này mở rộng tốt hơn so với role sprawl vì tính năng mới thường thêm hành động mới, không phải vị trí công việc mới.
Rồi thêm policies (luật) cho ngữ cảnh. Ở đây bạn xử lý “chỉ sửa record của chính bạn” hoặc “chỉ duyệt hoá đơn dưới $5,000”. Policies tránh việc bạn tạo hàng chục permission gần giống nhau chỉ khác bởi điều kiện.
Một lớp dễ duy trì trông như sau:
Đặt tên quan trọng hơn bạn nghĩ. Nếu UI nói Export Customers nhưng API dùng download_all_clients_v2, cuối cùng bạn sẽ ẩn sai thứ hoặc chặn nhầm thứ. Giữ tên thân thiện, nhất quán và dùng chung giữa frontend và backend:
noun.verb (hoặc resource.action) nhất quánVí dụ: trong CRM, role Sales có thể gồm lead.create và lead.update, nhưng một policy giới hạn update chỉ với lead mà người dùng sở hữu. Điều đó giữ menu rõ ràng trong khi backend vẫn nghiêm ngặt.
Menu nhận biết quyền đem lại cảm giác tốt vì giảm rối và tránh nhấp nhầm. Nhưng chúng chỉ hữu ích khi backend vẫn nắm quyền. Hãy coi UI như gợi ý, và server là thẩm phán.
Bắt đầu bằng việc ghi ra bạn đang bảo vệ gì. Không phải trang, mà là hành động. Xem danh sách khách hàng khác với export khách hàng và xóa khách hàng. Đây là xương sống của menu nhận biết quyền mà không biến thành kịch bản an ninh giả.
canEditCustomers, canDeleteCustomers, canExport, hoặc một danh sách gọn các chuỗi permission. Giữ nó tối giản.Một quy tắc nhỏ nhưng quan trọng: đừng bao giờ tin flag role hoặc permission do client gửi lên. UI có thể ẩn nút dựa trên capabilities, nhưng API vẫn phải từ chối các request không được phép.
Menu nhận biết quyền nên giúp người dùng tìm ra những gì họ có thể làm, không giả vờ thi hành bảo mật. Frontend là thanh ray hướng dẫn. Backend là khoá.
Thay vì rải kiểm tra permission khắp các nút, định nghĩa navigation từ một config duy nhất chứa permission yêu cầu cho mỗi mục, rồi render từ config đó. Điều này giữ quy tắc dễ đọc và tránh kiểm tra bị quên ở những ngóc ngách UI.
Một mẫu đơn giản như sau:
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));
Ưu tiên ẩn cả phần lớn (ví dụ Admin) thay vì rải các kiểm tra trên từng link trang admin. Ít nơi để sai hơn.
Ẩn mục khi người dùng sẽ không bao giờ được phép dùng chúng. Vô hiệu hoá khi người dùng có quyền, nhưng ngữ cảnh hiện tại thiếu.
Ví dụ: Xoá contact nên bị vô hiệu hoá cho tới khi một contact được chọn. Cùng permission, chỉ là thiếu ngữ cảnh. Khi vô hiệu hoá, thêm một thông tin ngắn “tại sao” gần control (tooltip, helper text hoặc note inline): Chọn contact để xóa.
Một bộ quy tắc bền:
Ẩn menu giúp người dùng tập trung, nhưng không bảo vệ gì cả. Backend phải là thẩm phán cuối cùng vì request có thể được replay, sửa, hoặc kích hoạt ngoài UI.
Một quy tắc tốt: mỗi hành động thay đổi dữ liệu cần một kiểm tra authorization duy nhất, ở một chỗ, mà mọi request phải đi qua. Có thể là middleware, wrapper handler, hoặc một lớp policy nhỏ bạn gọi ở đầu mỗi endpoint. Chọn một cách và tuân theo, nếu không bạn sẽ bỏ sót đường đi.
Tách authorization ra khỏi validation input. Trước hết quyết định “người này có được phép làm việc này không?”, rồi mới validate payload. Nếu bạn validate trước, có thể lộ thông tin (ví dụ ID bản ghi tồn tại) cho người không nên biết hành động đó.
Một mẫu mở rộng:
Can(user, "invoice.delete", invoice)).Dùng status code giúp frontend và logs:
401 Unauthorized khi caller chưa đăng nhập.403 Forbidden khi đã đăng nhập nhưng không được phép.Cẩn thận với 404 Not Found như mánh che giấu. Nó hữu ích để không tiết lộ resource tồn tại, nhưng nếu dùng lung tung thì debug sẽ khó. Chọn quy tắc nhất quán cho từng loại resource.
Đảm bảo cùng authorization chạy dù hành động đến từ click nút, app mobile, script, hay gọi API trực tiếp.
Cuối cùng, log các lần bị từ chối để debug và audit, nhưng giữ logs an toàn. Ghi ai, hành động gì, loại resource ở mức cao. Tránh các trường nhạy cảm, payload đầy đủ hoặc bí mật.
Hầu hết lỗi permission xuất hiện khi người dùng đi theo đường mà menu không dự đoán. Đó là lý do menu nhận biết quyền có ích, nhưng chỉ khi bạn thiết kế cho cả các đường vượt qua menu.
Nếu menu ẩn Billing cho một role, người dùng vẫn có thể dán URL lưu sẵn hoặc mở từ lịch sử trình duyệt. Xử lý mọi lần load trang như một request mới: lấy permissions hiện tại của user, và chính màn hình phải từ chối load dữ liệu bảo vệ khi thiếu permission. Một thông báo thân thiện “Bạn không có quyền truy cập” là ổn, nhưng bảo vệ thực sự là backend trả về không có dữ liệu.
Bất kỳ ai cũng có thể gọi API của bạn từ dev tools, script, hoặc client khác. Vì vậy kiểm tra permissions trên mọi endpoint, không chỉ các trang admin. Rủi ro dễ bỏ sót là bulk actions: một /items/bulk-update có thể vô tình cho non-admin thay đổi các field họ chưa bao giờ thấy trong UI.
Roles cũng có thể thay đổi giữa phiên. Nếu admin bỏ quyền, user có thể vẫn giữ token cũ hoặc menu cache cũ. Dùng token ngắn hạn hoặc lookup permission ở server, và xử lý 401/403 bằng cách refresh permissions và cập nhật UI.
Thiết bị chung tạo một bẫy khác: cache menu có thể rò rỉ giữa các account. Lưu visibility menu theo key user ID, hoặc tránh lưu vĩnh viễn.
Năm bài test đáng chạy trước khi release:
Hãy tưởng tượng một CRM nội bộ với ba role: Sales, Support và Admin. Mọi người đăng nhập và app hiển thị menu bên trái, nhưng menu chỉ là tiện nghi. An toàn thực sự là những gì server cho phép.
Đây là một bộ permission đơn giản và dễ đọc:
UI bắt đầu bằng việc hỏi backend về hành động mà user hiện tại được phép (thường là danh sách chuỗi permission) kèm ngữ cảnh cơ bản như user id và team. Menu được xây từ đó. Nếu bạn không có billing.view, bạn không thấy Billing. Nếu bạn có leads.export, bạn thấy nút Export trên màn hình Leads. Nếu bạn chỉ có thể edit lead của mình, nút Edit vẫn có thể xuất hiện nhưng nên bị vô hiệu hoá hoặc hiện thông báo rõ khi lead không thuộc sở hữu bạn.
Bây giờ phần quan trọng: mọi endpoint hành động đều thực thi cùng luật.
Ví dụ: Sales có thể tạo leads và sửa leads họ sở hữu. Support có thể xem tickets và gán tickets, nhưng không đụng vào billing. Admin có thể quản lý users và billing.
Khi ai đó cố xóa lead, backend kiểm tra:
leads.delete không?lead.owner_id == user.id không?Ngay cả khi một Support user gọi thủ công endpoint delete, họ sẽ nhận forbidden. Mục menu ẩn chưa bao giờ là biện pháp bảo vệ. Quyết định của backend mới là.
Cái bẫy lớn nhất với menu nhận biết quyền là nghĩ rằng công việc hoàn tất khi menu đã nhìn đúng. Ẩn nút giảm nhầm lẫn, nhưng không giảm rủi ro.
Những lỗi thường gặp nhất:
isAdmin cho mọi thứ. Ban đầu thấy nhanh, rồi lan rộng. Sớm hay muộn mọi ngoại lệ đều thành case đặc biệt và không ai giải thích nổi quy tắc truy cập.role, isAdmin, hoặc permissions từ browser là sự thật. Dẫn xuất danh tính và quyền từ session hoặc token của bạn, rồi lookup server-side.Ví dụ cụ thể: bạn ẩn menu Export leads cho non-managers. Nếu endpoint export không kiểm tra permission, bất kỳ user nào đoán được request (hoặc sao chép từ đồng nghiệp) vẫn có thể tải file.
Trước khi phát hành menu nhận biết quyền, làm một lần rà soát cuối cùng tập trung vào những gì người dùng thực sự có thể làm, không chỉ những gì họ thấy.
Đi qua app theo từng role chính và thử cùng một tập hành động. Làm cả bằng UI và bằng cách gọi endpoint trực tiếp (hoặc dev tools) để chắc server là nguồn chân lý.
Checklist:
Một cách thực tế để phát hiện lỗ hổng: chọn một nút “nguy hiểm” (xóa user, export CSV, thay billing) và trace end-to-end. Mục menu nên ẩn khi phù hợp, API nên từ chối các cuộc gọi trái phép, và UI nên hồi phục mượt khi nhận 403.
Bắt đầu nhỏ. Bạn không cần ma trận truy cập hoàn hảo ngay ngày đầu. Chọn vài hành động quan trọng (view, create, edit, delete, export, manage users), map chúng vào các role hiện có, và tiếp tục. Khi tính năng mới xuất hiện, chỉ thêm các hành động mới nó mang lại.
Trước khi xây màn hình, làm một pass lập kế hoạch nhanh liệt kê hành động, không phải trang. Một mục menu như Invoices che giấu nhiều hành động: view list, view details, create, refund, export. Viết chúng ra trước giúp UI và backend rõ ràng hơn, và tránh sai lầm phổ biến là chặn cả trang trong khi để endpoint rủi ro chưa được bảo vệ.
Khi refactor quy tắc truy cập, đối xử nó như thay đổi rủi ro: giữ lưới an toàn. Snapshot cho phép so sánh hành vi trước và sau. Nếu một role mất truy cập cần thiết, hoặc được cấp truy cập không đúng, rollback nhanh hơn sửa nóng trên production khi người dùng bị khoá.
Quy trình phát hành đơn giản giúp đội di chuyển nhanh mà không đoán mò:
Nếu bạn xây trên nền tảng chat như Koder.ai (koder.ai), cấu trúc này vẫn áp dụng: định nghĩa permissions và policies một lần, UI đọc capabilities từ server, và kiểm tra backend là bắt buộc trong mọi handler.
Permission-aware menus chủ yếu giải quyết sự rõ ràng, không phải bảo mật. Chúng giúp người dùng tập trung vào những gì họ thực sự có thể làm, giảm các lần nhấp đi vào ngõ cụt, và bớt các câu hỏi hỗ trợ kiểu “tại sao tôi thấy cái này?”.
Bảo mật vẫn phải được thực thi ở backend, vì bất kỳ ai cũng có thể thử deep link, bookmark cũ hoặc gọi API trực tiếp bất kể UI hiển thị ra sao.
Ẩn khi một tính năng nên hoàn toàn không được khám phá với một vai trò, và sẽ không có con đường hợp lệ để họ dùng nó.
Vô hiệu hoá khi người dùng có thể có quyền nhưng thiếu ngữ cảnh hiện tại, ví dụ chưa chọn bản ghi, form không hợp lệ, hoặc dữ liệu đang tải. Nếu vô hiệu hoá, thêm giải thích ngắn để người dùng không nghĩ là bị lỗi.
Bởi vì hiển thị không đồng nghĩa với phân quyền. Người dùng có thể dán URL, dùng bookmark admin cũ, hoặc gọi API ngoài UI.
Hãy coi UI như hướng dẫn. Hãy coi backend là nơi quyết định cuối cùng cho mọi yêu cầu nhạy cảm.
Server của bạn nên trả về một phản hồi “capabilities” nhỏ sau khi đăng nhập hoặc refresh session, dựa trên kiểm tra phân quyền ở server. UI sau đó render menu và nút từ dữ liệu đó.
Không tin vào flag do client cung cấp như isAdmin từ trình duyệt; hãy tính toán quyền từ danh tính đã xác thực trên server.
Bắt đầu bằng việc lập danh sách các hành động, không phải trang. Với mỗi tính năng, tách rõ: read, create, update, delete, export, invite, thay đổi billing.
Sau đó bắt buộc mỗi hành động ở handler backend (hoặc middleware/wrapper) trước khi làm việc gì khác. Nối menu với cùng tên quyền để UI và API đồng bộ.
Mặc định thực tế: roles là thùng chứa, permissions là nguồn chân lý. Giữ permissions nhỏ và theo hành động (ví dụ invoice.create), và gán chúng vào roles.
Nếu roles bắt đầu nhân lên để mã hoá điều kiện (ví dụ theo vùng hay chỉ đọc), hãy chuyển điều kiện đó vào chính sách thay vì tạo vô số variant role.
Dùng chính sách cho các quy tắc có điều kiện như “chỉ sửa record của mình” hoặc “chỉ duyệt hoá đơn dưới một ngưỡng”. Điều này giữ danh sách permission ổn định nhưng vẫn diễn đạt được các ràng buộc thực tế.
Backend nên đánh giá chính sách dựa trên ngữ cảnh resource (như owner ID, org ID), không dựa trên giả định từ UI.
Không phải lúc nào cũng cần. Những read trả về dữ liệu nhạy cảm hoặc bỏ qua lọc bình thường cũng nên bị kiểm soát, ví dụ export, audit logs, dữ liệu lương, danh sách user admin, hoặc bất kỳ endpoint trả nhiều dữ liệu hơn UI thường hiển thị.
Quy tắc tốt: mọi write phải được kiểm tra, và các read nhạy cảm cũng cần kiểm tra.
Endpoints bulk dễ bị quên vì có thể thay đổi nhiều record hoặc field trong một request. Người dùng có thể bị chặn ở UI nhưng vẫn gọi /items/bulk-update trực tiếp.
Kiểm tra permission cho hành động bulk và kiểm tra thêm những field được phép thay đổi cho vai trò đó, nếu không bạn có thể vô tình cho phép chỉnh các field vốn bị ẩn.
Giả sử quyền có thể thay đổi khi người dùng đang đăng nhập. Khi API trả 401 hoặc 403, UI nên xử lý như trạng thái bình thường: refresh capabilities, cập nhật menu, và hiển thị thông báo rõ ràng.
Cũng tránh lưu trạng thái menu theo cách có thể rò rỉ giữa các tài khoản trên thiết bị chia sẻ; nếu cache, hãy key theo user ID hoặc không lưu vĩnh viễn.