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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3430181/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-25 09:43:20  来源:igfitidea点击:

How can I implement an Access Control List in my Web MVC application?

phpoopmodel-view-controlleracl

提问by Kirzilla

First question

第一个问题

Please, could you explain me how simpliest ACL could be implemented in MVC.

请您解释一下如何在 MVC 中实现最简单的 ACL。

Here is the first approach of using Acl in 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!");
    ...    
  }

}
?>

It is very bad approach, and it's minus is that we have to add Acl piece of code into each controller's method, but we don't need any additional dependencies!

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

Next approach is to make all controller's methods privateand add ACL code into controller's __callmethod.

下一个方法是制作所有控制器的方法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!");
    ...   
  }

}
?>

It is better than previous code, but main minuses are...

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

  • All controller's methods should be private
  • We have to add ACL code into each controller's __call method.
  • 所有控制器的方法都应该是私有的
  • 我们必须将 ACL 代码添加到每个控制器的 __call 方法中。

The next approach is to put Acl code into parent Controller, but we still need to keep all child controller's methods private.

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

What is the solution? And what is the best practice? Where should I call Acl functions to decide allow or disallow method to be executed.

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

Second question

第二个问题

Second question is about getting role using Acl. Let's imagine that we have guests, users and user's friends. User have restricted access to viewing his profile that only friends can view it. All guests can't view this user's profile. So, here is the logic..

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

  • we have to ensure that method being called is profile
  • we have to detect owner of this profile
  • we have to detect is viewer is owner of this profile or no
  • we have to read restriction rules about this profile
  • we have to decide execute or not execute profile method
  • 我们必须确保被调用的方法是 profile
  • 我们必须检测此配置文件的所有者
  • 我们必须检测查看者是否是此个人资料的所有者
  • 我们必须阅读有关此配置文件的限制规则
  • 我们必须决定执行或不执行配置文件方法

The main question is about detecting owner of profile. We can detect who is owner of profile only executing model's method $model->getOwner(), but Acl do not have access to model. How can we implement this?

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

I hope that my thoughts are clear. Sorry for my English.

我希望我的想法是清楚的。对不起我的英语不好。

Thank you.

谢谢你。

回答by tere?ko

First part/answer (ACL implementation)

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

In my humble opinion, the best way to approach this would be to use decorator pattern, Basically, this means that you take your object, and place it insideanother object, which will act like a protective shell. This would NOT require you to extend the original class. Here is an example:

以我的拙见,解决这个问题的最好方法是使用装饰器模式,基本上,这意味着您将对象放在另一个对象中,该对象将起到保护壳的作用。这不需要您扩展原始类。下面是一个例子:

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

}

And this would be how you use this sort of structure:

这将是您使用这种结构的方式:

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

As you might notice, this solution has several advantages:

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

  1. containment can be used on any object, not just instances of Controller
  2. check for authorization happens outside the target object, which means that:
    • original object is not responsible for access control, adheres to SRP
    • when you get "permission denied", you are not locked inside a controller, more options
  3. you can inject this secured instancein any other object, it will retain the protection
  4. wrap it & forget it .. you can pretendthat it is the original object, it will react the same
  1. 遏制可用于任何对象,而不仅仅是实例 Controller
  2. 检查授权发生在目标对象之外,这意味着:
    • 原始对象不负责访问控制,坚持SRP
    • 当您收到“权限被拒绝”时,您不会被锁定在控制器内,更多选项
  3. 您可以将此安全实例注入任何其他对象,它将保留保护
  4. 包装它并忘记它..你可以假装它是原始对象,它会做出相同的反应

But, there are one major issue with this method too - you cannot natively check if secured object implements and interface ( which also applies for looking up existing methods ) or is part of some inheritance chain.

但是,此方法也存在一个主要问题——您无法在本机检查受保护对象是否实现和接口(这也适用于查找现有方法)或是否属于某些继承链。

Second part/answer (RBAC for objects)

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

In this case the main difference you should recognize is that you Domain Objects(in example: Profile) itself contains details about owner. This means, that for you to check, if (and at which level) user has access to it, it will require you to change this line:

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

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

Essentially you have two options:

基本上你有两个选择:

  • Provide the ACL with the object in question. But you have to be careful not to violate Law of Demeter:

    $this->acl->isAllowed( get_class($this->target), $method )
    
  • Request all the relevant details and provide the ACL only with what it needs, which will also make it a bit more unit-testing friendly:

    $command = array( get_class($this->target), $method );
    /* -- snip -- */
    $this->acl->isAllowed( $this->target->getPermissions(), $command )
    
  • 提供具有相关对象的 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 )
    

Couple videos that might help you to come up with your own implementation:

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

Side notes

旁注

You seem to have the quite common ( and completely wrong ) understanding of what Model in MVC is. Model is not a class. If you have class named FooBarModelor something that inherits AbstractModelthen you are doing it wrong.

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

In proper MVC the Model is a layer, which contains a lot of classes. Large part of the classes can be separated in two groups , based on the responsibility:

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

- Domain Business Logic

-域业务逻辑

(read more: hereand here):

阅读更多这里这里):

Instances from this group of classes deal with computation of values, check for different conditions, implement sales rules and do all the rest what you would call "business logic". They have no clue how data is stored, where it is stored or even if storage exists in first place.

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

Domain Business object do not depend on database. When you are creating an invoice, it does not matter where data comes from. It can be either from SQL or from a remote REST API, or even screenshot of a MSWord document. The business logic does no change.

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

- Data Access and Storage

-数据访问和存储

Instances made from this group of classes are sometimes called Data Access Objects. Usually structures that implement Data Mapperpattern ( do not confuse with ORMs of same name .. no relation ). This is where your SQL statements would be (or maybe your DomDocument, because you store it in XML).

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

Beside the two major parts, there is one more group of instances/classes, that should be mentioned:

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

- Services

-服务

This is where your and 3rd party components come in play. For example, you can think of "authentication" as service, which can be provided by your own, or some external code. Also "mail sender" would be a service, which might knit together some domain object with a PHPMailer or SwiftMailer, or your own mail-sender component.

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

Another source of servicesare abstraction on to on domain and data access layers. They are created to simplify the code used by controllers. For example: creating new user account might require to work with several domain objectsand mappers. But, by using a service, it will need only one or two lines in the controller.

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

What you have to remember when making services, is that the whole layer is supposed to be thin. There is no business logic in services. They are only there to juggle domain object, components and mappers.

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

One of things they all have in common would be that services do not affect the View layer in any direct way, and are autonomous to such an extent, that they can be ( and quit often - are ) used outside the MVC structure itself. Also such self-sustained structures make the migration to a different framework/architecture much easier, because of extremely low coupling between service and the rest of application.

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

回答by hakre

ACL and Controllers

ACL 和控制器

First of all: These are different things / layers most often. As you criticize the exemplary controller code, it puts both together - most obviously too tight.

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

tere?ko already outlineda way how you could decouple this more with the decorator pattern.

tere?ko 已经概述了如何将其与装饰器模式更多地解耦。

I'd go one step back first to look for the original problem you're facing and discuss that a bit then.

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

On the one hand you want to have controllers that just do the work they're commanded to (command or action, let's call it command).

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

On the other hand you want to be able to put ACL in your application. The field of work of these ACLs should be - if I understood your question right - to control access to certain commands of your applications.

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

This kind of access control therefore needs something else that brings these two together. Based on the context in which a command is executed in, ACL kicks in and decisions need to be done whether or not a specific command can be executed by a specific subject (e.g. the user).

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

Let's summarize to this point what we have:

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

  • Command
  • ACL
  • User
  • 命令
  • 访问控制列表
  • 用户

The ACL component is central here: It needs to know at least something about the command (to identify the command to be precise) and it needs to be able to identify the user. Users are normally easily identified by a unique ID. But often in webapplications there are users that are not identified at all, often called guest, anonymous, everybody etc.. For this example we assume that the ACL can consume a user object and encapsulate these details away. The user object is bound to the application request object and the ACL can consume it.

ACL 组件在这里是核心:它至少需要知道一些关于命令的信息(以准确识别命令)并且需要能够识别用户。用户通常可以通过唯一 ID 轻松识别。但是在 web 应用程序中经常有一些根本没有被识别的用户,通常称为来宾、匿名、所有人等。对于这个例子,我们假设 ACL 可以使用用户对象并将这些细节封装起来。用户对象绑定到应用程序请求对象,ACL 可以使用它。

What about identifying a command? Your interpretation of the MVC pattern suggests that a command is compound of a classname and a method name. If we look more closely there are even arguments (parameters) for a command. So it's valid to ask what exactly identifies a command? The classname, the methodname, the number or names of arguments, even the data inside any of the arguments or a mixture of all this?

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

Depending on which level of detail you need to identify a command in your ACL'ing, this can vary a lot. For the example let's keep it simply and specify that a command is identified by the classname and method name.

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

So the context of how these three parts (ACL, Command and User) are belonging to each other is now more clear.

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

We could say, with an imaginary ACL compontent we can already do the following:

我们可以说,有了一个虚构的 ACL 组件,我们已经可以做到以下几点:

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

Just see what is happeninig here: By making both the command and the user identifiable, the ACL can do it's work. The job of the ACL is unrelated to the work of both the user object and the concrete command.

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

There is only one part missing, this can't live in the air. And it doesn't. So you need to locate the place where the access control needs to kick in. Let's take a look what happens in a standard webapplication:

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

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

To locate that place, we know it must be before the concrete command is executed, so we can reduce that list and only need to look into the following (potential) places:

要定位那个位置,我们知道它必须在执行具体命令之前,因此我们可以减少该列表,只需要查看以下(潜在)位置:

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

At some point in your application you know that a specific user has requested to perform a concrete command. You already do some sort of ACL'ing here: If a user requests a command which does not exists, you don't allow that command to execute. So where-ever that happens in your application might be a good place to add the "real" ACL checks:

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

The command has been located and we can create the identification of it so the ACL can deal with it. In case the command is not allowed for a user, the command will not be executed (action). Maybe a CommandNotAllowedResponseinstead of the CommandNotFoundResponsefor the case a request could not be resolved onto a concrete command.

该命令已被定位,我们可以创建它的标识,以便 ACL 可以处理它。如果用户不允许使用该命令,则不会执行该命令(操作)。也许 aCommandNotAllowedResponse而不是CommandNotFoundResponse对于请求无法解析为具体命令的情况。

The place where the mapping of a concrete HTTPRequest is mapped onto a command is often called Routing. As the Routingalready has the job to locate a command, why not extend it to check if the command is actually allowed per ACL? E.g. by extending the Routerto a ACL aware router: RouterACL. If your router does not yet know the User, then the Routeris not the right place, because for the ACL'ing to work not only the command but also the user must be identified. So this place can vary, but I'm sure you can easily locate the place you need to extend, because it's the place that fullfills the user and command requirement:

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

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

User is available since the beginning, Command first with Request(Command).

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

So instead of putting your ACL checks inside eachcommand's concrete implementation, you place it before it. You don't need any heavy patterns, magic or whatever, the ACL does it's job, the user does it's job and especially the command does it's job: Just the command, nothing else. The command has no interest to know whether or not roles apply to it, if it's guarded somewhere or not.

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

So just keep things apart that don't belong to each other. Use a slightly rewording of the Single Responsibility Principle (SRP): There should be only one reason to change a command - because the command has changed. Not because you now introduce ACL'ing in your application. Not because you switch the User object. Not because you migrate from an HTTP/HTML interface to a SOAP or command-line interface.

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

The ACL in your case controls the access to a command, not the command itself.

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

回答by Artefacto

One possibility is to wrap all your controllers in another class that extends Controller and have it delegate all the function calls to the wrapped instance after checking for authorization.

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

You could also do it more upstream, in the dispatcher (if your application does indeed have one) and lookup the permissions based on the URLs, instead of control methods.

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

edit: Whether you need to access a database, a LDAP server, etc. is orthogonal to the question. My point was that you could implement an authorization based on URLs instead of controller methods. These is more robust because you typically won't be changing your URLs (URLs area kind of public interface), but you might as well change the implementations of your controllers.

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

Typically, you have one or several configuration files where you map specific URL patterns to specific authentication methods and authorization directives. The dispatcher, before dispatching the request to the controllers, determines if the user is authorized and aborts the dispatching if he's not.

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