rbac 教程
说明
前后端分离的前提下,后台接口使用多路由和多语言 rbac
本项目: github 地址
本项目前准备项目: github 地址 基于已开发好的 jwt 和多端路由
更新说明
本篇 rbac 教程有 2 个前提,不同于 laravel-admin 的 rbac
- 角色是后台运营自己管理的
- 权限本地和线上统一,方便维护
安装 spatie/laravel-permission
扩展
composer require spatie/laravel-permission
发布 migration 文件
php artisan vendor:publish --provider="Spatie.ermission.ermissionServiceProvider" --tag="migrations"
此命令会在 database/migrations 下生成 xxxx_create_permission_tables.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\Policies\RolePolicy;
use Spatie\Permission\Models\Role;
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。
参考教程
分割线
补充上次未完成的角色多语言
给用户关联角色 bug 修复
思路
感觉不是很好直接修改 name 字段,所以我在 roles 表加了一个 name_en 的字段表示英文语言角色名,之前的 name 保留不变,作为中文名。
添加 name_en 字段
php artisan make:migration add_name_en_to_roles_table –table=roles
database/migrations/xxxx_add_name_en_to_roles_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddNameEnToRolesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('roles', function (Blueprint $table) {
$table->string('name_en')->nullable()->after('name')->comment('英文名称');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('roles', function (Blueprint $table) {
$table->dropColumn('name_en');
});
}
}
修改 Admin 模型
app/Admin.php
// 权限名称语言
CONST ROLE_NAME = [
'zh-CN' => 'name',
'en' => 'name_en'
];
备注:这里的键表示语言,值表示对应角色表中的多语言名称字段
修改 app/Http/Controllers/Admin/RolesController.php 为如下代码
public function store(RoleRequest $request, Role $role)
{
// 如果使用 $request->all(), 这里会把其他字段(如 language)也存入表,然后报错
$role = $role::create($request->only(Admin::ROLE_NAME));
return new RoleResource($role);
}
public function update(RoleRequest $request, Role $role)
{
$this->authorizeForUser(Auth::guard('admin')->user(), 'authorize', $role);
$role->update($request->only(Admin::ROLE_NAME));
return new RoleResource($role);
}
修改 app/Http/Requests/Admin/RoleRequest.php 为如下代码
<?php
namespace App\Http\Requests\Admin;
use App\Admin;
use App\Rules\CheckPermission;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Spatie\Permission\Guard;
class RoleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
// 获取路由的名称
$routeName = $this->route()->getName();
switch ($routeName) {
case 'admin.roles.store':
$rules = [];
foreach (Admin::ROLE_NAME as $value) {
$rules[$value] = [
'required', 'string',
Rule::unique('roles')->where(function ($query) {
return $query->where('guard_name', Guard::getDefaultName(self::class));
})
];
}
return $rules;
break;
case 'admin.roles.update':
$rules = [];
foreach (Admin::ROLE_NAME as $value) {
$rules[$value] = [
'string',
Rule::unique('roles')->where(function ($query) {
return $query->where('guard_name', Guard::getDefaultName(self::class));
})->ignore($this->role->id)
];
}
return $rules;
break;
case 'admin.roles.syncPermissions':
return [
'permissions' => 'required|array',
'permissions.*' => [
'required',
new CheckPermission()
]
];
break;
}
}
}
修改 app/Http/Resources/RoleResource.php 为如下代码
public function toArray($request)
{
$data = parent::toArray($request);
// 获取当前语言角色名称
$data['default_name'] = $data[Admin::ROLE_NAME[app()->getLocale()]];
return $data;
}
备注:default_name 表示当前语言的名称字段
postman 测试
问题 1 修复 (重要)
前端同事在使用的时候发现如果角色名是纯数字,在给用户关联角色时他其实是判断的角色表的 id
在 vendor/spaite/laravel-permission/src/Traits/HasRoles.php 中找到 getStoredRole($role)
protected function getStoredRole($role): Role
{
$roleClass = $this->getRoleClass();
if (is_numeric($role)) {
return $roleClass->findById($role, $this->getDefaultGuardName());
}
if (is_string($role)) {
return $roleClass->findByName($role, $this->getDefaultGuardName());
}
return $role;
}
可以看出确实是纯数字的情况他会查找 id,所以给在用户关联角色的时候我们直接使用 id 传值即可
简单的加一下 rule 验证
php artisan make:rule CheckRole
app/Rules/CheckRole.php
public function passes($attribute, $value)
{
$roles = Role::where('guard_name', Guard::getDefaultName(self::class))->get()->pluck('id')->toArray();
return in_array($value, $roles);
}
问题 2
之前看过一篇帖子说在同一个测试服务器相同的权限名称会有冲突,需要加前缀
目前还未验证过,但是我先加了!
发布配置文件
php artisan vendor:publish --provider="Spatie.ermission.ermissionServiceProvider" --tag="config"
在 config/permission.php 中找到 'cache => [··· 'key']'
修改为如下代码
‘key’ => env(‘SPATIE_PERMISSION_CACHE’, ‘spatie.permission.cache’),
在 .env 中加入
SPATIE_PERMISSION_CACHE=rbac
总结
到此应该整个项目完成。本文主要是提供一些思路和简单的实现。具体实现可查看项目, postman 文件也已放入根目录下。
如有错误,请指正。如果有更好的实现方法或者能优化的地方或者疑惑的地方,也欢迎讨论。
本作品采用《CC 协议》,转载必须注明作者和本文链接
rbac可以参考laravel-admin是怎么做
朋友,你的GitHub地址是404,可以处理一下吗?我想看看你的源码
php artisan seed-permission执行之后没有更新数据啊
最近在用前后端分离,打算仔细看一下