如何在我的 Web MVC 应用程序中实现访问控制列表?第一部分/答案(ACL 实现)第二部分/答案(对象的 RBAC)附注

2022-08-30 07:43:08

第一个问题

请问,您能解释一下如何在MVC中实现最简单的ACL吗?

这是在控制器中使用 Acl 的第一种方法...

<?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!");
    ...    
  }

}
?>

这是非常糟糕的方法,它的缺点是我们必须将Acl代码段添加到每个控制器的方法中,但我们不需要任何其他依赖项!

下一种方法是制作所有控制器的方法,并将 ACL 代码添加到控制器的方法中。private__call

<?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!");
    ...   
  }

}
?>

它比以前的代码更好,但主要的缺点是...

  • 所有控制器的方法都应该是私有的
  • 我们必须将 ACL 代码添加到每个控制器的__call方法中。

下一种方法是将 Acl 代码放入父控制器中,但我们仍需要将所有子控制器的方法保密。

解决方案是什么?什么是最佳实践?我应该在哪里调用 Acl 函数来决定允许或不允许执行方法。

第二个问题

第二个问题是关于使用 Acl 获取角色。让我们想象一下,我们有客人,用户和用户的朋友。用户对查看其个人资料的权限受到限制,只有朋友才能查看。所有来宾都无法查看此用户的个人资料。所以,这是逻辑。

  • 我们必须确保被调用的方法是配置文件
  • 我们必须检测此配置文件的所有者
  • 我们必须检测查看者是此配置文件的所有者还是没有
  • 我们必须阅读有关此配置文件的限制规则
  • 我们必须决定执行或不执行配置文件方法

主要问题是关于检测配置文件的所有者。我们可以检测谁是配置文件的所有者,只执行模型的方法$model->getOwner(),但 Acl 无权访问模型。我们如何实现这一点?


答案 1

第一部分/答案(ACL 实现)

在我看来,解决这个问题的最好方法是使用装饰器图案,基本上,这意味着你把你的物体,把它放在另一个物体里面,这将像一个保护壳。这不需要您扩展原始类。下面是一个示例:

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
            );
        }
    }

}

这就是你使用这种结构的方式:

// 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();

您可能会注意到,此解决方案具有以下几个优点:

  1. 包含可用于任何对象,而不仅仅是Controller
  2. 检查授权发生在目标对象之外,这意味着:
    • 原始对象不负责访问控制,遵守SRP
    • 当您获得“权限被拒绝”时,您不会锁定在控制器内,更多选项
  3. 您可以将此安全实例注入任何其他对象,它将保留保护
  4. 包装它并忘记它..你可以假装它是原始对象,它会做出相同的反应

但是,此方法也存在一个主要问题 - 您无法本地检查安全对象实现和接口(也适用于查找现有方法)还是某些继承链的一部分。

第二部分/答案(对象的 RBAC)

在这种情况下,您应该认识到的主要区别在于,域对象(例如:)本身包含有关所有者的详细信息。这意味着,如果您要检查(以及在哪个级别)用户有权访问它,则需要您更改以下行:Profile

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

基本上,您有两种选择:

  • 向 ACL 提供相关对象。但你必须小心不要违反得墨忒耳定律

    $this->acl->isAllowed( get_class($this->target), $method )
    
  • 请求所有相关的详细信息,并仅向 ACL 提供其所需的内容,这也将使它对单元测试更加友好:

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

几个视频可以帮助您提出自己的实现:

附注

你似乎对MVC中的模型有相当普遍(而且完全错误)的理解。模型不是一个类。如果你有名为类或继承的东西,那么你做错了。FooBarModelAbstractModel

在适当的MVC中,模型是一个层,其中包含许多类。大部分类可以分为两组,基于职责:

- 域业务逻辑

(阅读更多这里这里):

这组类中的实例处理值的计算,检查不同的条件,实现销售规则并执行所有所谓的“业务逻辑”的其余部分。他们不知道数据是如何存储的,存储在哪里,甚至不知道存储是否首先存在。

域业务对象不依赖于数据库。创建发票时,数据来自何处并不重要。它可以来自SQL或远程REST API,甚至是MSWord文档的屏幕截图。业务逻辑不会更改。

- 数据访问和存储

从这组类创建的实例有时称为数据访问对象。通常是实现数据映射器模式的结构(不要与同名的ORM混淆..没有关系)。这是你的SQL语句的位置(或者可能是你的DomDocument,因为你把它存储在XML中)。

除了两个主要部分之外,还有一组实例/类,应该提到:

- 服务业

这就是您和第三方组件发挥作用的地方。例如,您可以将“身份验证”视为服务,它可以由您自己的或一些外部代码提供。此外,“邮件发送者”将是一种服务,它可能会将某些域对象与PHPMailer或SwiftMailer或您自己的邮件发送器组件编织在一起。

服务的另一个来源是在域和数据访问层上进行抽象。创建它们是为了简化控制器使用的代码。例如:创建新的用户帐户可能需要使用多个域对象映射器。但是,通过使用服务,控制器中只需要一两行。

在制作服务时,你必须记住的是,整个层应该是的。服务中没有业务逻辑。它们仅用于处理域对象、组件和映射器。

它们的一个共同点是,服务不会以任何直接的方式影响视图层,并且在某种程度上是自主的,以至于它们可以(并且经常退出)在MVC结构本身之外使用。此外,这种自我维持的结构使得迁移到不同的框架/架构变得更加容易,因为服务与应用程序的其余部分之间的耦合极低。


答案 2

前交叉韧带和控制器

首先:这些通常是不同的东西/层。当您批评示例性控制器代码时,它将两者放在一起 - 最明显的是太紧了。

tereško已经概述了一种如何将其与装饰器模式分离的方法。

我会先退后一步,寻找你面临的原始问题,然后讨论一下。

一方面,您希望控制器只执行命令它们的工作(命令或操作,我们称之为命令)。

另一方面,您希望能够将 ACL 放入应用程序中。如果我对你的问题理解正确的话,这些 ACL 的工作领域应该是控制对应用程序某些命令的访问。

因此,这种访问控制需要其他东西将这两者结合在一起。根据执行命令的上下文,ACL 开始生效,并且需要决定特定命令是否可以由特定主体(例如用户)执行。

让我们总结一下我们所拥有的:

  • 命令
  • 前交叉韧带
  • 用户

ACL组件是这里的核心:它至少需要知道一些关于命令的知识(以精确地识别命令),并且它需要能够识别用户。用户通常很容易通过唯一的ID来识别。但是在Web应用程序中,通常存在根本没有识别的用户,通常称为访客,匿名,每个人等。对于此示例,我们假设 ACL 可以使用用户对象并封装这些详细信息。用户对象绑定到应用程序请求对象,ACL 可以使用它。

如何识别命令?您对 MVC 模式的解释表明,命令是类名和方法名的复合体。如果我们更仔细地观察,甚至有命令的参数(参数)。因此,问问究竟是什么标识了命令是有效的?类名,方法名,参数的数量或名称,甚至是任何参数中的数据还是所有这些的混合?

根据在 ACL 中标识命令所需的详细程度,这可能会有很大差异。对于该示例,让我们简单地保留它,并指定命令由类名和方法名标识。

因此,这三个部分(ACL,命令和用户)如何相互归属的上下文现在更加清晰。

我们可以说,使用虚构的ACL组件,我们已经可以执行以下操作:

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

只需看看这里发生了什么:通过使命令和用户都可识别,ACL可以完成它的工作。ACL 的工作与用户对象和具体命令的工作无关。

只缺少一个部分,这不能活在空中。事实并非如此。因此,您需要找到访问控制需要启动的地方。让我们来看看在标准 Web 应用程序中发生了什么:

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

要找到那个地方,我们知道它必须在执行具体命令之前,所以我们可以减少该列表,只需要查看以下(潜在)地方:

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

在应用程序中的某个时刻,您知道特定用户已请求执行具体命令。您已经在此处执行了某种 ACL:如果用户请求的命令不存在,则不允许执行该命令。因此,在应用程序中发生这种情况的任何地方可能是添加“真实”ACL 检查的好地方:

该命令已被找到,我们可以创建它的标识,以便ACL可以处理它。如果用户不允许使用该命令,则不会执行该命令(操作)。也许 a 而不是对于案例,请求无法解析到具体命令上。CommandNotAllowedResponseCommandNotFoundResponse

将具体 HTTPRequest 的映射映射到命令的位置通常称为路由。由于路由已经具有查找命令的作业,为什么不扩展它以检查每个ACL是否确实允许该命令?例如,通过将 扩展到 ACL 感知路由器:。如果您的路由器还不知道 ,则不是正确的位置,因为要使 ACL 工作,不仅要命令,还要识别用户。所以这个地方可能会有所不同,但我相信你可以很容易地找到你需要扩展的地方,因为它是满足用户和命令要求的地方:RouterRouterACLUserRouter

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

用户从一开始就可用,命令优先,带有 。Request(Command)

因此,您不必将 ACL 检查放在每个命令的具体实现中,而是将其放在它之前。你不需要任何沉重的模式,魔法或其他任何东西,ACL做它的工作,用户做它的工作,特别是命令做它的工作:只是命令,没有别的。该命令没有兴趣知道角色是否适用于它,如果它在某个地方被保护。

因此,只需将不属于彼此的东西分开即可。对单一责任原则(SRP)稍作改写:更改命令应该只有一个原因 - 因为命令已更改。不是因为您现在在应用程序中引入了 ACL'ing。不是因为您切换了 User 对象。不是因为您从 HTTP/HTML 接口迁移到 SOAP 或命令行接口。

在您的案例中,ACL 控制对命令的访问,而不是命令本身。


推荐