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

Posted

技术标签:

【中文标题】如何在我的 Web MVC 应用程序中实现访问控制列表?【英文标题】:How can I implement an Access Control List in my Web MVC application? 【发布时间】:2011-03-26 16:52:11 【问题描述】:

第一个问题

请您解释一下如何在 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中,但我们仍然需要将所有子控制器的方法保持私有。

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

第二个问题

第二个问题是关于使用 Acl 获取角色。假设我们有客人、用户和用户的朋友。用户有权查看他的个人资料,只有朋友才能查看。所有客人都无法查看此用户的个人资料。所以,这是逻辑..

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

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

【问题讨论】:

我什至不明白为什么您需要“访问控制列表”来进行用户交互。你不会直接说if($user-&gt;hasFriend($other_user) || $other_user-&gt;profileIsPublic()) $other_user-&gt;renderProfile() 之类的话(否则,显示“您无权访问此用户的个人资料”或类似的话?我不明白。 可能是因为 Kirzilla 希望在一个地方管理所有访问条件 - 主要是在配置中。因此,任何权限更改都可以在 Admin 中进行,而不是更改代码。 【参考方案1】:

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

以我的拙见,解决此问题的最佳方法是使用decorator pattern,基本上,这意味着您将对象放在内部另一个对象中,其行为类似于一个保护壳。这不需要您扩展原始类。这是一个例子:

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 提供相关对象。但是你要注意不要违反Law of 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 )

几个视频可能会帮助您提出自己的实施方案:

Inheritance, Polymorphism, & Testing Don't Look For Things!

附注

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

在正确的 MVC 中,模型是一个层,其中包含很多类。根据职责,大部分类可以分为两组:

- 领域业务逻辑

了解更多:here 和 here):

这组类中的实例处理值的计算、检查不同的条件、实施销售规则并完成所有其他您称之为“业务逻辑”的工作。他们不知道数据是如何存储的,存储在哪里,甚至不知道存储是否存在。

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

- 数据访问和存储

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

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

- 服务

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

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

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

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

【讨论】:

重读这篇文章,我在 5 分钟内学到的东西比我几个月学到的还要多。您是否同意:瘦控制器分派给收集视图数据的服务?另外,如果您直接接受问题,请给我发消息。 我部分同意。当您初始化 Request 实例(或它的一些类似物)时,视图中的数据收集发生在 MVC 三元组之外。控制器仅从Request 实例中提取数据,并将其中的大部分传递给适当的服务(其中一些也用于查看)。服务执行您命令它们执行的操作。然后,当视图生成响应时,它会向服务请求数据,并根据该信息生成响应。所述响应可以是由多个模板生成的 html,也可以只是一个 HTTP 位置标头。取决于控制器设置的状态。 使用简化的解释:控制器“写入”模型和视图,视图“读取”模型。模型层是所有受 MVC 启发的 Web 相关模式中的被动结构。 @Stephane ,至于直接提问,你可以随时在推特上给我发消息。还是你的问题有点“长篇”,不能塞进 140 个字符? 从模型中读取:这是否意味着模型的某些积极作用?我以前从未听说过。如果您愿意,我可以随时通过 Twitter 向您发送链接。正如你所看到的,这些回复很快就变成了对话,我试图尊重这个网站和你的 Twitter 关注者。【参考方案2】:

ACL 和控制器

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

tereško already outlined 一种可以将其与装饰器模式进一步解耦的方法。

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

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

另一方面,您希望能够将 ACL 放入您的应用程序中。如果我正确理解您的问题,这些 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 可以处理它。如果用户不允许该命令,则该命令将不会被执行(动作)。对于无法将请求解析为具体命令的情况,可能是 CommandNotAllowedResponse 而不是 CommandNotFoundResponse

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

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

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

因此,不是将 ACL 检查放在 each 命令的具体实现中,而是将其放在它之前。您不需要任何繁重的模式、魔法或其他任何东西,ACL 完成了它的工作,用户完成了它的工作,尤其是命令完成了它的工作:只是命令,没有别的。该命令没有兴趣知道角色是否适用于它,是否在某个地方受到保护。

所以把不属于彼此的东西分开。稍微改写一下单一职责原则 (SRP):更改命令应该只有一个原因 - 因为命令已更改。不是因为您现在在应用程序中引入了 ACL。不是因为您切换了 User 对象。不是因为您从 HTTP/HTML 界面迁移到 SOAP 或命令行界面。

在您的情况下,ACL 控制对命令的访问,而不是命令本身。

【讨论】:

两个问题:CommandNotFoundResponse 和 CommandNotAllowedResponse:您会将这些从 ACL 类传递到路由器或控制器并期望得到通用响应吗? 2:如果你想包含方法+属性,你会怎么处理? 1:响应是响应,这里不是来自ACL而是来自路由器,ACL帮助路由器找出响应类型(未找到,特别是:禁止)。 2:视情况而定。如果您的意思是将属性作为操作的参数,并且您需要带参数的 ACL,请将它们放在 ACL 下。【参考方案3】:

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

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

编辑:是否需要访问数据库、LDAP 服务器等与问题无关。我的观点是,您可以基于 URL 而不是控制器方法实现授权。这些更健壮,因为您通常不会更改 URL(URL 区域类型的公共接口),但您不妨更改控制器的实现。

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

【讨论】:

请您更新您的答案并添加有关 Dispatcher 的更多详细信息。我有调度程序 - 它检测我应该通过 URL 调用的控制器方法。但我不明白如何在 Dispatcher 中获得角色(我需要访问数据库才能做到这一点)。希望很快能听到你的声音。 啊哈,明白了。我应该决定是否允许执行而不访问方法!竖起大拇指!最后一个未解决的问题 - 如何从 Acl 访问模型。有什么想法吗? @Kirzilla 我对控制器有同样的问题。似乎依赖项必须存在于某个地方。就算没有 ACL,那模型层呢?如何防止它成为依赖项?

以上是关于如何在我的 Web MVC 应用程序中实现访问控制列表?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Web 表单中实现 AngularJS 控制器?

如何在 ASP.NET MVC 5 和 WEB API 2 中实现 oauth2 服务器 [关闭]

在 ASP.NET MVC 中实现“记住我”功能

如何在我的 ASP.NET MVC 应用程序中实现“全选”复选框?

在我的 Spring MVC 应用程序中实现 Spring Actuator 而不添加 Spring Boot

如何在我的 Web 应用程序中实现 REST。我想为我的网站制作一个休息 API?