第一个问题

请您解释一下如何在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!");
    ...   
  }

}
?>


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


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

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

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

第二个问题

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


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

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

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

谢谢。

评论

我什至不明白为什么您需要“访问控制列表”来进行用户交互。您是否只说类似if($ user-> hasFriend($ other_user)|| $ other_user-> profileIsPublic())$ other_user-> renderProfile()(否则,显示“您无权访问此用户的配置文件或类似的东西?我不明白。

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

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


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


容器可用于任何对象,而不仅仅是Controller的实例

检查授权发生在外部目标对象,即:


原始对象不负责访问控制,它会遵循SRP

,当您遇到“权限被拒绝”时,您未锁定在控制器内的更多选项


您可以将此受保护的实例注入任何其他对象中,它将保留保护
将其包裹起来而忘记..您可以假装它是原始对象,它会做出相同的反应。

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

第二部分/答案(对象的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中,模型是一层,其中包含许多类。根据职责,大部分类可以分为两组:

-域业务逻辑


(阅读更多:此处和此处):

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

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

-数据访问和存储



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

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

-服务



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

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

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

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

评论


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

– Stephane
2012年9月28日在2:49

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

–tereško
2012年9月28日,下午3:12

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

–tereško
2012年9月28日下午3:15

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

–tereško
2012年9月28日,下午3:17

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

– Stephane
2012-09-28的3:29

#2 楼

ACL和控制器

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

tereško已经概述了一种方法,可以使它与装饰器模式进一步脱钩。

我先回去寻找您面临的原始问题,然后再讨论一下。

一方面,您想让控制器来完成他们正在做的工作命令(命令或动作,我们称其为命令)。另一方面,您希望能够在应用程序中添加ACL。如果我理解您的问题的正确性,那么这些ACL的工作范围应该是控制对您的应用程序中某些命令的访问。

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

让我们总结一下至此,我们所拥有的是:


命令
ACL
用户

ACL组件在这里很重要:它需要了解至少与命令有关(以精确识别命令),并且它必须能够识别用户。通常,用户可以通过唯一的ID轻松识别。但是通常在Web应用程序中,根本没有被标识的用户,通常被称为来宾,匿名,所有人等。在本示例中,我们假定ACL可以使用用户对象并将这些详细信息封装起来。用户对象绑定到应用程序请求对象,并且ACL可以使用它。

识别命令呢?您对MVC模式的解释表明,命令由类名和方法名组成。如果我们仔细观察,甚至会有命令的参数(参数)。因此,问什么能准确识别命令是有效的?类名,方法名,参数的数目或名称,甚至任何参数内部的数据,还是所有这些参数的混合?

根据需要在哪一级的详细信息来识别命令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可以处理它。如果用户不允许使用该命令,则该命令将不会执行(动作)。在无法将请求解析到具体命令的情况下,可能是CommandNotAllowedResponse而不是CommandNotFoundResponse

将具体HTTPRequest的映射映射到命令的位置通常称为“路由”。由于路由已经可以找到命令,为什么不扩展它以检查每个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年9月28日在3:56



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

– hakre
2012年9月28日在8:42

#3 楼

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

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

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

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

评论


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

– Kirzilla
2010年8月7日,11:25

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

– Kirzilla
2010年8月7日,12:23

@Kirzilla我在Controllers上有同样的问题。似乎依赖项必须在某个地方。即使没有ACL,模型层又如何?如何防止它成为依赖项?

– Stephane
2012年9月28日下午4:00