deefour/authorizer

通过PHP类实现简单授权

2.2.0 2017-02-12 21:58 UTC

This package is auto-updated.

Last update: 2024-09-16 07:54:08 UTC


README

Build Status Total Downloads Latest Stable Version License

通过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$actionauthorize方法。如果解析的策略的动作方法返回的不是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 修改为支持此异常。

  1. Deefour\Authorizer\Exception\NotAuthorizedException:class 添加到 $dontReport 列表。

  2. 在文件顶部导入 Deefour\Authorizer\Exception\NotAuthorizedException

  3. 将您的 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日

许可证

版权所有 (c) 2016 Jason Daly (deefour)。在 MIT 许可证 下发布。0Looking