Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Thành Thức Phân Quyền Ruby: Pundit vs. CanCanCan

Ngày nay, nhiều ứng dụng web sẽ có các trang có sẵn công khai — như trang chủ — và các trang an toàn hơn nơi người dùng phải đăng nhập để truy cập. Quy trình đăng ký người dùng, đăng nhập và theo dõi trạng thái phiên người dùng được gọi là "xác thực" (authentication).

Cùng lúc đó, khi đối mặt với người dùng đã đăng nhập, cần tách biệt các hành động và tài nguyên có sẵn cho họ tùy thuộc vào vai trò người dùng. Ví dụ, "quản trị viên" (admins) thường có quyền truy cập nhiều hơn người dùng bình thường. Quy trình tách biệt quyền truy cập của người dùng đã xác thực được gọi là "phân quyền" (authorization).

Trong bài viết này, chúng ta sẽ khám phá hai thư viện phân quyền phổ biến nhất trong Ruby cho đến nay: Pundit và CanCanCan.

Hãy đi sâu vào việc này!

Thiết Lập & Điều Kiện Tiên Quyết

Trong bài viết này, chúng ta sẽ sử dụng một ứng dụng Rails 7 đơn giản có người dùng và bài viết (posts). Người dùng sẽ được gán vai trò "editor" (biên tập viên) hoặc "writer" (người viết). Senario như vậy rất hoàn hảo để trình bày cách phân quyền hoạt động.

Xem kho mã (code repos) cho ứng dụng ví dụ của chúng ta với:

  • Pundit
  • CanCanCan

Dù bài viết tập trung vào phân quyền, chủ thể đi kèm là xác thực không thể bỏ qua.

Chúng ta sẽ không đi sâu vào chi tiết thiết lập xác thực vì đó nằm ngoài phạm vi bài viết. Bạn có thể làm theo hướng dẫn cài đặt trong tài liệu gem Devise (chúng ta sẽ kết hợp Devise với các gem phân quyền của mình).

Một điều nữa — dù bạn dùng Pundit hay CanCanCan, công việc thực tế định nghĩa vai trò người dùng của ứng dụng không xảy ra tự động khi bạn cài đặt một trong hai gem phân quyền. Bạn sẽ cần thiết lập nó thủ công.

Hãy làm điều đó ngay.

Định Nghĩa Vai Trò Người Dùng

Giả sử bạn đã cài đặt gem Devise và thiết lập model User. Bước tiếp theo là quyết định vai trò mà người dùng ứng dụng sẽ có. Trong trường hợp của chúng ta, chúng ta sẽ thiết lập các vai trò sau:

  • Writer (Người viết) - Vai trò này sẽ có thể tạo, chỉnh sửa, cập nhật và xóa bài viết của chính họ. Đồng thời, một writer cũng có thể xem bài viết của các writer khác.
  • Editor (Biên tập viên) - Người dùng có vai trò editor có thể chỉnh sửa, cập nhật, xem và xóa bài viết của bất kỳ người dùng nào, nhưng họ không thể tạo bài viết của riêng mình.

Thêm một cột để lưu vai trò của người dùng (sử dụng migration để sửa đổi bảng users):


Sau đó chạy migration:


Và sau đó sửa đổi model user để bao gồm các vai trò chúng ta vừa định nghĩa:


Trong khi đang ở đây, hãy tiếp tục thiết lập model Post với các thuộc tính title và body, cũng như tham chiếu đến người dùng viết bài:


Chúng ta thêm khóa ngoại user_id vào model Post để liên kết mọi bài viết được tạo với một người dùng cụ thể. Vì chúng ta đã thiết lập xác thực người dùng bằng Devise, chúng ta có thể đơn giản sửa đổi phương thức create của Posts controller để tự động đặt user_id thành người dùng đang đăng nhập:


Chúng ta cũng có thể sửa đổi model User để đảm bảo nó liên kết với model Post:


Cuối cùng, hãy tạo dữ liệu mẫu (seed) cho cơ sở dữ liệu với một số người dùng, mỗi người với một vai trò khác nhau:


Sau đó seed cơ sở dữ liệu:


Cho đến nay, ứng dụng của chúng ta hiện có:

  • Xác thực được thiết lập bằng Devise.
  • Hai vai trò người dùng được định nghĩa là "writer" và "editor".
  • Một model Post.

Với điều đó, chúng ta hiện có mọi thứ cần thiết để làm việc đúng cách với Pundit.

Phân Quyền Trong Ứng Dụng Ruby Với Pundit

Pundit là một thư viện phân quyền được xây dựng dựa trên kiến trúc hướng đối tượng và các lớp Ruby thuần túy. Nó cung cấp cho bạn các công cụ để xây dựng một lớp phân quyền vững chắc có thể mở rộng cùng với ứng dụng của bạn.

Cài Đặt Pundit Trong Ứng Dụng Ruby Của Bạn

Thêm gem vào Gemfile của ứng dụng:


Sau đó trong terminal, chạy lệnh:


Hoặc chạy lệnh sau:


Vì phân quyền chủ yếu liên quan đến việc cấp hoặc từ chối truy cập vào tài nguyên controller, bước tiếp theo là thêm module phân quyền của Pundit vào application controller:


Và cuối cùng, tạo một lớp policy cơ sở mà tất cả các policy khác sẽ kế thừa từ:


Điều này cho bạn lớp policy cơ sở sau:


Với đó, Pundit hiện đã được thiết lập đúng cách và sẵn sàng sử dụng. Ngoài ra, chúng ta đã có xác thực và vai trò người dùng được thiết lập.

Tiếp theo, hãy sử dụng các policy để triển khai các quy tắc định nghĩa cách mỗi vai trò người dùng sẽ truy cập tài nguyên Post.

Cấu Hình Policy Của Pundit

Trong thuật ngữ của Pundit, một "policy" là một lớp Ruby thuần túy nơi bạn định nghĩa tất cả các quy tắc về cách một vai trò người dùng tương tác với các tài nguyên khác nhau.

Các policy này đi kèm với một số tính năng đáng chú ý:

  • Mỗi policy được đặt tên theo một model hiện có, thêm hậu tố "Policy". Ví dụ, policy định nghĩa cách model Post được truy cập được gọi là PostPolicy.
  • Một attr_reader: nhận hai đối số - đối số đầu tiên là user, cụ thể là người dùng đang đăng nhập — current_user — và đối số thứ hai là model bạn muốn định nghĩa quy tắc phân quyền cho, trong trường hợp của chúng ta là post.
  • Các phương thức truy vấn (query methods) sẽ ánh xạ tới các phương thức controller của tài nguyên đã thiết lập quy tắc phân quyền. Với mục đích tổ chức, tốt nhất là để tất cả các policy nằm trong thư mục app/policies.

Vì chúng ta biết các quy tắc truy cập cần định nghĩa cho các vai trò người dùng khác nhau trong ứng dụng, hãy bắt đầu với vai trò writer.

Định Nghĩa Policy Pundit Cho Một Vai Trò

Hãy dùng vai trò writer để xem cách thực hiện điều này. Để bắt đầu, chúng ta có thể phác thảo quyền truy cập của writer vào tài nguyên Post như sau:

  • Tạo bài viết của chính họ
  • Chỉnh sửa và cập nhật bài viết của chính họ
  • Xem (hoặc đọc) bài viết của chính họ cũng như của người dùng khác
  • Xóa bài viết của chính họ

Với điều đó trong tâm trí, hãy tạo một policy mới để kiểm soát cách truy cập bài viết:


Điều này cho chúng ta lớp policy chung sau kế thừa từ policy cơ sở chúng ta tạo trước đó:


Hãy thêm quy tắc truy cập cho vai trò writer tương ứng (các quy tắc này cũng ghi đè bất kỳ quy tắc nào được kế thừa từ lớp policy cơ sở):


Ở đây, chúng ta định nghĩa những gì writer có thể làm khi tạo, chỉnh sửa, cập nhật và xóa bài viết - các hành động tương ứng trên controller posts. Nếu bạn đã nhận ra, các quy tắc này áp dụng cho bài viết nói chung và không nhất thiết là bài viết của writer (chúng ta sẽ đến phần đó ở mục scopes).

Hiện tại, hãy xem cách sử dụng policy này.

Sử Dụng Policy Trong Pundit

Để sử dụng policy, gọi phương thức authorize của Pundit trên phương thức controller nơi bạn muốn kiểm tra quy tắc truy cập. Bạn khởi tạo lớp policy liên quan và, cụ thể hơn, hành động cần được gọi dựa trên nơi phương thức authorize được gọi.

Ví dụ, hãy gọi authorize trên phương thức create của post controller:


Để kiểm tra điều này, hãy đăng nhập với tư cách editor và thử create một bài viết. Việc này sẽ dẫn đến lỗi sau:

Thành Thức Phân Quyền Ruby: Pundit vs. CanCanCan

Mặc dù điều này làm những gì chúng ta muốn, nhưng hiển thị trang lỗi như vậy không tốt cho trải nghiệm người dùng. Trong phần tiếp theo, bạn sẽ học cách rescue (xử lý) NotAuthorizedError và phục vụ thứ gì đó thân thiện hơn với người dùng.

Rescue Từ NotAuthorizedError Của Pundit

Đầu tiên, chúng ta cần sửa ApplicationController như sau:


Ở đây, chúng ta về cơ bản yêu cầu Pundit sử dụng phương thức user_not_authorized bất cứ khi nào NotAuthorizedError được ném ra. Nó sẽ đơn giản chuyển hướng người dùng không được ủy quyền đến một trang cụ thể và cung cấp thông báo flash liên quan giải thích điều gì đã xảy ra:

Thành Thức Phân Quyền Ruby: Pundit vs. CanCanCan

Chúng ta hiện có một hệ thống phân quyền đơn giản có thể xử lý trường hợp quyền hạn rất tổng quát.

Nhưng nếu chúng ta muốn quyền hạn chi tiết hơn? Để làm điều đó, chúng ta cần tận dụng scopes (phạm vi) của Pundit.

Scopes Của Pundit

Scopes của Pundit tương tự như scopes của ActiveRecord. Trong ActiveRecord, bạn có thể dùng scopes để lấy các bản ghi theo tiêu chí cụ thể. Tuy nhiên, với scopes của Pundit, bạn quản lý quyền truy cập vào các tài nguyên cụ thể theo các quy tắc bạn thiết lập.

Giả sử chúng ta muốn editor có thể xem và chỉnh sửa bài viết ở trạng thái "draft" (nháp), và đồng thời cho phép writer tạo, xem, chỉnh sửa, cập nhật và xóa bài viết chỉ thuộc về họ.

Chúng ta có thể bắt đầu bằng cách sửa policy post như sau:


Sau đó chúng ta sẽ authorize (ủy quyền) truy cập tài nguyên trong posts controller, như sau:


Tất nhiên, scoping với Pundit có thể sâu hơn nhiều những gì chúng ta đã chỉ ra, nhưng chúng ta sẽ dừng lại ở đây trong bài viết này. Nếu bạn muốn, hãy xem tài liệu Pundit để biết cách sử dụng scopes theo cách nâng cao hơn.

Hiện tại, hãy xem qua cách bạn có thể sử dụng thư viện này với strong parameters của Rails.

Sử Dụng Pundit Với Strong Parameters Của Rails

Bằng cách kết hợp các quy tắc phân quyền của Pundit với strong parameters của Rails, bạn có thể đạt được quyền truy cập khóa chặt (lockdown) vào các thuộc tính của tài nguyên. Giả sử bạn muốn editor là người duy nhất có quyền truy cập trường excerpt của model Post. Làm thế nào để thực hiện?

Đầu tiên, thêm một khối phù hợp vào policy liên quan:


Sau đó, sửa đổi khối permitted params trong controller:


Với điều đó, bạn đã làm cho thuộc tính excerpt chỉ khả dụng cho editor.

Chúng ta sẽ chuyển sang thư viện phân quyền khác, CanCanCan.

Giới Thiệu CanCanCan Cho Ứng Dụng Ruby Của Bạn

CanCanCan là một thư viện phân quyền sử dụng một lớp "ability" (khả năng) để định nghĩa ai có quyền truy cập gì trong ứng dụng Rails. Kiểm soát truy cập thực tế được đạt được bằng cách sử dụng module phân quyền và các view helper khác nhau.

Cài Đặt CanCanCan

Cài đặt dễ dàng như chạy lệnh dưới đây:


Giống như Pundit, với CanCanCan, bạn có thể định nghĩa tất cả các quy tắc truy cập trong một đối tượng lớp Ruby thuần túy gọi là lớp "ability". Hãy làm điều đó tiếp theo với lệnh generator sau:


Điều này tạo ra đối tượng lớp dưới đây:


Hãy tìm hiểu thêm về cách lớp ability có thể định nghĩa quy tắc truy cập cho ứng dụng Rails ví dụ của chúng ta tiếp theo.

Định Nghĩa & Kiểm Tra Khả Năng (Abilities) Của CanCanCan

Chúng ta sẽ dùng cùng các vai trò người dùng như ví dụ Pundit: writer và editor. Writer có thể tạo, chỉnh sửa, cập nhật, hủy (destroy) bài viết của chính mình, và xem bài viết của writer khác; editor có thể làm mọi thứ trừ tạo bài viết của riêng mình.

Để sử dụng CanCanCan, trước tiên hãy định nghĩa những gì mỗi người dùng hoặc vai trò có thể truy cập trong lớp ability, theo định dạng này:


Là ví dụ:


Sau đó, trong controller, hãy kiểm tra xem quy tắc truy cập có tồn tại cho một hành động cụ thể không — dùng ví dụ của chúng ta, hành động edit:


Với điều này, nếu chúng ta truy cập view chỉnh sửa bài viết với tư cách một writer khác, chúng ta nhận được lỗi dưới đây:

Thành Thức Phân Quyền Ruby: Pundit vs. CanCanCan

Và giống như chúng ta đã làm với Pundit, hãy rescue lỗi này và hiển thị cho người dùng một trang lỗi tốt hơn.

Xử Lý Lỗi "Access Denied" Của CanCanCan

Bất cứ khi nào tài nguyên không được ủy quyền, CanCanCan sẽ ném ra lỗi CanCan::AccessDenied. Cách dễ nhất để bắt ngoại lệ này là sửa đổi ApplicationController như sau:


Việc này tạo ra trải nghiệm người dùng tốt. Trong ảnh chụp màn hình dưới đây, người dùng không được ủy quyền được chuyển hướng về trang chủ và hiển thị thông báo flash liên quan:

Thành Thức Phân Quyền Ruby: Pundit vs. CanCanCan

Bạn thậm chí có thể tùy chỉnh thông báo lỗi hiển thị cho người dùng:


Nếu ứng dụng của bạn phục vụ XML như phản hồi, hoặc bạn chỉ muốn đi sâu hơn vào xử lý ngoại lệ CanCanCan::AccessDenied, hãy xem tài liệu CanCanCan. Hiện tại, hãy xem cách chúng ta có thể kết hợp các abilities khác nhau của CanCanCan để tạo một lớp phân quyền vững chắc hơn.

Kết Hợp Nhiều Abilities Của CanCanCan

Bạn có thể định nghĩa nhiều quy tắc truy cập cho một tài nguyên trong lớp ability. Lấy ví dụ vai trò writer và editor, chúng ta có thể làm như sau:


Câu hỏi là, tại sao bạn muốn làm điều này?

Với CanCanCan, bạn có thể định nghĩa tất cả quy tắc truy cập trong một file ability. Điều này có cả ưu và nhược điểm.

Việc tập trung tất cả quy tắc ở một nơi rất tiện lợi để xử lý quy tắc truy cập vì tất cả quy tắc được định nghĩa ở một chỗ.

Tuy nhiên, nếu ứng dụng của bạn xử lý nhiều vai trò người dùng hoặc bạn có một số tài nguyên cần phân quyền, lớp ability dễ dàng trở nên quá lớn và phức tạp để xử lý. Một cách để giải quyết là tổ chức lại lớp ability để sử dụng định nghĩa phương thức, như sau:


Còn rất nhiều về CanCanCan không thể bao quát hiệu quả trong bài viết này. Hãy xem tài liệu chi tiết của CanCanCan để tìm hiểu thêm.

Để kết thúc, hãy chạm nhẹ đến các tính năng của từng thư viện và đưa ra lý do tại sao bạn có thể chọn cái này thay vì cái kia.

So Sánh Tính Năng: Pundit vs. CanCanCan Cho Ứng Dụng Ruby Của Bạn

  • Tổ chức file - Với Pundit, bạn có thể dễ dàng tổ chức phân quyền ứng dụng qua nhiều file policy. Nhưng với CanCanCan, quy tắc phân quyền sẽ nằm trong một file ability. Làm việc với nhiều file ability vẫn có thể, nhưng đây không phải là phong cách triển khai mặc định.
  • Kiểm thử (Tests) - Vì mã phân quyền chủ yếu nằm trong một lớp duy nhất so với nhiều lớp ability của Pundit, viết test cho CanCanCan có thể dễ hơn so với Pundit.
  • Helpers - Cả hai thư viện cung cấp nhiều view helper để kiểm tra quyền trong lớp view. CanCanCan cho bạn phương thức can? dùng trong view, trong khi Pundit cung cấp chức năng tương đối tương tự với helper policy.
  • Tích hợp Devise - Như bạn thấy từ các ví dụ trong bài viết, cả hai thư viện đều tích hợp với Devise rất tốt.

Tóm Tắt

Trong bài viết này, chúng ta đã xem xét hai gem phân quyền phổ biến nhất trong hệ sinh thái Ruby và Rails: Pundit và CanCanCan.

Cả hai thư viện đều cung cấp bộ tính năng phong phú để quản lý quyền trong ứng dụng Rails của bạn. Vì vậy, gần như không thể nói cho bạn biết nên chọn gem nào — cả hai đều có thể quản lý ngay cả các thiết lập quyền phức tạp nhất.

Chúng tôi khuyến khích bạn thử cả Pundit và CanCanCan trong ứng dụng của mình và xem cái nào phù hợp nhất với nhu cầu của bạn.

Chúc bạn viết mã vui vẻ!

P.S. Nếu bạn muốn đọc các bài Ruby Magic ngay khi chúng xuất bản, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ bất kỳ bài viết nào!