rbac 教程

____發表於2019-12-20

說明

前後端分離的前提下,後臺介面使用多路由和多語言 rbac

本專案: github 地址

本專案前準備專案: github 地址 基於已開發好的 jwt 和多端路由

安裝 spatie/laravel-permission 擴充套件

composer require spatie/laravel-permission

rbac 教程

釋出 migration 檔案

php artisan vendor:publish --provider="Spatie.ermission.ermissionServiceProvider" --tag="migrations"
rbac 教程
此命令會在 database/migrations 下生成 _xxxx_create_permissiontables.php 檔案
rbac 教程

填充 root 使用者

備註:root 使用者將作為超級管理員,不受許可權限制
php artisan make:migration seed_admins_table
rbac 教程
rbac 教程

php artisan make:migration seed_users_table
rbac 教程
rbac 教程

說明

  • 關於許可權驗證, 我是通過路由名和許可權名一一對應的

  • 關於許可權入庫,我將許可權寫在語言檔案裡(為了多語言功能,並且方便管理),通過一份命令檔案將許可權入庫,具體邏輯見下面說明

  • 許可權預設只有 2 級

許可權語言檔案

resources/language/en 下 新建 permission/admin.phppermission/api.php

備註:此處檔名必須和路由檔名一致

resources/language/en/permission/admin.php

<?php

return [
    // 管理員
    [
        'value' => 'admin.admins.index',
        'title' => 'Admin',
        'children' => [
            ['value' => 'admin.admins.show', 'title' => 'View'],
            ['value' => 'admin.admins.store', 'title' => 'Add'],
            ['value' => 'admin.admins.update', 'title' => 'Update'],
            ['value' => 'admin.admins.destroy', 'title' => 'Delete'],
            ['value' => 'admin.admins.syncRoles', 'title' => 'Associated Role'],
        ]
    ],
    // 角色
    [
        'value' => 'admin.roles.index',
        'title' => 'Role',
        'children' => [
            ['value' => 'admin.roles.show', 'title' => 'View'],
            ['value' => 'admin.roles.store', 'title' => 'Add'],
            ['value' => 'admin.roles.update', 'title' => 'Update'],
            ['value' => 'admin.roles.destroy', 'title' => 'Delete'],
            ['value' => 'admin.roles.syncPermissions', 'title' => 'Association Permissions'],
        ],
    ],
    // 許可權
    [
        'value' => 'admin.permissions.index',
        'title' => 'Permission',
    ],
];

resources/language/zh-CN/permission/admin.php

<?php

return [
    // 管理員
    [
        'value' => 'admin.admins.index',
        'title' => '管理員',
        'children' => [
            ['value' => 'admin.admins.show', 'title' => '檢視'],
            ['value' => 'admin.admins.store', 'title' => '新增'],
            ['value' => 'admin.admins.update', 'title' => '修改'],
            ['value' => 'admin.admins.destroy', 'title' => '刪除'],
            ['value' => 'admin.admins.syncRoles', 'title' => '關聯角色'],
        ]
    ],
    // 角色
    [
        'value' => 'admin.roles.index',
        'title' => '角色',
        'children' => [
            ['value' => 'admin.roles.show', 'title' => '檢視'],
            ['value' => 'admin.roles.store', 'title' => '新增'],
            ['value' => 'admin.roles.update', 'title' => '修改'],
            ['value' => 'admin.roles.destroy', 'title' => '刪除'],
            ['value' => 'admin.roles.syncPermissions', 'title' => '關聯許可權'],
        ],
    ],
    // 許可權
    [
        'value' => 'admin.permissions.index',
        'title' => '許可權',
    ],
];

整體目錄結構如下
rbac 教程

許可權入庫命令檔案

php artisan make:command SeedPermission
rbac 教程

config/filesystems.php 下找到 disks => [···]

加入如下程式碼

 'disks' => [
        ·
        ·
        ·
        'root' => [
            'driver' => 'local',
            'root' => '/'
        ]
    ]

修改 app/Commands/SeedPermission.php 為如下程式碼

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\Permission\Models\Permission;

class SeedPermission extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'seed-permission';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '填充許可權';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        // 獲取 zh-CN 語言包中的許可權下所有檔案
        $files = Storage::disk('root')->files(resource_path('lang/zh-CN/permission'));

        try {
            is_null(!$files);
        } catch (\Exception $e) {
            report($e);
            return false;
        }

        DB::transaction(function () use ($files) {
            foreach ($files as $file) {
                // 獲取守衛名稱
                $guardName = basename($file, '.php');
                $array = include(resource_path('lang/zh-CN/permission/') . $guardName . '.php');

                $values = [];
                foreach ($array as $arr) {
                    $values[] = $arr['value'];
                    if (isset($arr['children']) && is_array($arr['children'])) {
                        foreach ($arr['children'] as $child) {
                            $values[] = $child['value'];
                        }
                    }
                }
                // 獲取資料庫中的許可權
                $permissions = Permission::select('name')
                    ->where('guard_name', $guardName)
                    ->get()
                    ->pluck('name');
                // 篩選出不同的許可權
                $diff = collect($values)->diff($permissions);
                foreach ($diff as $item) {
                    Permission::create(['name' => $item, 'guard_name' => $guardName]);
                }
                // 反向刪除
                $diff2 = collect($permissions)->diff($values);
                foreach ($diff2 as $item) {
                    Permission::where(['name' => $item, 'guard_name' => $guardName])->delete();
                }
            }
        });

        $this->info('許可權已更新');
    }
}

執行 php artisan seed-permission
rbac 教程
可以看到資料庫 permissions 表已更新
rbac 教程

說明

關於許可權的驗證, 上文已說過,是通過許可權名和路由名一一對應驗證,考慮大部分名稱統一,所以我寫了一箇中介軟體,

如果某些路由不需要驗證,或者某些路由和其他路由共用,則會有一份許可權路由檔案單獨處理。具體見以下程式碼邏輯

建立中介軟體

語言中介軟體

php artisan make:middleware Locale
rbac 教程

app/Http/Middleware/Locale.php

<?php

namespace App\Http\Middleware;

use Closure;

class Locale
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        // 是否設定語言引數,沒有的情況下預設使用中文
        $language = $request->has('language') ? $request->language : 'zh-CN';
        app()->setLocale($language);
        return $next($request);
    }
}

許可權驗證中介軟體

php artisan make:middleware CheckPermissions
rbac 教程

app/Http/Middleware/CheckPermissions.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Spatie\Permission\Guard;

class CheckPermissions
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        // 超級管理員預設通過所有許可權
        if (Auth::user()->isRoot()) {
            return $next($request);
        }

        // 獲取當前路由名稱
        $currentRouteName = Route::currentRouteName();

        // 獲取當前守衛名稱
        $guardName = Guard::getDefaultName(self::class);
        // 引入當前守衛的許可權檔案
        $routes = include(app_path('Permissions/') . $guardName . '.php');

        // 替換設定了關聯關係的許可權
        if (is_array($routes) && key_exists($currentRouteName, $routes)) {
            $currentRouteName = $routes[$currentRouteName];
        }

        // 當路由不為 null 時,驗證許可權
        if (!is_null($currentRouteName)) {
            Gate::authorize($currentRouteName);
        }

        return $next($request);
}

app/Admin.php 增加如下程式碼

  /**
     * 判斷是否是 root 使用者
     *
     * @return bool
     */
    public function isRoot()
    {
        return $this->name === 'root';
    }

app 下新建 Permissions/admin.phpPermissions/api.php

備註:此處檔名必須和路由檔名一致

app/Permissions/admin.php

<?php
return [
    'admin.admins.current.show' => null,
    'admin.admins.current.update' => null,
];

app/Permissions/api.php

<?php

return [
    'api.users.current.show' => null,
    'api.users.current.update' => null,
];

如上所述,這裡我們為特殊的路由額外設定關聯

註冊中介軟體

app/Http/Kernel.php 中找到 protected $routeMiddleware = [···]

protected $routeMiddleware = [
        ·
        ·
        ·
        // 設定語言中介軟體
        'locale' => \App\Http\Middleware\Locale::class,
        // 檢查許可權
        'check.permissions' => CheckPermissions::class
    ];

修改 routes/admin.php

<?php

Route::group([
    'prefix' => 'v1',
    'middleware' => ['bindings', 'locale']
], function () {
    // 登入介面
    Route::group([
    ], function () {
        // 獲取 token
        Route::post('authorizations', 'AuthorizationsController@store')
            ->name('admin.authorizations.store');
        // 重新整理 token
        Route::put('authorizations/current', 'AuthorizationsController@update')
            ->name('admin.authorizations.update');
        // 刪除 token
        Route::delete('authorizations/current', 'AuthorizationsController@destroy')
            ->name('admin.authorizations.destroy');
    });

    // 需要 token 驗證的介面
    Route::group([
        'middleware' => [
            // 此處認證的是 admin 守衛
            'auth:admin',
            'check.permissions'],
    ], function () {
        /****************************************************管理員*******************************************************/
        // 列表
        Route::get('admins', 'AdminsController@index')
            ->name('admin.admins.index');
        // 檢視當前使用者
        Route::get('admins/current/show', 'AdminsController@currentShow')
            ->name('admin.admins.current.show');
        // 詳情
        Route::get('admins/{admin}', 'AdminsController@show')
            ->name('admin.admins.show');
        // 新增
        Route::post('admins', 'AdminsController@store')
            ->name('admin.admins.store');
        // 修改當前使用者
        Route::patch('admins/current/update', 'AdminsController@currentUpdate')
            ->name('admin.admins.current.update');
        // 修改
        Route::patch('admins/{admin}', 'AdminsController@update')
            ->name('admin.admins.update');
        // 刪除
        Route::delete('admins/{admin}', 'AdminsController@destroy')
            ->name('admin.admins.destroy');
        // 關聯角色
        Route::post('admins/{admin}/syncRoles', 'AdminsController@syncRoles')
            ->name('admin.admins.syncRoles');

        /****************************************************角色*******************************************************/
        // 列表
        Route::get('roles', 'RolesController@index')
            ->name('admin.roles.index');
        // 詳情
        Route::get('roles/{role}', 'RolesController@show')
            ->name('admin.roles.show');
        // 新增
        Route::post('roles', 'RolesController@store')
            ->name('admin.roles.store');
        // 修改
        Route::patch('roles/{role}', 'RolesController@update')
            ->name('admin.roles.update');
        // 刪除
        Route::delete('roles/{role}', 'RolesController@destroy')
            ->name('admin.roles.destroy');
        // 關聯許可權
        Route::post('roles/{role}/syncPermissions', 'RolesController@syncPermissions')
            ->name('admin.roles.syncPermissions');

        /****************************************************許可權*******************************************************/
        // 列表
        Route::get('permissions', 'PermissionsController@index')
            ->name('admin.permissions.index');
    });
});

首先我們來驗證一下

用 admin1 的 token 請求

rbac 教程

此時 403, 訪問被拒絕

用 root 的 token 請求

rbac 教程

成功返回資料

接下來我們給 admin1 使用者新增一個角色,並且該角色擁有檢視管理員列表的許可權

建立角色控制器

php artisan make:controller Admin/RolesController

rbac 教程

app/Http/Controllers/Admin/RolesController.php

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Requests\Admin\RoleRequest;
use App\Http\Resources\RoleResource;
use Illuminate\Http\Request;
use Spatie\Permission\Guard;
use Spatie\Permission\Models\Role;

class RolesController extends Controller
{
    /**
     * 列表
     *
     * @param Request $request
     * @param Role $role
     * @return mixed
     */
    public function index(Request $request, Role $role)
    {
        // 預設 20 頁
        $limit = $request->has('limit') ? $request->limit : 20;

        // 獲取當前守衛名稱
        $guardName = Guard::getDefaultName(self::class);
        $roles = $role->where('guard_name', $guardName)->paginate($limit);
        return RoleResource::collection($roles);
    }

    /**
     * 詳情
     *
     * @param $role
     * @return RoleResource
     */
    public function show($role)
    {
        return new RoleResource($role);
    }

    /**
     * 新增
     *
     * @param RoleRequest $request
     * @param Role $role
     * @return RoleResource
     */
    public function store(RoleRequest $request, Role $role)
    {
        $role = $role::create($request->all());
        return new RoleResource($role);
    }

    /**
     * 修改
     *
     * @param RoleRequest $request
     * @param Role $role
     * @return RoleResource
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function update(RoleRequest $request, Role $role)
    {
        $role->update($request->all());
        return new RoleResource($role);
    }

    /**
     * 刪除
     *
     * @param Role $role
     * @return \Illuminate\Http\Response
     * @throws \Exception
     */
    public function destroy(Role $role)
    {

        $role->delete();
        return response()->noContent();
    }

    /**
     * 給角色新增許可權
     *
     * @param RoleRequest $request
     * @param Role $role
     * @return RoleResource
     */
    public function syncPermissions(RoleRequest $request, Role $role)
    {
        $role->syncPermissions($request->permissions);
        return new RoleResource($role);
    }
}

建立資源

php artisan make:resource RoleResource

我們用 root 使用者建立一個角色

rbac 教程

然後我們為該角色新增檢視管理員列表和詳情的許可權

rbac 教程

接下來我們為 admin1 使用者關聯角色

修改 app/Admin.php

use Spatie\Permission\Traits\HasRoles;

class Admin extends Authenticatable implements JWTSubject
{
    use HasRoles;

    // admin 表我們使用的是 admin 守衛
    protected $guard_name = 'admin';
}

修改 app/Http/Controllers/Admin/AdminsController.php

 /**
     * 為管理員新增角色
     *
     * @param Request $request
     * @param Admin $admin
     * @return AdminResource
     */
    public function syncRoles(Request $request, Admin $admin)
    {
        $admin->syncRoles($request->roles);
        return new AdminResource($admin);
    }

rbac 教程

此時我們再用 admin1 的 token 去請求管理員列表介面時便會返回資料

許可權列表檢視

php artisan make:controller Admin/PermissionsController

rbac 教程

app/Http/Controllers/Admin/PermissionsController.php

<?php

namespace App\Http\Controllers\Admin;

use Illuminate\Http\Request;

class PermissionsController extends Controller
{
    /**
     * 列表
     *
     * @return array|\Illuminate\Contracts\Translation\Translator|string|null
     */
    public function index()
    {
        $permissions = trans('permission/admin', [], app()->getLocale());
        return response()->json(['data' => $permissions]);
    }
}

rbac 教程

rbac 教程

補充

此時我們角色表是可以跨端操作的,所以我加了 policy 不允許跨端操作

php artisan make:policy RolePolicy

rbac 教程

app/Providers/AuthServiceProvider.php 找到 protected $policies = [···];

use App.olicies.olePolicy;
use Spatie.ermission.odels.ole;
protected $policies = [
    Role::class => RolePolicy::class
];

修改 app/Policies/RolePolicy.php

use Spatie\Permission\Guard;
use Spatie\Permission\Models\Role;

     /**
     * 不允許跨守衛操作
     *
     * @param Role $role
     * @return bool
     */
    public function authorize($current, Role $role)
    {
        return $role->guard_name == Guard::getDefaultName(self::class);
    }

app/Http/Controllers/Admin/RolesController.php 中找到 show(), update(), destroy()

加入以下程式碼

$this->authorizeForUser(Auth::guard('admin')->user(), 'authorize', $role);

總結

另外一個端以及一些驗證參見具體程式碼,邏輯整體不變。

還有點缺陷的是角色多語言並未實現,之前考慮的一個思路是在 roles 表裡橫向擴充套件,比如 name_en, name_zh_CN。

參考教程

spatie/laravel-permission

相關文章