最近需要在 Lumen 框架中构建一个 RBAC 系统,刚开始找了几个 composer 扩展,但体验了一下始终觉得不甚流畅。主要是由于我们的系统并不大,不需要那么大而全的 RBAC。在后端扛把子的帮助下,我也终于理清了思路,自己实现了一套非常简单的 RBAC 逻辑。

分析

当访问某个路由时,判断当前访问的用户对应哪个角色,再判断该角色是否能够有进入此路由的权限。因此:

  • 需要建立 users 表,roles 表,permissions 表和 permission_role 表。permissions 表存储路由,permission_role 表存储角色拥有的路由权限
  • 需要鉴权的路由需要经过一个中间件去管理当前用户是否拥有访问的权限
  • 用户鉴权通过后需要存储当前访问的角色信息,以便后续代码根据不同的角色执行不同的动作

建立迁移文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// create_users_table
$table->increments('id');
$table->unsignedInteger('role_id')->comment('用户的角色 id');
$table->string('name', 24)->comment('用户姓名');
$table->string('phone', 11)->unique()->comment('用户手机号');
$table->string('password', 32)->comment('用户密码');
$table->timestamps();

// create_roles_table
$table->increments('id');
$table->string('name', 24)->comment('角色的英文名称');
$table->string('alias', 24)->comment('角色的中文名称');
$table->timestamps();

// create_permissions_table
$table->increments('id');
$table->string('route', 256)->comment('路由 url');
$table->string('desc', 32)->comment('路由的描述');
$table->timestamps();

// create_permission_role_table
$table->increments('id');
$table->unsignedInteger('role_id')->comment('角色 id');
$table->unsignedInteger('permission_id')->comment('权限 id');
$table->timestamps();

建立模型关联

app/Models/User.php

1
2
3
4
public function role()
{
return $this->belongsTo('App\Models\Role');
}

app/Models/Role.php

1
2
3
4
5
6
7
8
9
public function users()
{
return $this->hasMany('App\Models\User');
}

public function permissions()
{
return $this->belongsToMany('App\Models\Permission')->withTimestamps();
}

编写鉴权中间件

app/Http/Middleware/CheckPermission.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace App\Http\Middleware;

use App\Models\Role;
use App\Models\User;
use Closure;

class CheckPermission
{
public function handle($request, Closure $next)
{
if (!$request->has('user_id')) {
return response()->json(['status' => 403, 'msg' => '请携带 user_id 参数'], 403);
}

// 获取当前用户信息
$user = User::find($request->user_id);
// 获取当前用户可以进入的路由
$availableRoutes = Role::with('permissions:route')->find($user->role_id)->permissions->toArray();

// 如果当前路由在权限内,可以进入
return in_array($request->path(), array_column($availableRoutes, 'route'))
? $next($request)
: response()->json(['status' => 403, 'msg' => '您没有访问权限'], 403);
}
}

注册鉴权中间件

此处以 Lumen 举例。在 bootstrap/app.php 中添加代码

1
2
3
$app->routeMiddleware([
'check_permission' => App\Http\Middleware\CheckPermission::class,
]);

记录授权后的用户信息和角色信息

如果使用 laravel 原生的授权方法,可以根据需求修改 app/Providers/AuthServiceProvider.php

还可以在 app/Http/Controllers/Controller.php 中的 __construct 方法中手动记录。

1
2
3
4
5
6
7
8
9
10
protected $currentUser;
protected $currentRole;

public function __construct(Request $request)
{
if ($request->has('user_id')) {
$this->currentUser = User::with('role')->find($request->user_id);
$this->currentRole = $this->currentUser->role->name;
}
}

可优化点

  • 目前通过读取 Request $request 中的 user_id 实现用户鉴权,这是由于本项目的架构所限,但实际上可以直接使用 Laravel 的 Token 机制
  • 可以使用 Laravel 自带的授权和策略功能,比如获取用户信息只需要使用 $request->user() 即可,不需要在 Controller.php__construct 中手动查询用户信息
  • rolespermissions 表一般情况下读操作远大于写操作,因此非常适合将表数据进行缓存,从而尽可能减少从数据库中的查询
  • permissions 中目前仅存储了路由名称作为权限的判断条件,但假如使用标准的 restful 风格 api,那么 get /resourcesPOST /resources 在鉴权时将视为同一个路由。因此应当在表中存入更多的 request 信息以做区分