0

Dẹp ngay đống if(role == 'admin') đi! Đây là cách Frontend Master xử lý Phân quyền (Authz)

Chào anh em thiện lành! Nếu bài trước mình đã chém gió về backend với Service Provider, thì hôm nay mình xin mạn phép đổi gió sang một góc nhìn mang đậm mùi "hành củ" của anh em Frontend/Fullstack: Phân quyền hiển thị UI (View-level Authorization).

Chắc hẳn ai đi code dạo cũng từng ít nhất một lần đau đầu với bài toán: "Thằng A được thấy nút này, thằng B không được thấy, thằng C thấy nhưng không bấm được".

Và thế là, chúng ta bắt đầu viết if-else. Nhưng tin mình đi, đó là khởi đầu của một thảm họa. Hôm nay, mình sẽ kể cho anh em nghe về Declarative view-level authz (Phân quyền mức giao diện theo kiểu Khai báo) - thứ đã cứu rỗi cuộc đời mình khỏi những đêm thức trắng dò bug.

1. Mở bài: Nỗi ám ảnh mang tên "Yêu cầu thay đổi luồng phân quyền"

Chuyện xảy ra vào năm thứ hai mình đi làm, lúc đó đang code một cái dashboard quản lý nội dung (CMS) bằng React. Ban đầu, requirement rất trong sáng: "Chỉ Admin mới được sửa/xóa bài viết của người khác, User thường chỉ được sửa bài của mình".

Thế là mình tặc lưỡi, phang luôn một đống logic "mệnh lệnh" (Imperative) rải rác khắp hàng chục component:

// Nằm lù lù ở file PostDetail.jsx
{ 
  (user.role === 'admin' || (user.role === 'user' && post.authorId === user.id)) && (
    <Button color="red">Delete Post</Button>
  )
}

Mọi thứ chạy ngon ơ... cho đến một ngày, khách hàng (PO) đập bàn: "Thêm cho anh quyền Moderator. Thằng này được xóa bài, nhưng không được xóa bài của Admin, và thêm một quyền Guest chỉ được xem".

Lúc đó, mặt mình đúng nghĩa biến sắc. Mớ logic if-else kia đang nằm rải rác ở danh sách bài viết, trang chi tiết, trang chỉnh sửa, sidebar, mobile menu... Việc tìm và sửa lại cái đống điều kiện đó không khác gì đi gỡ mìn. Mình đã sửa sót đúng một chỗ ở view Mobile, dẫn đến việc user bình thường có thể thấy nút Xóa và... bấm xóa luôn bài của sếp. Một bài học nhớ đời!

2. Hành trình giác ngộ: Declarative View-level Authz là gì?

Nguyên nhân gốc rễ của sai lầm trên là do mình đã trộn lẫn Business Logic (Luật phân quyền) vào UI Logic (Việc render giao diện). Mình đang code theo kiểu Imperative (Mệnh lệnh): Dạy cho UI biết chính xác các bước tính toán để quyết định ẩn/hiện.

Sau cú vấp đó, mình tìm hiểu và chuyển sang hệ tư tưởng Declarative (Khai báo).

Tư duy Declarative là gì? Bạn chỉ cần nói (khai báo) cho View biết MỤC ĐÍCH của bạn, còn việc tính toán đúng/sai cứ để một trung tâm khác lo. View chỉ nên ngu ngơ và làm đúng việc của nó là vẽ giao diện.

Thay vì viết cục if-else thô thiển, mình refactor lại code như sau (ví dụ sử dụng kết hợp với thư viện siêu đỉnh là CASL):

Bước 1: Tập trung mọi rule vào một Single Source of Truth (Nguồn chân lý duy nhất

Mình định nghĩa một file Ability.js để chứa TOÀN BỘ luật lệ phân quyền:

// abilities.js
import { defineAbility } from '@casl/ability';

export default function defineRulesFor(user) {
  return defineAbility((can, cannot) => {
    if (user.role === 'admin') {
      can('manage', 'all'); // Admin làm trùm
    } else if (user.role === 'moderator') {
      can('delete', 'Post');
      cannot('delete', 'Post', { authorRole: 'admin' }); // Không được đụng bài admin
    } else {
      can('read', 'Post');
      can(['update', 'delete'], 'Post', { authorId: user.id }); // Tự xử bài của mình
    }
  });
}

Bước 2: Thay đổi View thành "Khai báo"

Ở phía React, mình tạo ra một component bọc (Wrapper Component) tên là <Can>. Từ nay về sau, UI của mình nhìn "sạch như Ngọc Trinh":

// PostDetail.jsx
import { Can } from './AbilityContext';

const PostDetail = ({ post }) => {
  return (
    <div>
      <h1>{post.title}</h1>
      
      {/* KHAI BÁO: Tôi muốn XÓA cái BÀI VIẾT này. Cho phép thì hiện, không thì thôi! */}
      <Can I="delete" a={post}>
        <Button color="red">Delete Post</Button>
      </Can>

      <Can I="update" a={post}>
        <Button color="blue">Edit Post</Button>
      </Can>
    </div>
  );
};

Sự khác biệt?

  • View không còn quan tâm user.role là cái quái gì nữa. Nó chỉ đưa ra câu hỏi: "Tao muốn hành động delete trên object post này, được không?".
  • Khi khách hàng muốn thêm role Super Admin hay Super Moderator, mình KHÔNG CẦN CHẠM VÀO VIEW. Mình chỉ sửa duy nhất file abilities.js. Cả hệ thống tự động cập nhật.

3. Bài học rút ra (The Takeaways)

  1. Separation of Concerns (Tách biệt mối quan tâm): UI chỉ nên lo UI. Authorization Rules phải được tách ra một layer riêng (Policy layer).
  2. Khả năng mở rộng (Scalability) tuyệt đối: Khi logic phân quyền phức tạp lên (dựa vào gói subscription, dựa vào ngày hết hạn, dựa vào location...), file View của bạn vẫn không hề dài ra hay phức tạp thêm.
  3. Khả năng tái sử dụng (Reusability): Cái logic can('delete', post) có thể được dùng lại ở hàm gọi API, ở router guard, ở sidebar... đảm bảo frontend đồng nhất 100%.

Lời kết

Declarative view-level authz không phải là một thư viện cụ thể, nó là một tư duy thiết kế. Dù bạn dùng React, Vue, Angular hay thậm chí là render HTML từ Backend (như Blade của Laravel với @can), thì việc áp dụng tư duy này sẽ cứu bạn khỏi hàng tá technical debt trong tương lai.

Đừng để cái UI của bạn trở thành một bãi rác if-else. Hãy làm cho nó trong sáng như cách bạn tán tỉnh crush hồi cấp 3 vậy!

Anh em đang dùng giải pháp nào để xử lý phân quyền trên Frontend ở công ty? Dùng CASL, react-authorization, tự viết Custom Hook hay vẫn trung thành với "đạo tà" If-Else? Chia sẻ dưới comment để anh em cùng đàm đạo nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí