deefour / authorizer
通过PHP类实现简单授权
Requires
- php: >=5.6.0
- deefour/transformer: ^1.2
Requires (Dev)
- friendsofphp/php-cs-fixer: 2.1.0
- phpspec/phpspec: ^3.1
README
通过PHP类实现简单授权。受elabs/pundit启发。
入门指南
运行以下命令将Authorizer添加到项目的composer.json
中。有关具体版本,请参阅Packagist。
composer require deefour/authorizer
需要>=PHP5.6.0
。
策略
Authorizer的核心是策略类概念。策略在接受实例化时接受一个$user
和一个$record
。公共方法(操作)包含检查$user
是否可以在$record
上执行操作的逻辑。以下是一个授权用户创建和编辑文章对象的策略示例。
class ArticlePolicy { protected $user; protected $record; public function __construct($user, $record) { $this->user = $user; $this->record = $record; } public function create() { return $this->user->exists; } public function edit() { return $this->record->exists && $this->record->author->is($user); } }
此策略允许任何现有用户创建新文章,并且只有文章的作者可以修改现有文章。以下是如何直接与该策略交互的示例。
(new ArticlePolicy($user, new Article))->create(); // => true (new ArticlePolicy($user, Article::class))->create(); // => true (new ArticlePolicy($user, new Article))->edit(); // => false (new ArticlePolicy($user, $user->articles->first()))->edit(); // => true
质量属性保护
策略上的permittedAttributes
方法为用户执行操作时的请求提供属性的白名单。
class ArticlePolicy { public function permittedAttributes() { $attributes = [ 'title', 'body', ]; // prevent the author and slug from being modified after the article // has been persisted to the database. if ( ! $this->record->exists) { return array_merge($attributes, [ 'user_id', 'slug', ]); } return $attributes; } }
还可以提供格式为permittedAttributesFor{Action}
的操作特定方法。
class ArticlePolicy { public function permittedAttributesForCreate() { return [ 'title', 'body', 'user_id', 'slug ]; } public functoin permittedAttributesForEdit() { return [ 'title', 'body' ]; } }
作用域
Authorizer还提供了通过作用域支持根据用户的权限检索受限制的结果集。作用域对象在接受实例化时接收一个$user
和一个基本$scope
。它应实现一个具有逻辑来细化$scope
并通常返回当前用户能够访问的对象的可迭代集合的resolve()
方法。例如
class ArticleScope { protected $user; protected $scope; public __construct($user, $scope) { $this->user = $user; $this->scope = $scope; } public function resolve() { if ($this->user->isAdmin()) { return $this->scope->all(); } return $this->scope->where('published', true)->get(); } }
此作用域在当前用户是管理员时检索所有文章,而对于其他用户则只检索已发布的文章。
$user = User::first(); $query = Article::newQuery(); (new ArticleScope($user, $query))->resolve(); //=> iterable list of Article objects
Authorizer对象
直接创建和使用策略和作用域类是可以的,但还有更简单的方式来授权用户活动。第一种是Deefour\Authorizer\Authorizer
类。
解析策略
可以根据$user
和$record
实例化并返回一个策略。
(new Authorizer)->policy(new User, Article::class); //=> ArticlePolicy
默认情况下,策略解析将根据$record
的类名追加'Policy'
。这可以通过在$record
类上提供静态policyClass
方法来自定义。例如,如果Article
的策略在Policies\ArticlePolicy
中,创建一个类似的方法
class Article { static public function policyClass() { return \Policies\ArticlePolicy::class; } }
建议您的
$record
对象扩展一个类,该类实现了一个policyClass
方法,该方法适用于大多数/所有记录类,而不是在每个记录上手动指定FQN。
解析作用域
可以根据$user
和基本$scope
实例化并返回一个作用域。与返回作用域类不同,Authorizer
会为您在作用域类上调用resolve()
,并返回结果集。
(new Authorizer)->scope(new User, new Article); //=> a scoped resultset
与策略解析类似,默认情况下,作用域解析将在$scope
对象末尾追加'Scope'
。这可以通过在$record
类上提供静态scopeClass
方法来自定义。
class Article { static public function scopeClass() { return \Policies\ArticleScope::class; } }
重要的是要注意,很多时候您会将部分构建的查询对象作为$record
传递给scope()
方法,而不是一个实际解析到作用域类的记录实例。例如,上面提到的更实际的例子可能看起来像这样
(new Authorizer)->scope(new User, Article::where('promoted', true)); //=> ArticleScope
上面的第二个参数将返回一个Illuminate\Database\Eloquent\Builder
实例,而不是一个Article
实例。如果没有更多帮助,范围解析将失败。解析器必须被告知如何确定实际的记录来解析范围。这是通过一个闭包作为可选的第三个参数来完成的,该闭包将传递作者化器接收到的$scope
。
(new Authorizer)->scope( new User, Article::where('promoted', true), function ($scope) { return $scope->getModel(); } ); //=> a scoped resultset
严格解析
如果找不到策略或范围,将返回null
。如果您需要停止执行,请调用policyOrFail()
或scopeOrFail()
而不是简单地调用policy()
或scope()
。
(new Authorizer)->policyOrFail(new User, new Blog); //=> throws Deefour\Authorizer\Exception\NotDefinedException
授权
授权器还提供了一个接收$user
、$record
和$action
的authorize
方法。如果解析的策略的动作方法返回的不是true
,将抛出一个异常。
(new Authorizer)->policyOrFail(new User, new Article, 'edit'); //=> throws Deefour\Authorizer\Exception\NotAuthorizedException
失败原因
授权器将策略动作方法返回的除true
之外的所有值视为失败。如果返回一个字符串,它将通过NotAuthorizedException
的message传递。此消息可以用来通知用户他们尝试执行操作被拒绝的确切原因。
class ArticlePolicy { public function edit() { if ($this->record->user->is($this->user)) { return true; } return 'You are not the owner of this article.'; } }
try { (new Authorizer)->authorize(new User, new Article, 'edit'); } catch (NotAuthorizedException $e) { echo $e->getMessage(); //=> 'You are not the owner of this article.' }
允许的属性
授权器可以获取特定操作的允许的大规模分配属性的白名单。
(new Authorizer)->permittedAttributes(new User, new Article); //=> ArticlePolicy::permittedAttributes() (new Authorizer)->permittedAttributes(new User, new Article, 'store'); //=> ArticlePolicy::permittedAttributesForStore()
封闭系统
许多应用程序只允许用户在认证的情况下执行操作。您不必在每次策略动作上验证当前用户是否已登录,您可以创建一个所有其他策略都扩展的基础策略。
abstract class Policy { public function __construct($user, $record) { if (is_null($user) or ! $user->exists) { throw new NotAuthorizedException($record, $this, 'initalization', 'You must be logged in!'); } parent::__construct($user, $record); } }
让类知道授权
除了Authorizer
类之外,还提供了一个Deefour\Authorizer\ProvidesAuthorization
特质,以便更容易地授权用户活动。
准备授权
该特质可以用于任何类,只要它覆盖实现类上的以下三个protected
方法
authorizerUser()
这应该返回用于授权的用户对象。如果没有登录用户,返回一个新的/新鲜的/空的用户对象可能很有用。
authorizerAction()
这应该返回策略中要调用的动作的名称。通常这基于处理当前请求的控制器方法。
authorizerAttributes()
这应该返回请求的输入数据的数组。这只有在您利用大规模分配保护时才需要覆盖。
用法
检索策略
包含这个特质后,可以在控制器中检索策略。用于策略实例化的所需$user
是从覆盖的authorizerUser()
方法中派生的。
$this->policy(new Article); //=> ArticlePolicy
检索范围
范围可以通过类似的简单方式完成。类似于Authorizer
类,这将为您调用范围的resolve()
,返回结果集。以下提供了一个闭包,返回$record
,范围类应该基于传递的基$scope
进行解析。
$this->scope( Article::newQuery(), function($scope) { return $scope->getModel(); } ); //=> a scoped resultset
与策略解析一样,用于策略实例化的所需$user
是从覆盖的authorizerUser()
方法中派生的。
授权检查
失败的授权检查将抛出一个Deefour\Authorizer\Exception\NotAuthorizedException
实例。这可以单行代码中断方法执行。
public function edit(Article $article) { $this->authorize($article); //=> NotAuthorizedException will be thrown on failure echo "You can edit this article!" }
与策略类似,用于范围实例化的所需$user
和$action
是从覆盖的authorizerUser()
和authorizerAction()
方法中派生的。可以将动作作为第二个参数传递,以在策略上调用特定的方法,而不是authorizerAction()
将返回的方法。
$this->authorize($article, 'modify');
大规模分配
模型属性也可以安全地进行大规模分配。调用permittedAttributes()
将从authorizerAttributes()
方法返回的请求信息中拉取属性的白名单。在幕后,为$record
实例化一个策略,所需的$user
和$action
也是从覆盖的authorizerUser()
和authorizerAction()
方法中派生的。
public function update(Article $article) { $article->forceFill($this->permittedAttributes(new Article))->save(); }
可以向 permittedAttributes()
提供第二个参数,以调用策略中的特定方法变体(如果可用)。
Laravel 内的授权
将此库集成到 Laravel 应用程序中非常简单。
实现特性方法覆盖
在 Laravel 应用程序中,满足上述覆盖的实现可能如下所示
use App\User; use Auth; use Deefour\Authorizer\ProvidesAuthorization; use Illuminate\Routing\Controller as BaseController; use Request; use Route; class Controller extends BaseController { use ProvidesAuthorization; protected function authorizerAction() { $action = Route::getCurrentRoute()->getActionName(); return substr($action, strpos($action, '@') + 1); } protected function authorizerUser() { return Auth::user() ?: new User; } protected function authorizerAttributes() { return Request::all(); } }
优雅地处理未授权异常
当调用 authorize()
失败时,会抛出 Deefour\Authorizer\NotAuthorizedException
异常。您可以将 Laravel 应用程序的 App\Exceptions\Handler
修改为支持此异常。
-
将
Deefour\Authorizer\Exception\NotAuthorizedException:class
添加到$dontReport
列表。 -
在文件顶部导入
Deefour\Authorizer\Exception\NotAuthorizedException
。 -
将您的
prepareException()
方法修改如下
protected function prepareException(Exception $e) { if ($e instanceof NotAuthorizedException) { return new HttpException(403, $e->getMessage()); }}
return parent::prepareException($e);
}
```
确保使用策略
可以在控制器构造函数中提供一个中间件闭包,以防止默认情况下未进行授权检查的操作被广泛开放。
public function __construct() { $this->middleware(function ($request, $next) { $response = $next($request); $this->verifyAuthorized(); return $response; }); }
如果控制器操作在没有调用 authorize()
的情况下运行,将会抛出 Deefour\Authorizer\Exceptions\AuthorizationNotPerformedException
异常。
存在一个 verifyScoped
方法来确保使用了一个作用域,如果没有调用 scope()
,将会抛出 Deefour\Authorizer\Exceptions\ScopingNotPerformedException
异常。
有时,绕过这种全面的授权或作用域要求可能是必要的。如果在验证之前调用 skipAuthorization()
或 skipScoping()
,则不会抛出异常。
帮助表单请求
Laravel 的 Illuminate\Foundation\Http\FormRequest
类有一个 authorize()
方法。将策略集成到表单请求对象中很容易。一个额外的优点是验证规则也可以基于授权。
namespace App\Http\Requests; use Deefour\Authorizer\ProvidesAuthorization; use Illuminate\Foundation\Http\FormRequest; class CreateArticleRequest extends FormRequest { use ProvidesAuthorization; public function authorize() { return $this->authorize(new Article); } public function rules() { $rules = [ 'title' => 'required' ]; if ( ! $this->policy->createWithoutApproval()) { $rules['approval_from'] => 'required'; } return $rules; } protected authorizerUser() { return $this->user(); } protected authorizerAttributes() { return $this->all(); } protected authorizerAction() { return $this->has('id') ? 'create' : 'edit'; } }
贡献
- 问题跟踪器:[https://github.com/deefour/authorizer/issues](https://github.com/deefour/authorizer/issues)
- 源代码:[https://github.com/deefour/authorizer](https://github.com/deefour/authorizer)
变更日志
2.2.0 - 2017年2月12日
- 在类上检查
modelName()
方法以解析针对不同模型的不同策略和作用域已被更改为modelClass()
。
2.1.1 - 2017年2月8日
- 感谢 @gmedeiros 修复了作用域解析的 Bug。
2.1.0 - 2016年9月14日
- 将
permittedAttributes()
方法添加到Authorizer
类中。 - 文档块。
2.0.0 - 2016年9月13日
- 完全重写。
- API 的大部分内容保持不变,但为了简单起见,删除了许多接口和基类。
- 删除了 Laravel 特定的全局函数、外观和服务提供者。
- 简化了类解析(不再依赖于 deefour/producer)。
1.1.0 - 2016年1月14日
Authorizer
现在执行严格的类型检查。如果返回true
,则抛出NotAuthorizedException
。其他 '真值' 将失败授权。- 从策略返回的字符串现在将设置为授权失败的原因。
1.0.0 - 2015年10月7日
- 发布 1.0.0。
- 添加了
skipAuthorization()
和skipScoping()
方法,以绕过验证 API 的异常抛出。
0.6.0 - 2015年8月8日
- 对策略和作用域解析器进行了大量重写,现在使用
deefour/producer
。 - 已删除
policyNamespace()
、policyClass()
、scopeNamespace()
和scopeClass()
方法,改为使用resolve()
方法,该方法由deefour/producer
解析器使用。 - 现在政策要求在构造函数中传递一个
Authorizee
。
0.5.2 - 2015年7月31日
- 当未授权时,抛出
403
而不是401
。
0.5.1 - 2015年6月5日
- 现在遵循 PSR-2。
0.5.0 - 2015年6月2日
- 所有静态方法现在都是公共实例方法。
- 为了简单和与 Laravel 兼容,将
currentUser()
改为user()
。 - 代码清理。
0.4.0 - 2015年3月25日
- 新增
ResolvesAuthorizable
接口。这可以在例如deefour/presenter
的装饰器类中使用,将授权尝试映射回底层模型,因为呈现器本身没有实现Authorizable
接口。 - 现在当授权失败时,要求
symfony/http-kernel
抛出完整的 HTTP 异常。 - 代码格式改进。
0.3.0 - 2015年3月19日
- 添加了对策略作用域的改进支持。
- 从 Composer 自动加载中移除
helpers.php
。开发者应该能够选择是否包含这些函数。 - 清理了文档块。
0.2.0 - 2015年2月4日
- 添加
Authorizee
合同,以便将其附加到User
模型上,以便通过服务容器轻松查找。 - 类重组。
- Laravel 服务提供者的修复。
0.1.0 - 2014年11月13日
- 首次发布,独立于 deefour/Aide。
许可证
版权所有 (c) 2016 Jason Daly (deefour)。在 MIT 许可证 下发布。0Looking