如何在Web MVC应用程序中实现访问控制列表?


96

第一个问题

请,请您说明一下如何在MVC中实现最简单的ACL。

这是在Controller中使用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代码添加到每个控制器的方法中,但是我们不需要任何其他依赖项!

下一种方法是制作所有控制器的方法private,并将ACL代码添加到控制器的__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代码放入父Controller,但是我们仍然需要将所有子Controller的方法保持私有。

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

第二个问题

第二个问题是关于使用Acl的角色。假设我们有客人,用户和用户的朋友。用户具有查看其个人资料的访问权限,只有朋友可以查看。所有访客都无法查看该用户的个人资料。所以,这是逻辑。

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

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

我希望我的想法是明确的。对不起我的英语不好。

谢谢。


1
我什至不明白为什么您需要“访问控制列表”来进行用户交互。您是否只说一句if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(否则,显示“您无权访问此用户的个人资料”或类似的话?我听不懂。)
Buttle Butkus 2014年

2
可能是因为Kirzilla希望在一个地方管理所有访问条件-主要是在配置中。因此,可以在Admin中进行权限的任何更改,而无需更改代码。
Mariyo

Answers:


185

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

我个人认为,最好的方式来处理,这将是使用Decorator模式,基本上,这意味着,你把你的对象,并把它里面的另一个对象,它会像一个保护壳。这将不需要您扩展原始类。这是一个例子:

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提供相关对象。但您必须注意不要违反Demeter法则

    $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中的模型有相当普遍的理解(而且完全是错误的)。模式不是一类。如果您有命名的类FooBarModel或继承的东西AbstractModel那么您做错了。

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

- 领域业务逻辑

阅读更多这里这里):

这组类中的实例处理值的计算,检查不同的条件,实施销售规则并执行其余所有您称为“业务逻辑”的工作。他们不知道数据的存储方式,存储位置或存储位置。

域业务对象不依赖于数据库。创建发票时,数据来自哪里都没有关系。它可以来自SQL或来自远程REST API,甚至可以来自MSWord文档的屏幕截图。业务逻辑没有改变。

- 数据访问和存储

由这组类制成的实例有时称为数据访问对象。通常实现Data Mapper模式的结构(不要与同名..无关联的ORM混淆)。这就是您的SQL语句所在的位置(或者您的DomDocument,因为您将其存储在XML中)。

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

- 服务

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

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

进行服务时,您必须记住的是整个层应该很薄。服务中没有业务逻辑。它们只是在杂耍领域对象,组件和映射器。

它们共有的一件事是,服务不会以任何直接方式影响View层,并且具有一定程度的自治性,因此可以在MVC结构本身之外(并且经常退出)使用它们。而且,由于服务和应用程序其余部分之间的耦合极低,因此这种自我维持的结构使向其他框架/体系结构的迁移更加容易。


34
重新阅读此书仅花了5分钟,而几个月后,我学到了更多。您是否同意:将精简控制器调度到收集视图数据的服务?另外,如果您直接接受问题,请给我发消息。
Stephane 2012年

2
我部分同意。初始化Request实例(或类似实例)时,从视图收集数据发生在MVC triad外部。控制器仅从Request实例中提取数据,并将大多数数据传递给适当的服务(其中一些也可以查看)。服务执行您命令它们执行的操作。然后,当视图正在生成响应时,它向服务请求数据,并基于该信息生成响应。所述响应可以是由多个模板制成的HTML,也可以是HTTP位置标头。取决于控制器设置的状态。
tereško

4
使用简化的解释:控制器“写入”以进行建模和查看,查看模型中的“读取”。模型层是受MVC启发的所有Web相关模式中的被动结构。
tereško

@Stephane,直​​接问问题,您可以随时在Twitter上给我发消息。还是您质疑一种“长格式”,不能用140个字符填充?
tereško

从模型中读取:这是否意味着该模型发挥了积极作用?我从来没有听说过。如果您愿意,我总是可以通过Twitter发送链接。如您所见,这些回复很快就变成了对话,我试图尊重这个站点和您的Twitter追随者。
Stephane 2012年

16

ACL和控制器

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

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可以处理它。如果用户不允许使用该命令,则该命令将不会执行(动作)。CommandNotAllowedResponseCommandNotFoundResponse无法将请求解析为具体命令的情况下,也许用a 代替。

具体的HTTPRequest映射到命令的映射位置通常称为Routing。由于路由已经可以找到命令,为什么不扩展它以检查每个ACL是否实际允许该命令?例如,将扩展Router 到支持ACL的路由器:RouterACL。如果您的路由器尚不知道User,则Router位置不正确,因为要使ACL正常工作,不仅必须识别命令,还必须识别用户。因此,这个地方可能会有所不同,但是我敢肯定,您可以轻松找到需要扩展的地方,因为这是满足用户和命令要求的地方:

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

从一开始就可以使用用户,首先使用 Request(Command)

因此,与其将您的ACL检查放入每个命令的具体实现中,而是将其放在它的前面。您不需要任何繁琐的模式,魔术或任何其他内容,ACL可以完成任务,用户可以完成任务,特别是命令可以完成任务:仅是命令,仅此而已。该命令没有兴趣知道角色是否适用于它,无论它是否受到保护。

因此,将不属于彼此的事物分开。请对“ 单一责任原则”(SRP)稍加改写:更改命令应该只有一个原因-因为该命令已更改。不是因为您现在在应用程序中引入了ACL。不是因为您切换了User对象。不是因为您从HTTP / HTML界面迁移到SOAP或命令行界面。

您所用的ACL控制对命令的访问,而不是命令本身。


两个问题:CommandNotFoundResponse和CommandNotAllowedResponse:您是否将这些内容从ACL类传递到路由器或控制器,并希望获得通用响应?2:如果要包括方法+属性,您将如何处理?
Stephane 2012年

1:响应是响应,此处不是来自ACL,而是来自路由器,ACL帮助路由器找出响应类型(未找到,尤其是:禁止)。2:视情况而定。如果您将属性作为操作中的参数,并且需要对参数进行ACL,请将其放在ACL下。
hakre 2012年

13

一种可能是将所有控制器包装在扩展Controller的另一个类中,并在检查授权后让所有函数调用委派给包装的实例。

您还可以在调度程序的上游(如果您的应用程序确实有一个)执行此操作,并根据URL(而不是控制方法)查找权限。

编辑:是否需要访问数据库,LDAP服务器等与问题正交。我的观点是,您可以基于URL而非控制器方法来实现授权。这些功能更强大,因为您通常不会更改URL(URLs区域的公共接口),但是您也可以更改控制器的实现。

通常,您有一个或几个配置文件,在其中将特定的URL模式映射到特定的身份验证方法和授权指令。在将请求分派到控制器之前,分派器确定用户是否已被授权,如果未授权则中止分派。


请,您能否更新您的答案并添加有关Dispatcher的更多详细信息。我有调度程序-它检测我应该通过URL调用哪种控制器的方法。但是我不明白如何在Dispatcher中获得角色(我需要访问DB才能做到)。希望很快能听到你的声音。
Kirzilla

啊哈,明白了。我应该决定是否允许执行而不访问方法!竖起大拇指!最后一个未解决的问题-如何从Acl访问模型。有任何想法吗?
Kirzilla

@Kirzilla我在Controllers上有同样的问题。似乎依赖项必须在某个地方。即使没有ACL,模型层又如何?如何防止它成为依赖项?
Stephane 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.