micc83 / rooles

为 Laravel 5 的一款非常简单的角色和权限管理器

3.0.0 2021-06-28 15:51 UTC

README

简单的 Laravel ^7.0 角色和权限管理器

为什么还需要另一个 Laravel RBAC (基于角色的访问控制) ?!?

嗯,这是个好问题!大多数现有的 ACL 系统如 romanbican/roleskodeine/laravel-aclSentinel 都包含了大量的功能... 但这些功能我大多数时候并不需要! :D

这就是为什么我想构建一个最小的 Laravel 角色和权限管理器,它提供了一个非常简单的 RBAC 实现。每个用户可以被分配一个单独的角色,而每个角色的权限则存储在一个单独的配置文件中。此包提供了一个直观且文档完善的 API、一个用于在 Eloquent 用户模型上直接检查权限的 Trait 以及两个中间件来轻松保护路由和控制器。

然而,随着应用程序的增长,您可能需要一个更复杂的 ACL 系统。这就是为什么此包附带了一些 Contract,您可以根据需要使用它们来改进或替换功能。您可以将 Rooles 不仅视为一个完整工作的 RBAC,还可以作为一个 开发您自己的自定义角色和权限管理器的起点

设置

从包含 Laravel composer.json 文件的路径中,在终端运行以下命令

$ composer require micc83/rooles

打开 config/app.php 并在提供者数组末尾添加以下行

Rooles\RoolesServiceProvider::class

从您的终端运行以下命令以发布迁移文件(它将简单地向默认用户表添加一个 role 列),配置文件和一个默认的 blade 模板用于 403-Forbidden 视图(如果已创建,则不会发布)

$ php artisan vendor:publish

为了能够使用路由和控制器中间件(以便能够过滤谁能够访问特定的路由或控制器方法),打开 App/Http/Kernel.php 并在 $routeMiddleware 数组末尾添加以下行

'perms' => \Rooles\PermsMiddleware::class,
'role'  => \Rooles\RoleMiddleware::class,

由于 Rooles 是在 Laravel 默认的 Auth 系统和 Eloquent 用户模型上工作的,您必须将 Rooles\Traits\UserRole Trait 添加到位于 App/User.php 的用户类中,如下所示

use \Rooles\Traits\UserRole;

class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{

    use Authenticatable, CanResetPassword, UserRole;

    // ...
}

关于 Laravel 版本 >= 1.1.11 的重要说明 从这个版本开始,Laravel 通过 Authorizable Trait/Contract 实现了自己的权限管理器,因此,为了让用户模型与 Rooles 一起工作,您必须从 Eloquent 用户模型中删除对 Authorizable Trait 和接口的任何引用。

设置用户角色

每个用户只能分配一个角色。您可以在 User Eloquent 模型中通过添加以下方式将角色属性进行硬编码

protected $attributes = [
    'role' => 'admin'
];

或者运行提供的迁移来将 role 列添加到用户表中,以便能够在运行时更改用户角色

$user = User::find(1);
$user->role = 'admin';
$user->save();

设置权限

任何给定角色的所有权限都设置在 config/rooles.php 文件中,如下所示

<?php return [
    'roles' => [
        'default' => []
        'admin' => [
            'name' => 'Administrator',
            'grant' => '*'
        ],
        'editor' => [
            'grant' => [
                'posts.*',
                'users.*.read',
                'users.*.ban',
                'comments.*',
                'profile.*'
            ],
            'deny' => [
                'users.admin.ban',
                'posts.delete',
                'comments.delete'
            ]
        ]
    ]
];

如您所见,使用的格式是

[
    'roles' => [
        'role_id' => [
            'name' => 'role_name',
            'grant' => 'string_or_array_of_granted_permissions',
            'deny' => 'string_or_array_of_denied_permissions',
        ]
    ]
]

default 角色应用于没有分配角色的任何用户,并且不提供任何权限,除非配置文件中另有说明。

属性 name 是可选的,允许设置与提供的角色 ID 不同的名称。

您还可以手动创建角色和处理权限。以下是一个示例

app()->make(\Rooles\Contracts\RoleRepository::class)
     ->getOrCreate('customer')
     ->assignName('Client')
     ->grant(['cart.*', 'products.buy'])
     ->deny('cart.discount');

权限策略

在为 角色 创建权限策略时,有四个主要概念需要记住

  1. 每个角色都默认没有权限;
  2. 通配符字符 * 用于定义一组可用的权限。例如,如果我们考虑授权 users.*.ban,这意味着编辑可以禁止任何用户组(例如 users.readerusers.author 等),但不能禁止 users.admin,因为该权限已在拒绝数组中被拒绝。
  3. 当您授予或拒绝权限时,如果尚未设置,则将自动附加通配符,因此 customerscustomers.* 相同。这也意味着将授予或拒绝给定权限的任何子权限,例如
        $role->grant('comments'); // Same as writing comments.*
    
        $role->can('comments.write'); // true
        $role->can('comments.pingbacks.write') // true
  4. 当您按顺序应用授予和拒绝权限以确定哪个规则将“获胜”时,您必须从 具体性 的角度考虑。更具体的规则总是获胜。让我们看看一个例子
        $role->grant('comments.write.*') // Same as writing comments.write
             ->deny('*.write');
    
        $role->can('comments.write'); // true
        $role->can('users.write') // false

正如您可能从示例中猜测的那样,具体性是根据通配符的位置和权限的长度计算的。您将通配符向右移动时,您将获得更高的具体性。

检查用户权限

您可以在控制器方法或您感到舒适的地方检查特定用户的权限,如下所示

$user = User::find(1);
if ($user->can('comments.post')){
    // Do something...
}

同样,检查登录用户的权限

public function index(Illuminate\Contracts\Auth\Guard $auth) {

    if ( $auth->user->can('users.list') ){
        // Do something...
    }

}

API公开了一个方便的方法来否定权限断言

if ( $user->cannot('users.list') ) redirect()->to('dashboard');

您可以通过传递数组来评估多个断言

if ( $user->can(['users.list', 'users.read']) ) // Do something when the user has both the permissions (AND)

还有两个方便的操作符可以与 can/cannot 断言一起使用

if ( $user->can('users.list&users.read') ) // Do something when the user has both the permissions (& > AND)
if ( $user->can('users.list|users.read') ) // Do something when the user has one of the requested permissions (| > OR)

可以将多个操作符连接在一起,但请注意,AND 操作符始终比 OR 操作符具有优先级。

检查用户角色

您可以对用户角色 ID(不区分大小写)进行更一般的断言

if ( $user->role->is('admin') ) echo 'Hello Boss';

或检查用户角色 ID 是否在给定的范围内(仍然不区分大小写)

if ( $user->role->isIn(['lamer', 'trool']) ) echo 'Hello Looser';

您还可以使用以下语法之一获取用户角色名称(如果没有提供名称,将返回 ID)

// If in a string context:
echo $user->role;
// Otherwise:
if ($user->role->name() === 'Admin') // Do something

如果您需要进行一些比较,例如在 Select 输入字段中,您最好使用 ID 而不是名称。例如

{!! Form::select('role', ['editor' => 'Editor', 'admin' => 'Administrator'], $user->role->id()) !!}

请记住,角色 ID 会自动转换为小写,支持 UTF8。

通过中间件保护路由和控制器

角色 提供了两个中间件来保护路由和控制器。

要使用户角色保护路由,您可以使用 角色中间件

Route::get('admin/users/', [
    'middleware' => [
        'auth',
        'role:admin|editor', // Give access to both admins and editors
    ],
    function () {
        return view('admin.users.index');
    }
]);

为了检查用户在路由上的权限,您可以使用 perms 中间件 如下

Route::get('admin/users/', [
    'middleware' => [
        'auth',
        'perms:users.list|users.edit', // Give access to users with users.list OR users.edit permissions
    ]
    function () {
        return view('admin.users.index');
    }
]);

在这两种情况下,您可能已经注意到我调用了 Auth 中间件,因为用户必须登录才能检查其角色和权限。

大多数时候,您可能会处理路由组,在这种情况下,您可以直接

// Route Group
Route::group([
    'middleware' => [
        'auth',
        'role:admin|editor' // Give access to both admins and editors
    ]
], function () {
    Route::resource('users', 'UserController');
    Route::resource('posts', 'PostController');
});

中间件也可以在控制器中使用,如下所示

class UserController extends Controller
{

    /**
     * @var UserRepository
     */
    private $users;

    /**
     * @param UserRepo $users
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
        $this->middleware('perms:users', ['except' => 'show']);
    }

在这里,我们表示,为了访问任何控制器方法,我们必须拥有提供所有 users 权限的角色,但我们不需要任何权限来显示用户资料。您可以在官方 Laravel 网站 上找到有关控制器中间件更好的文档。

处理中间件的 HTTP 错误响应

中间件根据请求的性质以不同的方式处理错误响应。对于Ajax请求,它们将以JSON对象和以下403状态码响应:

{
    "error" : {
        "code" : 403,
        "message" : "Forbidden"
    }
}

这样您就可以在JavaScript中拦截它,如下所示:

if ('error' in response) console.log(response.error.message);

对于正常请求,如果缺少授权,将抛出Rooles\ForbiddenHttpException异常,默认情况下(当禁用调试时)将导致之前发布的403错误页面,并带有403状态码。您可以通过编辑resources/views/errors/403.blade.php模板来自定义页面。

否则,如果您不想显示视图而是实现一些自定义行为,您可以在app/Exceptions/Handler.php中的render方法中进行操作,如下所示:

public function render($request, Exception $e)
{
    if ($e instanceof \Rooles\ForbiddenHttpException) {
        return redirect('/')->withErrors(['You don\'t have the needed permissions to perform this action!']);
    }
    return parent::render($request, $e);
}

这样,当抛出禁止错误时,您将被重定向到指定的页面,并带有错误闪存消息。要显示消息,您可以在您的blade模板中添加以下内容:

@if ($errors->has())
<div class="alert alert-danger">
    @foreach ($errors->all() as $error)
        {{ $error }}
    @endforeach
</div>
@endif

文档

我坚信,即使世界上最好的编码应用程序,如果没有良好的文档,也注定要失败。这就是为什么我谦卑地请求您在认为某些内容缺失或可以改进时提交一个问题。

贡献

如果您愿意为项目做出贡献,我将非常高兴,但我要求不要实现新功能,而是改进现有的一些功能(改进模式、算法等)。每个PR必须遵循PSR-2编码规范,并通过所有现有测试(或在需要时添加更多测试)。