說明
前後端分離的前提下,後臺介面使用多路由和多語言 rbac
本專案: github 地址
本專案前準備專案: github 地址 基於已開發好的 jwt 和多端路由
安裝 spatie/laravel-permission
擴充套件
composer require spatie/laravel-permission
釋出 migration 檔案
php artisan vendor:publish --provider="Spatie.ermission.ermissionServiceProvider" --tag="migrations"
此命令會在 database/migrations 下生成 _xxxx_create_permissiontables.php 檔案
填充 root 使用者
備註:root 使用者將作為超級管理員,不受許可權限制php artisan make:migration seed_admins_table
php artisan make:migration seed_users_table
說明
-
關於許可權驗證, 我是通過路由名和許可權名一一對應的
-
關於許可權入庫,我將許可權寫在語言檔案裡(為了多語言功能,並且方便管理),通過一份命令檔案將許可權入庫,具體邏輯見下面說明
-
許可權預設只有 2 級
許可權語言檔案
在 resources/language/en 下 新建 permission/admin.php 和 permission/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' => '許可權',
],
];
整體目錄結構如下
許可權入庫命令檔案
php artisan make:command SeedPermission
在 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
可以看到資料庫 permissions 表已更新
說明
關於許可權的驗證, 上文已說過,是通過許可權名和路由名一一對應驗證,考慮大部分名稱統一,所以我寫了一箇中介軟體,
如果某些路由不需要驗證,或者某些路由和其他路由共用,則會有一份許可權路由檔案單獨處理。具體見以下程式碼邏輯
建立中介軟體
語言中介軟體
php artisan make:middleware Locale
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
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.php 和 Permissions/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 請求
此時 403, 訪問被拒絕
用 root 的 token 請求
成功返回資料
接下來我們給 admin1 使用者新增一個角色,並且該角色擁有檢視管理員列表的許可權
建立角色控制器
php artisan make:controller Admin/RolesController
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 使用者建立一個角色
然後我們為該角色新增檢視管理員列表和詳情的許可權
接下來我們為 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);
}
此時我們再用 admin1 的 token 去請求管理員列表介面時便會返回資料
許可權列表檢視
php artisan make:controller Admin/PermissionsController
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]);
}
}
補充
此時我們角色表是可以跨端操作的,所以我加了 policy 不允許跨端操作
php artisan make:policy RolePolicy
在 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。