实现简单的、针对咖啡店增删改查的 RBAC 权限管理功能
随着应用的扩张,用户越来越多,数据越来越多,用来管理这些数据的后台管理系统日渐被提上日程,此外我们还要为用户进行分级,为不同角色用户设置不同的操作权限,比如商家可以编辑或删除自家的咖啡店,以及在自家公司下新增咖啡店,普通用户只有浏览权限,新增和更新操作需要审核通过才能执行,管理员才能登录到管理后台对大盘数据进行管理,以及为不具备权限的咖啡店更新和新增操作进行审核和处理。所以在开发管理后台功能之前,我们有必要先对用户进行角色和权限设置,并对之前的咖啡店增删改查添加相应的权限判断。
第一步:设置用户角色类型
在 Roast 应用中,我们通过为 users
表新增 permission
字段用来设置对应用户角色:
php artisan make:migration alter_users_add_permission --table=users
在新生成的迁移类的 up()
方法中编写代码如下:
Schema::table('users', function (Blueprint $table) {
$table->tinyInteger('permission')->after('id')->default(0);
});
我们将会为应用定义四种角色类型,它们的 permission
字段值及对应描述信息如下:
-
3
– 超级管理员,具备所有权限 -
2
– 管理员,具备后台管理权限和咖啡店增删改权限 -
1
– 商家,具备对自有咖啡店和对应公司的更新权限 -
0
– 普通用户,具备更新个人信息、喜欢及咖啡店浏览权限
所以默认用户角色是普通用户,为了简化应用流程,permission
既承担了用户角色功能,又承担了对应的权限功能,我们将基于这个字段实现简单的 RBAC 权限管理功能。
运行数据库迁移命令 php artisan migrate
应用上述数据库更新。
第二步:在模型类中定义常量属性
在 User
模型类中定义如下常量属性以便在其他地方引用:
const ROLE_GENERAL_USER = 0; // 普通用户
const ROLE_SHOP_OWNER = 1; // 商家用户
const ROLE_ADMIN = 2; // 管理员
const ROLE_SUPER_ADMIN = 3; // 超级管理员
同理,在 Action
模型类中定义如下常量属性:
const STATUS_PENDING = 0; // 待审核
const STATUS_APPROVED = 1; // 已通过
const STATUS_DENIED = 2; // 已拒绝
这样做的好处是一旦后续属性值有修改,只需要维护这一个地方就好了。
第三步:实现咖啡店策略类
我们将基于 Laravel 自带授权功能中的策略类结合用户实例上的 permission
字段实现简单的 RBAC 权限管理,所以我们要创建针对咖啡店权限的策略类:
php artisan make:policy CafePolicy
初始化新生成的策略类 app/Policies/CafePolicy.php
代码如下:
<?php
namespace App\Policies;
use App\Models\Cafe;
use App\Models\Company;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class CafePolicy
{
use HandlesAuthorization;
/**
* 创建一个新的策略类实例.
*
* @return void
*/
public function __construct()
{
//
}
/**
* 如果用户是管理员或超级管理员则可以新增咖啡店
*
* @param User $user
* @param Company $company
* @return boolean
*/
public function create(User $user, Company $company)
{
if ($user->permission == || $user->permission == 3) {
return true;
} else if ($company != null && $user->companiesOwned->contains($company->id)) {
return true;
} else {
return false;
}
}
/**
* 如果用户是管理员、超级管理员或者拥有该咖啡店所属公司则可以更新该咖啡店
*
* @param User $user
* @param Cafe $cafe
* @return boolean
*/
public function update(User $user, Cafe $cafe)
{
if ($user->permission == 2 || $user->permission == 3) {
return true;
} else if ($user->companiesOwned->contains($cafe->company_id)) {
return true;
} else {
return false;
}
}
/**
* 如果用户是管理员、超级管理员或拥有该咖啡店所属公司则可以删除该咖啡店
*
* @param User $user
* @param Cafe $cafe
* @return boolean
*/
public function delete(User $user, Cafe $cafe)
{
if ($user->permission == 2 || $user->permission == 3) {
return true;
} else if ($user->companiesOwned->contains($cafe->company_id)) {
return true;
} else {
return false;
}
}
}
然后在 app/Providers/AuthServiceProvider.php
中注册这个策略类:
protected $policies = [
Cafe::class => CafePolicy::class,
];
第四步:实现咖啡店增删改查授权功能
在应用上述策略类实现权限判断之前,需要新建一张 actions
表,用于存储待审核/已处理动作记录:
php artisan make:migration create_actions_table
编写新生成的迁移类 up
方法如下:
public function up()
{
Schema::create('actions', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->integer('company_id')->unsigned()->nullable();
$table->integer('cafe_id')->unsigned()->nullable();
$table->integer('status');
$table->integer('processed_by')->unsigned()->nullable();
$table->timestamp('processed_on')->nullable();
$table->string('type');
$table->text('content');
$table->timestamps();
});
}
运行完 php artisan migrate
命令在数据库创建 actions
数据表之后,新建 Action
模型类:
php artisan make:model Models/Action
在新生成的 app/Models/Action.php
模型类中定义关联关系如下:
// 该更新动作所属咖啡店
public function cafe()
{
return $this->belongsTo(Cafe::class, 'cafe_id', 'id');
}
// 对应前台操作用户
public function by()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
// 对应后台处理管理员
public function processedBy()
{
return $this->belongsTo(User::class, 'processed_by', 'id');
}
然后在 app/User.php
模型类中与之相对的关联关系如下:
// 该用户名下所有动作
public function actions()
{
return $this->hasMany(Action::class, 'id', 'user_id');
}
// 该用户名下所有处理的后台审核动作
public function actionsProcessed()
{
return $this->hasMany(Action::class, 'id', 'processed_by');
}
接下来,我们需要在控制器的新增、编辑、删除咖啡店方法中编写基于策略类的权限判断代码,不过,在此之前,我们先创建一个服务类 app/Services/ActionService.php
用于在 actions
表中存储待审核动作和审核通过动作,我们的处理逻辑是,对于一个拥有对应操作权限的用户来说,调用控制器的新增、编辑或删除方法会在执行对应处理后在 actions
表中插入一条已处理记录,然后返回响应给用户,而对于一个没有对应操作权限的用户而言,调用对应控制器方法则不会执行新增、编辑或删除操作,而是在 actions
表中插入一条待后台管理员审核记录,并返回响应给用户,我们将 actions
表新增记录逻辑封装到 ActionService
类中:
<?php
namespace App\Services;
use App\Models\Action;
use Illuminate\Support\Carbon;
class ActionService
{
// 创建一条待审核记录
public function createPendingAction($cafeID, $companyID, $type, $content, $userId)
{
$action = new Action();
$action->cafe_id = $cafeID;
$action->company_id = $companyID;
$action->user_id = $userId;
$action->status = Action::STATUS_PENDING;
$action->type = $type;
$action->content = json_encode($content);
$action->save();
}
// 创建一条已处理操作
public function createApprovedAction($cafeID, $companyID, $type, $content, $userId)
{
$action = new Action();
$action->cafe_id = $cafeID;
$action->company_id = $companyID;
$action->user_id = $userId;
$action->status = Action::STATUS_APPROVED;
$action->type = $type;
$action->content = json_encode($content);
$action->processed_by = $userId;
$action->processed_on = Carbon::now();
$action->save();
}
}
最后我们在控制器 app/Http/Controllers/API/CafesController.php
中修改新增、编辑、删除方法实现代码如下:
public function postNewCafe(StoreCafeRequest $request)
{
$companyID = $request->input('company_id');
$company = Company::where('id', '=', $companyID)->first();
$company = $company == null ? new Company() : $company;
$actionService = new ActionService();
if (Auth::user()->can('create', [Cafe::class, $company])) {
$cafeService = new CafeService();
$cafe = $cafeService->addCafe($request->all(), Auth::user()->id);
$actionService->createApprovedAction(null, $cafe->company_id, 'cafe-added', $request->all(), Auth::user()->id);
$company = Company::where('id', '=', $cafe->company_id)
->with('cafes')
->first();
return response()->json($company, 201);
} else {
$actionService->createPendingAction(null, $request->get('company_id'), 'cafe-added', $request->all(), Auth::user()->id);
return response()->json(['cafe_add_pending' => $request->get('company_name')], 202);
}
}
// 更新咖啡店数据
public function putEditCafe($id, EditCafeRequest $request)
{
$cafe = Cafe::where('id', '=', $id)->with('brewMethods')->with('company')->first();
if (!$cafe) {
abort(404);
}
// 保存修改之前/之后的咖啡店数据
$content['before'] = $cafe;
$content['after'] = $request->all();
$actionService = new ActionService();
if (Auth::user()->can('update', $cafe)) {
// 具备更新权限自动审核通过
$actionService->createApprovedAction($cafe->id, $cafe->company_id, 'cafe-updated', $content, Auth::user()->id);
$cafeService = new CafeService();
$updatedCafe = $cafeService->editCafe($cafe->id, $request->all(), Auth::user()->id);
$company = Company::where('id', '=', $updatedCafe->company_id)
->with('cafes')
->first();
return response()->json($company, 200);
} else {
// 不具备更新权限需要等待后台审核通过才能更新这个咖啡店
$actionService->createPendingAction($cafe->id, $cafe->company_id, 'cafe-updated', $content, Auth::user()->id);
return response()->json(['cafe_updates_pending' => $request->get('company_name')], 202);
}
}
// 删除咖啡店
public function deleteCafe($id)
{
$cafe = Cafe::where('id', '=', $id)->with('company')->first();
if (!$cafe) {
abort(404);
}
$actionService = new ActionService();
if (Auth::user()->can('delete', $cafe)) {
// 具备删除权限自动审核通过
$actionService->createApprovedAction($cafe->id, $cafe->company_id, 'cafe-deleted', '', Auth::user()->id);
$cafe->delete();
return response()->json(['message' => '删除成功'], 204);
} else {
// 不具备删除权限需要等后台审核通过后才能执行删除操作
$actionService->createPendingAction($cafe->id, $cafe->company_id, 'cafe-deleted', '', Auth::user()->id);
return response()->json(['cafe_delete_pending' => $cafe->company->name], 202);
}
}
如果没有对应权限,则分别返回 cafe_add_pending
、cafe_updates_pending
、cafe_delete_pending
字段交由前端判断并提示用户,至此,Laravel 后端接口部分就已经完成了,接下来我们要修改前端代码了。
第五步:修改前端代码
最后我们在前端 resources/assets/js/modules/cafes.js
文件中调用后端接口成功后的相应 Action 处理做优化:
addCafe({commit, state, dispatch}, data) {
commit('setCafeAddStatus', 1);
CafeAPI.postAddNewCafe(data.company_name, data.company_id, data.company_type, data.subscription, data.website, data.location_name, data.address, data.city, data.state, data.zip, data.brew_methods, data.matcha, data.tea)
.then(function (response) {
if (typeof response.data.cafe_add_pending !== 'undefined') {
// 没有新增权限提示文本
commit('setCafeAddedText', response.data.cafe_add_pending + ' 审核通过后才能添加!');
} else {
commit('setCafeAddedText', response.data.name + ' 已经添加!');
}
commit('setCafeAddStatus', 2);
commit('setCafeAdded', response.data);
dispatch('loadCafes');
})
.catch(function () {
commit('setCafeAddStatus', 3);
});
},
editCafe({commit, state, dispatch}, data) {
commit('setCafeEditStatus', 1);
CafeAPI.putEditCafe(data.id, data.company_name, data.company_id, data.company_type, data.subscription, data.website, data.location_name, data.address, data.city, data.state, data.zip, data.brew_methods, data.matcha, data.tea)
.then(function (response) {
if (typeof response.data.cafe_updates_pending !== 'undefined') {
// 没有修改权限提示文本
commit('setCafeEditText', response.data.cafe_updates_pending + ' 审核通过才能更新!');
} else {
commit('setCafeEditText', response.data.name + ' 已经编辑成功!');
}
commit('setCafeEditStatus', 2);
dispatch('loadCafes');
})
.catch(function (error) {
commit('setCafeEditStatus', 3);
});
},
deleteCafe({commit, state, dispatch}, data) {
commit('setCafeDeleteStatus', 1);
CafeAPI.deleteCafe(data.id)
.then(function (response) {
if (typeof response.data.cafe_delete_pending !== 'undefined') {
// 没有删除权限提示文本
commit('setCafeDeletedText', response.data.cafe_delete_pending + ' 审核通过才能删除!');
} else {
commit('setCafeDeletedText', '咖啡店删除成功!');
}
commit('setCafeDeleteStatus', 2);
dispatch('loadCafes');
})
.catch(function () {
commit('setCafeDeleteStatus', 3);
});
},
在新增咖啡店表单页面组件 resources/assets/js/pages/NewCafe.vue
中,添加咖啡店成功后触发通知的代码片段如下(对应代码之前都已经存在了):
addCafeText(){
return this.$store.getters.getCafeAddText;
}
...
EventBus.$emit('show-success', {
notification: this.addCafeText
});
同理,在编辑咖啡店表单页面组件 resources/assets/js/pages/EditCafe.vue
中,编辑/删除咖啡店成功后对应触发通知的代码片段如下(同上,对应代码之前都已经存在了):
editCafeText() {
return this.$store.getters.getCafeEditText;
},
...
EventBus.$emit('show-success', {
notification: this.editCafeText
});
以及:
deleteCafeText() {
return this.$store.getters.getCafeDeletedText;
}
...
EventBus.$emit('show-success', {
notification: this.deleteCafeText
});
运行 npm run dev
重新编译前端资源,此时,应用默认用户的 permission
字段应该 0
,不具备新增、修改、删除咖啡店权限,此时试着去更新咖啡店,页面顶部弹出通知如下:
同时在 actions
表中会生成一条 status=0
的待审核记录:
至此,我们就完成了一个简单的基于 RBAC 的咖啡店增删改查权限管理功能,下一篇教程我们将在此授权功能基础上进行管理后台的开发工作,敬请期待。
2 Comments
学院君是自己写的权限管理么?
基于 Laravel 自带的授权功能加上用户角色做判断