使用中间件实现访问控制

顾名思义,中间件位于函数或方法调用序列的中间。相应地,中间件很适合做 "守门人 "的任务。你可以很容易地用中间件类实现访问控制列表(ACL)机制,中间件类读取ACL,并允许或拒绝对序列中下一个函数或方法调用的访问。

如何做...

1.这个过程中最困难的部分可能是确定哪些因素要包括在ACL中。为了说明问题,我们假设我们的用户都被分配了一个 level 和一个 status。在这个例子中,级别的定义如下。

  'levels' => [0, 'BEG', 'INT', 'ADV']

2. 状态可以表明他们在会员注册过程中的进展情况。例如,状态为0表示他们已经启动了会员注册程序,但尚未得到确认。状态为1时,表示他们的电子邮件地址已经确认,但他们还没有支付月费,以此类推。

3. 接下来,我们需要定义我们计划控制的资源。在本例中,我们将假设需要控制对网站上一系列网页的访问。据此,我们需要定义一个此类资源的数组。在ACL中,我们就可以参考键。

'pages'  => [0 => 'sorry', 'logout' => 'logout', 'login'  => 'auth',
             1 => 'page1', 2 => 'page2', 3 => 'page3',
             4 => 'page4', 5 => 'page5', 6 => 'page6',
             7 => 'page7', 8 => 'page8', 9 => 'page9']

4. 最后,最重要的一项配置是根据级别和状态对页面进行分配。配置数组中使用的通用模板可能是这样的。

status => ['inherits' => <key>, 'pages' => [level => [pages allowed], etc.]]

5. 现在我们可以定义Acl类了。和之前一样,我们使用一些类,并定义适合访问控制的常量和属性。

namespace Application\Acl;

use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Application\MiddleWare\ { Constants, Response, TextStream };

class Acl
{
  const DEFAULT_STATUS = '';
  const DEFAULT_LEVEL  = 0;
  const DEFAULT_PAGE   = 0;
  const ERROR_ACL = 'ERROR: authorization error';
  const ERROR_APP = 'ERROR: requested page not listed';
  const ERROR_DEF = 
    'ERROR: must assign keys "levels", "pages" and "allowed"';
  protected $default;
  protected $levels;
  protected $pages;
  protected $allowed; 

6. 在 __construct() 方法中,我们将分配数组分解为 $pages(要控制的资源)、$levels$allowed(实际的分配)。如果数组不包括这三个子组件中的一个,就会抛出一个异常。

7. 您可能已经注意到,我们允许继承。在$allowed中,继承的键可以设置为数组中的另一个键。如果是这样,我们需要将它的值与当前正在检查的值合并。我们反向迭代$allowed,每次通过循环合并任何继承的值。顺便说一下,这个方法也只是隔离适用于某个状态和级别的规则。

8. 在处理授权时,我们会初始化一些变量,然后从原始请求URI中提取所请求的页面。如果页面参数不存在,我们设置一个400代码。

9. 否则,我们对请求体内容进行解码,并获得状态和级别。然后我们就可以调用mergeInherited(),它返回一个可以访问这个状态和级别的页面数组。

10. 如果请求的页面在$allowed数组中,我们就将状态码设置为200,并将授权设置连同请求的页面代码对应的网页一起返回。

11. 然后我们返回JSON编码的响应,我们就完成了。

如何运行...

之后,你将需要定义ApplicationA\cl\Acl,这将在本文中讨论。现在移动到/path/to/source/for/this/chapter文件夹,创建两个目录:publicpages。在pages中,创建一系列PHP文件,如page1.phppage2.php等。下面是这些页面中的一个例子。

你也可以定义一个 menu.php页面,它可以被包含在输出中。

logout.php页面应该销毁会话。

auth.php页面将显示一个登录界面(如前所述)。

然后可以创建一个配置文件,允许根据级别和状态来访问网页。为了便于说明,调用chap_09_middleware_acl_config.php,并返回一个数组,看起来像这样。

最后,在公共文件夹中,定义index.php,它设置了自动加载,并最终调用AuthenticateAcl类。就像其他示例一样,定义配置文件,设置自动加载,并使用某些类。另外,别忘了启动会话。

最佳实践

最好的做法是保护你的会话。一个简单的方法是使用 session_regenerate_id(),它可以使现有的 PHP 会话标识符无效并生成一个新的标识符。因此,如果攻击者通过非法手段获得会话标识符,任何给定的会话标识符的有效时间窗口都会被保持在最小范围内。

现在你可以调出ACL配置,并为Authenticate以及Acl创建实例。

接下来,定义入站和出站请求实例。

如果传入的请求方法是post,则调用login()方法处理认证。

如果为认证定义的会话密钥被填充,就意味着用户已经成功认证。如果没有,我们编写一个匿名函数,后面会调用,其中包括认证登录页面。

否则,你可以继续进行ACL检查。首先需要从原始查询中找到用户想要访问的网页。

然后,您可以重新编写 $outbound 请求,以包含这些信息。

接下来,你就可以检查授权了,提供出站请求作为参数。

然后可以检查返回响应的授权参数,并编程匿名函数,如果OK则包含返回页参数,否则包含抱歉页。

现在你需要做的就是设置表单动作,并将匿名函数包装在HTML中。

为了测试它,你可以使用内置的PHP web服务器,但你需要使用-t标志来表明文档根是公开的。

从浏览器中,您可以访问http://localhost:8080/

如果试图访问任何页面,您会被重定向回登录页面。根据配置,status=1,level=BEG的用户只能访问第1页并注销。如果你以该用户身份登录时,试图访问第2页,这里是输出结果。

更多...

这个例子依靠$_SESSION作为用户登录后唯一的认证手段。关于如何保护PHP会话的好例子,请参见第12章,提高网站安全,特别是题为保护PHP会话的示例。

最后更新于