Làm cách nào để triển khai Danh sách kiểm soát truy cập trong ứng dụng Web MVC của tôi?


96

Câu hỏi đầu tiên

Xin vui lòng, bạn có thể giải thích cho tôi cách ACL đơn giản nhất có thể được triển khai trong MVC.

Đây là cách tiếp cận đầu tiên của việc sử dụng Acl trong Bộ điều khiển ...

<?php
class MyController extends Controller {

  public function myMethod() {        
    //It is just abstract code
    $acl = new Acl();
    $acl->setController('MyController');
    $acl->setMethod('myMethod');
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...    
  }

}
?>

Đó là cách tiếp cận rất tệ, và điểm trừ là chúng ta phải thêm đoạn mã Acl vào phương thức của mỗi bộ điều khiển, nhưng chúng ta không cần thêm bất kỳ phụ thuộc nào!

Phương pháp tiếp theo là tạo tất cả các phương thức của bộ điều khiển privatevà thêm mã ACL vào __callphương thức của bộ điều khiển .

<?php
class MyController extends Controller {

  private function myMethod() {
    ...
  }

  public function __call($name, $params) {
    //It is just abstract code
    $acl = new Acl();
    $acl->setController(__CLASS__);
    $acl->setMethod($name);
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...   
  }

}
?>

Nó tốt hơn so với mã trước đó, nhưng điểm tối thiểu chính là ...

  • Tất cả các phương thức của bộ điều khiển phải là riêng tư
  • Chúng ta phải thêm mã ACL vào phương thức __call của mỗi bộ điều khiển.

Phương pháp tiếp theo là đặt mã Acl vào Bộ điều khiển mẹ, nhưng chúng ta vẫn cần giữ kín tất cả các phương thức của Bộ điều khiển con.

Giải pháp là gì? Và thực hành tốt nhất là gì? Tôi nên gọi các hàm Acl ở đâu để quyết định phương thức cho phép hoặc không cho phép được thực thi.

Câu hỏi thứ hai

Câu hỏi thứ hai là về việc nhận vai trò bằng cách sử dụng Acl. Hãy tưởng tượng rằng chúng ta có khách, người dùng và bạn bè của người dùng. Người dùng đã hạn chế quyền truy cập để xem hồ sơ của mình mà chỉ bạn bè mới có thể xem được. Tất cả khách không thể xem hồ sơ của người dùng này. Vì vậy, đây là logic ..

  • chúng ta phải đảm bảo rằng phương thức được gọi là hồ sơ
  • chúng tôi phải phát hiện chủ nhân của hồ sơ này
  • chúng tôi phải phát hiện xem người xem có phải là chủ sở hữu của tiểu sử này hay không
  • chúng tôi phải đọc các quy tắc hạn chế về hồ sơ này
  • chúng ta phải quyết định thực thi hay không thực thi phương pháp hồ sơ

Câu hỏi chính là về việc phát hiện chủ sở hữu của hồ sơ. Chúng tôi có thể phát hiện ai là chủ sở hữu của hồ sơ chỉ thực thi phương thức của mô hình $ model-> getOwner (), nhưng Acl không có quyền truy cập vào mô hình. Làm thế nào chúng ta có thể thực hiện điều này?

Tôi hy vọng rằng suy nghĩ của tôi là rõ ràng. Xin lỗi vì tiếng Anh của tôi.

Cảm ơn bạn.


1
Tôi thậm chí không hiểu tại sao bạn cần "Danh sách kiểm soát truy cập" cho các tương tác của người dùng. Bạn sẽ không chỉ nói một cái gì đó giống như if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(khác, hiển thị "Bạn không cần phải truy cập vào của thành viên này hồ sơ" hoặc một cái gì đó giống như là tôi không có được nó?.
Buttle Butkus

2
Có lẽ, bởi vì Kirzilla muốn quản lý tất cả các điều kiện để truy cập ở một nơi - chủ yếu là trong cấu hình. Vì vậy, bất kỳ thay đổi nào về quyền có thể được thực hiện trong Quản trị viên thay vì thay đổi mã.
Mariyo,

Câu trả lời:


185

Phần đầu tiên / câu trả lời (triển khai ACL)

Theo ý kiến ​​khiêm tốn của tôi, cách tốt nhất để tiếp cận điều này là sử dụng mẫu trang trí , Về cơ bản, điều này có nghĩa là bạn lấy đối tượng của mình và đặt nó bên trong một đối tượng khác, nó sẽ hoạt động như một lớp vỏ bảo vệ. Điều này sẽ KHÔNG yêu cầu bạn mở rộng lớp ban đầu. Đây là một ví dụ:

class SecureContainer
{

    protected $target = null;
    protected $acl = null;

    public function __construct( $target, $acl )
    {
        $this->target = $target;
        $this->acl = $acl;
    }

    public function __call( $method, $arguments )
    {
        if ( 
             method_exists( $this->target, $method )
          && $this->acl->isAllowed( get_class($this->target), $method )
        ){
            return call_user_func_array( 
                array( $this->target, $method ),
                $arguments
            );
        }
    }

}

Và đây sẽ là cách bạn sử dụng loại cấu trúc này:

// assuming that you have two objects already: $currentUser and $controller
$acl = new AccessControlList( $currentUser );

$controller = new SecureContainer( $controller, $acl );
// you can execute all the methods you had in previous controller 
// only now they will be checked against ACL
$controller->actionIndex();

Như bạn có thể nhận thấy, giải pháp này có một số ưu điểm:

  1. ngăn chặn có thể được sử dụng trên bất kỳ đối tượng nào, không chỉ các trường hợp của Controller
  2. kiểm tra ủy quyền xảy ra bên ngoài đối tượng đích, có nghĩa là:
    • đối tượng ban đầu không chịu trách nhiệm kiểm soát truy cập, tuân theo SRP
    • khi bạn nhận được "quyền bị từ chối", bạn không bị khóa bên trong bộ điều khiển, nhiều tùy chọn hơn
  3. bạn có thể đưa phiên bản bảo mật này vào bất kỳ đối tượng nào khác, nó sẽ giữ lại sự bảo vệ
  4. quấn nó và quên nó đi .. bạn có thể giả vờ rằng nó là đối tượng ban đầu, nó sẽ phản ứng như cũ

Tuy nhiên , có một vấn đề lớn với phương pháp này - bạn không thể tự nhiên kiểm tra xem giao diện và triển khai đối tượng được bảo mật (cũng áp dụng cho việc tìm kiếm các phương thức hiện có) hay là một phần của chuỗi kế thừa nào đó.

Phần thứ hai / câu trả lời (RBAC cho các đối tượng)

Trong trường hợp này, sự khác biệt chính mà bạn nên nhận ra là Bản thân các Đối tượng Miền của bạn (ví dụ Profile:) chứa thông tin chi tiết về chủ sở hữu. Điều này có nghĩa là để bạn kiểm tra, nếu (và ở cấp độ nào) người dùng có quyền truy cập vào nó, nó sẽ yêu cầu bạn thay đổi dòng này:

$this->acl->isAllowed( get_class($this->target), $method )

Về cơ bản, bạn có hai lựa chọn:

  • Cung cấp ACL với đối tượng được đề cập. Nhưng bạn phải cẩn thận để không vi phạm Luật Demeter :

    $this->acl->isAllowed( get_class($this->target), $method )
  • Yêu cầu tất cả các chi tiết liên quan và chỉ cung cấp ACL những gì nó cần, điều này cũng sẽ làm cho nó trở nên thân thiện hơn với thử nghiệm đơn vị:

    $command = array( get_class($this->target), $method );
    /* -- snip -- */
    $this->acl->isAllowed( $this->target->getPermissions(), $command )

Một số video có thể giúp bạn tìm ra cách triển khai của riêng mình:

Ghi chú bên lề

Có vẻ như bạn đã hiểu khá phổ biến (và hoàn toàn sai lầm) về Model trong MVC là gì. Mô hình không phải là một lớp học . Nếu bạn có lớp được đặt tên FooBarModelhoặc thứ gì đó kế thừa AbstractModelthì bạn đang làm sai.

Trong MVC thích hợp, Model là một lớp, chứa rất nhiều lớp. Phần lớn các lớp có thể được tách thành hai nhóm, dựa trên trách nhiệm:

- Logic kinh doanh miền

( đọc thêm : đâyđây ):

Các phiên bản từ nhóm lớp này xử lý việc tính toán các giá trị, kiểm tra các điều kiện khác nhau, thực hiện các quy tắc bán hàng và thực hiện tất cả những việc còn lại mà bạn gọi là "logic nghiệp vụ". Họ không có manh mối về cách dữ liệu được lưu trữ, nơi nó được lưu trữ hoặc ngay cả khi nơi lưu trữ tồn tại ở vị trí đầu tiên.

Đối tượng Domain Business không phụ thuộc vào cơ sở dữ liệu. Khi bạn tạo hóa đơn, không quan trọng dữ liệu đến từ đâu. Nó có thể là từ SQL hoặc từ API REST từ xa, hoặc thậm chí là ảnh chụp màn hình của tài liệu MSWord. Logic kinh doanh không thay đổi.

- Truy cập và lưu trữ dữ liệu

Các cá thể được tạo từ nhóm lớp này đôi khi được gọi là Đối tượng truy cập dữ liệu. Thường là các cấu trúc thực hiện mẫu Data Mapper (đừng nhầm lẫn với các ORM cùng tên .. không có quan hệ). Đây là nơi chứa các câu lệnh SQL của bạn (hoặc có thể là DomDocument của bạn, vì bạn lưu trữ nó bằng XML).

Bên cạnh hai phần chính, có một nhóm các trường hợp / lớp nữa, cần được đề cập:

- Dịch vụ

Đây là nơi các thành phần của bạn và bên thứ 3 phát huy tác dụng. Ví dụ: bạn có thể coi "xác thực" là dịch vụ, có thể được cung cấp bởi chính bạn hoặc một số mã bên ngoài. Ngoài ra "mail sender" sẽ là một dịch vụ, có thể kết hợp một số đối tượng miền với PHPMailer hoặc SwiftMailer hoặc thành phần mail-sender của riêng bạn.

Một nguồn dịch vụ khác là trừu tượng hóa trên miền và các lớp truy cập dữ liệu. Chúng được tạo ra để đơn giản hóa mã được sử dụng bởi bộ điều khiển. Ví dụ: việc tạo tài khoản người dùng mới có thể yêu cầu làm việc với một số đối tượng miền và trình ánh xạ . Tuy nhiên, bằng cách sử dụng một dịch vụ, nó sẽ chỉ cần một hoặc hai dòng trong bộ điều khiển.

Điều bạn phải nhớ khi thực hiện các dịch vụ, đó là toàn bộ lớp phải mỏng . Không có logic kinh doanh trong dịch vụ. Họ chỉ ở đó để tung hứng đối tượng miền, các thành phần và trình ánh xạ.

Một trong những điểm chung của tất cả chúng là các dịch vụ không ảnh hưởng đến lớp View theo bất kỳ cách nào trực tiếp và tự trị ở mức độ như vậy, chúng có thể được (và thường xuyên bỏ qua - được) sử dụng bên ngoài cấu trúc MVC. Ngoài ra, các cấu trúc tự duy trì như vậy làm cho việc di chuyển sang một khuôn khổ / kiến ​​trúc khác dễ dàng hơn nhiều, bởi vì sự kết hợp cực kỳ thấp giữa dịch vụ và phần còn lại của ứng dụng.


34
Tôi chỉ học được nhiều hơn trong 5 phút khi đọc lại nó, nhiều hơn tôi có được trong nhiều tháng. Bạn có đồng ý với: bộ điều khiển mỏng gửi đến các dịch vụ thu thập dữ liệu chế độ xem không? Ngoài ra, nếu bạn trực tiếp chấp nhận câu hỏi, vui lòng gửi tin nhắn cho tôi.
Stephane

2
Tôi đồng ý một phần. Việc thu thập dữ liệu từ chế độ xem xảy ra bên ngoài bộ ba MVC, khi bạn khởi tạo Requestphiên bản (hoặc một số tương tự của nó). Bộ điều khiển chỉ trích xuất dữ liệu từ Requestcá thể và chuyển hầu hết dữ liệu đến các dịch vụ thích hợp (một số trong số đó cũng được xem). Các dịch vụ thực hiện các hoạt động mà bạn đã yêu cầu họ thực hiện. Sau đó, khi chế độ xem đang tạo phản hồi, nó yêu cầu dữ liệu từ các dịch vụ và dựa trên thông tin đó, tạo phản hồi. Phản hồi đã nói có thể là HTML được tạo từ nhiều mẫu hoặc chỉ là một tiêu đề vị trí HTTP. Phụ thuộc vào trạng thái do bộ điều khiển thiết lập.
tereško

4
Để sử dụng giải thích đơn giản: bộ điều khiển "ghi" vào mô hình và xem, xem "đọc" từ mô hình. Lớp mô hình là cấu trúc thụ động trong tất cả các mẫu liên quan đến Web được lấy cảm hứng từ MVC.
tereško

@Stephane, đối với việc đặt câu hỏi trực tiếp, bạn luôn có thể nhắn tin cho tôi trên twitter. Hay bạn có câu hỏi kiểu "dạng dài", không thể được nhồi nhét trong 140 ký tự?
tereško

Đọc từ mô hình: điều đó có nghĩa là một số vai trò tích cực đối với mô hình? Tôi chưa bao giờ nghe điều đó trước đây. Tôi luôn có thể gửi cho bạn một liên kết qua twitter nếu đó là sở thích của bạn. Như bạn có thể thấy, những phản hồi này nhanh chóng chuyển thành cuộc trò chuyện và tôi đang cố gắng tôn trọng trang web này và những người theo dõi twitter của bạn.
Stephane

16

ACL và Bộ điều khiển

Trước hết: Đây là những thứ / lớp khác nhau thường xuyên nhất. Khi bạn chỉ trích mã bộ điều khiển mẫu, nó đặt cả hai lại với nhau - rõ ràng là quá chặt chẽ.

tereško đã vạch ra một cách làm thế nào để bạn có thể phân tách điều này nhiều hơn với mẫu trang trí.

Trước tiên, tôi sẽ quay lại một bước để tìm kiếm vấn đề ban đầu mà bạn đang gặp phải và thảo luận về vấn đề đó một chút sau đó.

Một mặt, bạn muốn có các bộ điều khiển chỉ thực hiện công việc mà chúng được lệnh (lệnh hoặc hành động, hãy gọi nó là lệnh).

Mặt khác, bạn muốn có thể đưa ACL vào ứng dụng của mình. Lĩnh vực hoạt động của các ACL này phải - nếu tôi hiểu đúng câu hỏi của bạn - để kiểm soát quyền truy cập vào các lệnh nhất định của các ứng dụng của bạn.

Do đó, loại kiểm soát truy cập này cần một thứ gì đó khác kết hợp hai điều này lại với nhau. Dựa trên ngữ cảnh mà một lệnh được thực thi, ACL sẽ khởi động và các quyết định cần được thực hiện liệu một lệnh cụ thể có thể được thực thi bởi một chủ thể cụ thể hay không (ví dụ: người dùng).

Hãy tóm tắt đến thời điểm này những gì chúng ta có:

  • Chỉ huy
  • ACL
  • Người sử dụng

Thành phần ACL là trung tâm ở đây: Nó cần biết ít nhất một điều gì đó về lệnh (để xác định chính xác lệnh) và nó cần có khả năng xác định người dùng. Người dùng thường dễ dàng được xác định bằng một ID duy nhất. Nhưng thường trong các ứng dụng web có những người dùng hoàn toàn không được xác định, thường được gọi là khách, ẩn danh, mọi người, v.v. Đối với ví dụ này, chúng tôi giả định rằng ACL có thể sử dụng một đối tượng người dùng và gói gọn các chi tiết này lại. Đối tượng người dùng bị ràng buộc với đối tượng yêu cầu ứng dụng và ACL có thể sử dụng nó.

Điều gì về xác định một lệnh? Cách diễn giải của bạn về mẫu MVC cho thấy rằng một lệnh là kết hợp của tên lớp và tên phương thức. Nếu chúng ta quan sát kỹ hơn sẽ có các đối số (tham số) chẵn cho một lệnh. Vì vậy, nó hợp lệ để hỏi điều gì xác định chính xác một lệnh? Tên lớp, tên phương thức, số lượng hoặc tên của các đối số, thậm chí là dữ liệu bên trong bất kỳ đối số nào hoặc hỗn hợp của tất cả những thứ này?

Tùy thuộc vào mức độ chi tiết mà bạn cần để xác định một lệnh trong ACL'ing của mình, điều này có thể khác nhau rất nhiều. Đối với ví dụ, hãy giữ nó đơn giản và chỉ định rằng một lệnh được xác định bằng tên lớp và tên phương thức.

Vì vậy, bối cảnh của ba phần này (ACL, Command và User) thuộc về nhau như thế nào bây giờ rõ ràng hơn.

Chúng tôi có thể nói, với sự phù hợp ACL tưởng tượng, chúng tôi đã có thể làm như sau:

$acl->commandAllowedForUser($command, $user);

Chỉ cần xem điều gì đang xảy ra ở đây: Bằng cách làm cho cả lệnh và người dùng đều có thể nhận dạng được, ACL có thể thực hiện nó. Công việc của ACL không liên quan đến công việc của cả đối tượng người dùng và lệnh cụ thể.

Chỉ thiếu một bộ phận, cái này không thể sống trong không khí. Và nó không. Vì vậy, bạn cần xác định vị trí mà kiểm soát truy cập cần khởi động. Hãy xem điều gì xảy ra trong một ứng dụng web tiêu chuẩn:

User -> Browser -> Request (HTTP)
   -> Request (Command) -> Action (Command) -> Response (Command) 
   -> Response(HTTP) -> Browser -> User

Để xác định vị trí đó, chúng tôi biết nó phải có trước khi lệnh cụ thể được thực thi, vì vậy chúng tôi có thể giảm danh sách đó và chỉ cần xem xét các địa điểm (tiềm năng) sau:

User -> Browser -> Request (HTTP)
   -> Request (Command)

Tại một số điểm trong ứng dụng của bạn, bạn biết rằng một người dùng cụ thể đã yêu cầu thực hiện một lệnh cụ thể. Bạn đã thực hiện một số loại ACL ở đây: Nếu người dùng yêu cầu một lệnh không tồn tại, bạn không cho phép lệnh đó thực thi. Vì vậy, bất cứ nơi nào xảy ra trong ứng dụng của bạn có thể là một nơi tốt để thêm các kiểm tra ACL "thực":

Lệnh đã được định vị và chúng tôi có thể tạo ra nhận dạng của nó để ACL có thể xử lý nó. Trong trường hợp lệnh không được phép đối với người dùng, lệnh sẽ không được thực hiện (hành động). Có thể CommandNotAllowedResponsethay vì CommandNotFoundResponsecho trường hợp, một yêu cầu không thể được giải quyết bằng một lệnh cụ thể.

Nơi ánh xạ một HTTPRequest cụ thể được ánh xạ vào một lệnh thường được gọi là Định tuyến . Vì Định tuyến đã có công việc định vị một lệnh, tại sao không mở rộng nó để kiểm tra xem lệnh đó có thực sự được phép trên mỗi ACL không? Ví dụ như bằng cách mở rộng Router đến một router biết ACL: RouterACL. Nếu bộ định tuyến của bạn chưa biết User, thì Routerkhông phải là nơi thích hợp, bởi vì để ACL hoạt động không chỉ có lệnh mà còn phải xác định được người dùng. Vì vậy, địa điểm này có thể khác nhau, nhưng tôi chắc chắn rằng bạn có thể dễ dàng xác định vị trí bạn cần mở rộng, vì đó là nơi đáp ứng đầy đủ yêu cầu của người dùng và lệnh:

User -> Browser -> Request (HTTP)
   -> Request (Command)

Người dùng có sẵn ngay từ đầu, Lệnh đầu tiên với Request(Command).

Vì vậy, thay vì đặt các kiểm tra ACL của bạn bên trong triển khai cụ thể của mỗi lệnh, bạn đặt nó trước nó. Bạn không cần bất kỳ mẫu nặng nề, phép thuật hay bất cứ điều gì, ACL thực hiện công việc của nó, người dùng thực hiện công việc và đặc biệt là lệnh thực hiện công việc của nó: Chỉ là lệnh, không có gì khác. Lệnh không quan tâm đến việc liệu các vai trò có áp dụng cho nó hay không, nếu nó được bảo vệ ở đâu đó hay không.

Vậy nên hãy cứ để những thứ không thuộc về nhau. Sử dụng bản ghi lại một chút Nguyên tắc trách nhiệm duy nhất (SRP) : Chỉ nên có một lý do để thay đổi lệnh - bởi vì lệnh đã thay đổi. Không phải vì bây giờ bạn giới thiệu ACL'ing trong ứng dụng của mình. Không phải vì bạn chuyển đổi đối tượng Người dùng. Không phải vì bạn di chuyển từ giao diện HTTP / HTML sang SOAP hoặc giao diện dòng lệnh.

ACL trong trường hợp của bạn kiểm soát quyền truy cập vào một lệnh, không phải chính lệnh đó.


Hai câu hỏi: CommandNotFoundResponse & CommandNotAllowedResponse: bạn sẽ chuyển những câu hỏi này từ lớp ACL đến Bộ định tuyến hoặc Bộ điều khiển và mong đợi một phản hồi chung? 2: Nếu bạn muốn bao gồm phương thức + thuộc tính, bạn sẽ xử lý điều đó như thế nào?
Stephane

1: Response là phản hồi, ở đây nó không phải từ ACL mà là từ router, ACL giúp router tìm ra kiểu phản hồi (not found, đặc biệt là: Cấm). 2: Phụ thuộc. Nếu bạn muốn nói các thuộc tính là tham số từ các hành động và bạn cần ACL với các tham số, hãy đặt chúng dưới ACL.
hakre

13

Một khả năng là bọc tất cả các bộ điều khiển của bạn trong một lớp khác mở rộng Bộ điều khiển và để nó ủy quyền tất cả các lệnh gọi hàm cho thể hiện được bọc sau khi kiểm tra ủy quyền.

Bạn cũng có thể thực hiện điều đó ngược dòng nhiều hơn, trong trình điều phối (nếu ứng dụng của bạn thực sự có) và tra cứu các quyền dựa trên URL, thay vì các phương pháp kiểm soát.

chỉnh sửa : Việc bạn cần truy cập cơ sở dữ liệu, máy chủ LDAP, v.v. có trực giao với câu hỏi hay không. Ý của tôi là bạn có thể triển khai ủy quyền dựa trên các URL thay vì các phương thức của bộ điều khiển. Điều này mạnh mẽ hơn vì bạn thường sẽ không thay đổi URL của mình (loại giao diện công khai của khu vực URL), nhưng bạn cũng có thể thay đổi việc triển khai bộ điều khiển của mình.

Thông thường, bạn có một hoặc một số tệp cấu hình nơi bạn ánh xạ các mẫu URL cụ thể tới các phương thức xác thực và chỉ thị ủy quyền cụ thể. Người điều phối, trước khi gửi yêu cầu đến người điều khiển, xác định xem người dùng có được ủy quyền hay không và hủy bỏ việc điều phối nếu không.


Làm ơn, bạn có thể cập nhật câu trả lời của mình và bổ sung thêm chi tiết về Điều phối viên không. Tôi có điều phối viên - nó phát hiện phương thức của trình điều khiển mà tôi nên gọi bằng URL. Nhưng tôi không thể hiểu bằng cách nào tôi có thể nhận được vai trò (tôi cần truy cập vào DB để làm điều đó) trong Dispatcher. Hy vọng sớm được nghe tin từ bạn.
Kirzilla

Aha, có ý tưởng của bạn. Tôi nên quyết định cho phép thực thi hay không mà không cần truy cập vào phương thức! Thích! Câu hỏi cuối cùng chưa được giải đáp - cách truy cập mô hình từ Acl. Có ý kiến ​​gì không?
Kirzilla

@Kirzilla Tôi gặp vấn đề tương tự với Bộ điều khiển. Có vẻ như phụ thuộc phải có ở đâu đó. Ngay cả khi không phải là ACL, vậy còn lớp mô hình thì sao? Làm thế nào bạn có thể ngăn điều đó trở thành phụ thuộc?
Stephane
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.