psecio / propauth
基于属性的策略评估库
Requires
- php: >=5.5.0
Requires (Dev)
- phpunit/phpunit: 4.1.4
README
对认证凭证或用户权限集合进行评估有其局限性。使用这些工具,您只能进行类似于“是否有权限”或“凭证是否无效”的评估。PropAuth背后的目标是使这些评估更加灵活,并允许您定义可重用的策略,这些策略可以根据提供的用户动态评估。
嗨,Laravel用户,还有一个提供程序可以帮助您将PropAuth集成到应用程序和Blade模板中:[PropAuth-Provider](https://github.com/psecio/propauth-provider)
安装
您可以使用Composer轻松安装此库
composer.phar install psecio/propauth
示例
<?php require_once 'vendor/autoload.php'; use \Psecio\PropAuth\Enforcer; use \Psecio\PropAuth\Policy; $enforcer = new Enforcer(); $myUser = (object)[ 'username' => 'ccornutt', 'permissions' => ['test1'], 'password' => password_hash('test1234', PASSWORD_DEFAULT) ]; $myPolicy = new Policy(); $myPolicy->hasUsername('ccornutt'); $result = $enforcer->evaluate($myUser, $myPolicy); echo 'RESULT: '.var_export($result, true)."\n\n"; // result is true // You can also chain the evaluations to make more complex policies $myPolicy->hasUsername('ccornutt')->hasPermissions('test1'); // also true // There's also a static method to help make the creation more concise $myPolicy = Policy::instance()->hasUser('ccornutt')->hasPermissions('test1'); ?>
注意:所有匹配都视为AND,因此所有标准都必须为真,评估才能通过。
允许的用户类型
PropAuth引擎尝试以几种不同的方式从用户实例(属性)中获取信息,以适应大多数常见的用户类类型。在检查属性时,引擎将按照以下顺序进行
- 寻找具有给定名称的公共属性
- 寻找具有属性名称的getter(例如:对于“foo”,它寻找“getFoo”)
- 寻找特定的“getProperty”方法并使用属性名称调用它
在第一个代码示例中,我们只是创建了一个基本的类(stdClass
)并应用了公共属性,因此它会匹配到公共属性的第一次检查。
验证密码
您还可以使用PropAuth
验证密码,作为策略的一部分进行评估。以下是一个示例策略,它将验证上面定义的用户输入
<?php $myUser = (object)[ 'username' => 'ccornutt', 'password' => password_hash('test1234', PASSWORD_DEFAULT) ]; $gate = new Gateway($myUser); $subject = $gate->authenticate($password); if ($subject !== false && $subject->can('policy1') === true) { echo 'They can, woo!'; } ?>
密码验证假设使用密码散列方法,因此需要PHP >=5.5才能正确运行。明文密码提供给策略并在内部散列。然后,值与用户提供的值进行比较以检查匹配。在这种情况下,如果输入错误的用户名或密码,策略评估将失败。
如何检查属性
如果您注意到,我们在上面的Policy
上调用了一个hasUsername
方法,尽管它没有在User
类上定义。这是通过__call
魔术方法处理的。然后,它查找以下两个关键字之一:has
或not
。根据这个关键字确定需要执行哪种类型的检查。
- has:告诉系统执行“等于”匹配,比较给定值和属性值
- not:告诉系统执行“不等于”匹配
这使您可以定义基于用户具有和不具有的属性的定制策略,而不仅限于对象所需的子集。
规则(ANY
和ALL
)
您的检查可以在值之后有一个第二个参数,以进一步自定义执行的检查:Policy::ANY
和Policy::ALL
。根据属性中的数据和定义的数据,这些规则有不同的含义。以下是一个简要总结
注意:无论数据输入类型如何,
Policy::ANY
规则都是默认规则。所有匹配都作为完全相等进行处理,即使对于数组(如果值的顺序不同,它们不会匹配)。
按“路径”搜索
PropAuth 还允许您通过 "路径" 搜索要评估的数据。使用这种搜索,您可以递归地向下遍历对象和数组数据,找到您想要的,而无需事先收集它们。以下是一个示例
<?php class User { public $permissions = []; public $username = ['user1', 'user2']; } class Perm { public $name = ''; protected $value = ''; public $tests = []; public function __construct($name, $value) { $this->name = $name; $this->value = $value; } public function setTests(array $tests) { $this->tests = $tests; } public function getValue() { return $this->value; } } class Test { public $title; public $foo = []; public function __construct($title) { $this->title = $title; } public function setFoos(array $foo) { $this->foo = $foo; } } class Foo { public $test; public function __construct($test) { $this->test = $test; } } $policies = PolicySet::instance() ->add('test', Policy::instance()->find('permissions.tests.foo')->hasTest('t3')); ?>
在这种情况下,我们在 User
实例下有许多嵌套数据需要评估。然而,对于这个 test
策略,我们只想得到一件事情:来自 Foo
对象的 "test" 值。为了获取这些值,我们必须经过以下过程
- 在
User
实例上,获取permissions
属性值 - 对于这个集合中的每个条目,获取与其相关的
Test
实例 - 然后,对于这些测试中的每一个,我们只想得到一个集合中相关的
Foo
实例。
虽然这可以在 PropAuth 库外部完成,并直接传递给评估,但搜索 "路径" 处理使它更容易。要执行上述所有操作,只需使用上面示例中的搜索路径:permissions.tests.foo
。这会获取所有 Foo
实例,然后 hasTest
方法会查看 Foo
对象上的 test
值,以查看是否与 t3
值匹配。
其他示例
以下所有示例根据定义的用户都会评估为 true
。
<?php $policy = new Policy(); #### POSITIVE CHECKS // Check to see if the permission is in a set $user = new User([ 'permissions' => ['test1', 'test2'] ]); $policy->hasPermissions('test1'); // Check to see if any permissions match $policy->hasPermissions(['test3', 'test2', 'test5'], Policy::ANY); // Check to see that the permissions match exactly $policy->hasPermissions(['test1', 'test2'], Policy::ALL); #### NEGATIVE CHECKS // Check to see if the permission is NOT in the set $policy->notPermissions('test5'); // Check to see if NONE of the permissions match $policy->notPermissions(['test3', 'test5', 'test6'], Policy::ANY); // Check to see if the permissions are NOT equal $policy->notPermissions(['test4', 'test5'], Policy::ALL); ?>
使用回调
如果您有一些需要应用的更自定义的逻辑,您可能想使用 PropAuth 内置的回调处理。与属性检查中的 "has" 和 "not" 一样,对于回调有 "can"(结果应为 true)和 "cannot"(结果应为 false)。以下是一个示例
<?php // Make a user $myUser = (object)[ 'username' => 'ccornutt', 'permissions' => ['test1'] ]; // Make a post $post = (object)[ 'title' => 'This is a test post', 'id' => 1 ]; $myPolicy = new Policy(); $myPolicy ->hasUsername(['ccornutt', 'ccornutt1'], Policy::ANY) ->can(function($subject, $post) { return ($post->id === 1); }) ->cannot(function($subject, $post) { return (strpos($post->title, 'foobar') === false); }); $result = $enforcer->evaluate($myUser, $myPolicy, [ $post ]); // result is TRUE ?>
注意:传递给
evaluate
方法的额外参数将按照它们在数组中给出的顺序传递给闭包检查类型。然而,第一个参数将 始终 是被评估的主题(用户)。
策略集
您还可以通过键名(字符串)定义一个策略集。例如,如果我们想创建一个简单的策略,允许用户 "testuser1" 执行 "编辑帖子" 操作
<?php $set = new PolicySet(); $set->add( 'edit-post', Policy::instance()->hasUsername('testuser1') ); // Or, using the instance method $set = new PolicySet() ->add('edit-post', Policy::instance()->hasUsername('testuser1')); ?>
然后,当我们想要评估用户与这个策略时,我们可以使用在将集合注入 Enforcer
后的 allows
和 denies
方法。
<?php $myUser = (object)[ 'username' => 'testuser1', 'permissions' => ['test1'] ]; $enforcer = new Enforcer($set); if ($enforcer->allows('edit-post', $myUser) === true) { echo 'Hey, you can edit the post - rock on!'; } ?>
使用类与方法进行评估
您还可以使用类和方法进行评估,就像闭包一样,作为 can
和 cannot
检查的一部分。与前面的示例中传递闭包方法不同,您只需传递一个包含类和方法名称的字符串,这两个名称由双冒号(::
)分隔。例如
<?php $policy = Policy::instance()->can('\App\Foo::bar', [$post]); ?>
在这个例子中,您正在告诉它尝试创建一个 \App\Foo
类的实例,然后尝试在实例上调用 bar
方法。 注意:方法不需要是静态的,尽管它使用了双冒号。与闭包一样,主题将作为第一个参数传递。其他信息将作为其他参数随后传递。
因此,在我们的上述示例中,方法可能需要如下所示
<?php namespace App; class Foo { public function foo($subject, $post) { /* evaluation here, return boolean */ } } ?>
从外部源加载策略
在您的代码中定义策略是好的,但有时将它们放在外部位置(在需要时可以加载它们)更有意义。也许您有一种情况,其中只需要加载与 "Post" 相关的策略,而不需要加载整个站点的所有内容。 PropAuth
在 Policy
类上提供了一个 "load DSL" 方法,这可以帮助您。
什么是DSL?一个“特定领域语言”允许你定义一个特定格式的字符串,其中PropAuth
将理解如何解析它并从中创建一个策略实例。让我们从一个例子开始,然后分解其格式。
hasUsername:ccornutt||notUsername:ccornutt1||hasPermissions:(test1,test2)[ANY]
你会注意到,如果你自己创建策略,你会调用与DSL识别相似的方法名(例如hasUsername
)。这个DSL字符串定义了以下策略
- 主题拥有用户名为“ccornutt”
- 主题不拥有用户名为“ccornutt1”
- 主题拥有以下权限之一:test1、test2
逻辑与手动在策略对象上设置这些方法相同,只是更简化。下面是代码中的样子
<?php $dsl = 'hasUsername:ccornutt||notUsername:ccornutt1||hasPermissions:(test1,test2)[ANY]'; $myPolicy = Policy::load($dsl); ?>
很简单,对吧?好的,那么让我们看看格式
- 方法/值对由双竖线(
||
)分隔 - 方法名和值然后由冒号(
:
)分隔 - 单个字符串值直接放入字符串中,数组用括号包围并用逗号分隔
- 修饰符(如
ANY
或ALL
)添加到方法/值对末尾,用方括号([]
)括起来
显然,这仅适用于简单检查,其中标准可以由字符串和简单值定义,但它在多种情况下可能非常有用。当然,你总是可以将这些作为基本策略提取出来,然后在创建Policy
对象后手动添加。
使用网关接口进行评估
除了已经列出的强大功能外,PropAuth
库还提供了一个简化的接口来处理你的用户(主题)及其认证和授权。
首先,让我们看看认证。它使用后端的bcrypt
哈希方法来评估密码
<?php $myUser = (object)[ 'username' => 'ccornutt', 'password' => password_hash('test1234', PASSWORD_DEFAULT) ]; $gate = new Gateway($myUser); $subject = $gate->authenticate($password); // Then we can check if the user is authenticated echo 'Is authenticated? '.var_export($subject->isAuthed(), true)."\n"; ?>
我们创建Gateway
类实例,然后可以调用其上的authenticate
方法并传递密码。然后脚本假定它可以访问用户的password
属性作为对象上的值并执行比较。如果认证成功,将返回Subject
类的新实例,否则返回false
。Subject
类只是围绕你的对象(在本例中为$myUser
)的一个包装。可以使用Subject->getSubject()
方法检索原始对象。
此外,你可以提供更多上下文,并使用Gateway
接口进行策略评估。你只需在构造函数中将策略定义为Context
对象的一部分
<?php $context = new Context([ 'policies' => [ 'policy1' => Policy::instance()->hasUsername('ccornutt') ] ]); $gate = new Gateway($myUser, $context); // When we can call the "evaluate" method with the policies we want to check: if ($gate->evaluate('policy1') === true) { echo 'Policy1 passes!'; } // Or you can just add your own PolicySet instance and use "evaluate" the same way $myPolicySet = new PolicySet()->add('edit-post', Policy::instance()->hasUsername('testuser1')); $context = new Context([ 'policies' => $myPolicySet ]); ?>
一旦你有有效的Subject
实例,你就可以使用can
和cannot
方法检查其能力
<?php $myUser = (object)[ 'username' => 'ccornutt', 'password' => password_hash('test1234', PASSWORD_DEFAULT) ]; $context = new Context([ 'policies' => [ 'policy1' => Policy::instance()->hasUsername('ccornutt') ] ]); $gate = new Gateway($myUser, $context); $subject = $gate->authenticate($password); if ($subject !== false && $subject->can('policy1') === true) { echo 'They can, woo!'; } ?>
can
和cannot
方法的参数是你已经在上下文中定义的策略名。如果Policy
定义为一个具有更复杂逻辑的闭包,你可以将其(或多个)作为第二个参数提供
<?php $post = (object)[ 'author' => 'ccornutt' ]; $set = PolicySet::instance()->add( 'delete', Policy::instance()->can(function($subject, $post) { return ($subject->username == 'ccornutt' && $post->author == 'ccornutt'); }) ); $context = new Context(['policies' => $set]); $gate = new Gateway($myUser, $context); $subject = $gate->authenticate('test1234'); if ($subject->can('delete', $post) === true) { echo 'They can delete it!'; } ?>