前言
一个基本的 ACCESS CONTROL LIST 会基于 “用户”、“角色”、“权限”三个模型:
1.“用户”不直接获得“权限”,而是通过“用户”所属“角色”来继承该“角色”所拥有的“权限”;
2.“角色”直接获得“权限”。
用户(USER)
用户模型,仅仅用于 AUTH 认证,最常见的是“登录校验”这块。例如一个 CMS 管理系统后台,USER 最常见的字段有 username
、password
等等:
角色(ROLE)
角色模型,是系统为某个权限集合所定义的“权限集合”名称,你可以理解为“权限组”。
例如“运营员”,你可以将“增加商品”、“修改商品”、“查看商品”、“修改菜单”这四个“权限”作为一个“权限组”定义为“运营员”角色。然后将某个“用户”定义为“运营员”角色,则该“用户”即可继承“运营员”获得这三个“权限”。
再例如“仓库管理员”,你可以将订单相关的“权限组”定义为该角色等等。
有个特例是“超级管理员”,众所周知,这个角色,通常在系统中默认拥有全部权限,所以,这类角色,通常直接放在“用户”模型比较好。在程序中便可硬编码,判断该“用户”是否为超级管理,若为真则可不需要进行权限的判断了,在扩展方面会更好些。
权限(PERMISSION)
权限模型,是针对资源进行数据库操作时所定义的一些“操作名称”,例如对“商品”这个资源,常见的便是“增删改查”,更细化可以有“更换商品图片”等针对某些字段级别的资源操作。
用户 & 角色(Relation-Ship)
MANY-TO-MANY
角色 & 权限(Relation-Ship)
MANY-TO-MANY
模型 ER 图
模拟数据
- 用户
user_admin
为admin
角色,该角色拥有四个权限:
create|delete|update|select product
- 用户
user_staff
为staff
角色,该角色拥有三个权限:
create|update|select| product
- 用户
user_visitor
为visitor
角色,该角色拥有一个权限:
select product
Table Data
mysql> SELECT * FROM auth_user;
+----+--------------+--------------+
| id | username | password |
+----+--------------+--------------+
| 1 | user_admin | user_admin |
| 2 | user_staff | user_staff |
| 3 | user_visitor | user_visitor |
+----+--------------+--------------+
3 rows in set
mysql> SELECT * FROM auth_role;
+----+---------+
| id | name |
+----+---------+
| 1 | admin |
| 2 | staff |
| 3 | visitor |
+----+---------+
3 rows in set
mysql> SELECT * FROM auth_permission;
+----+----------------+
| id | name |
+----+----------------+
| 1 | create product |
| 2 | delete product |
| 3 | update product |
| 4 | select product |
+----+----------------+
4 rows in set
mysql> SELECT * FROM auth_user_role;
+---------+---------+
| role_id | user_id |
+---------+---------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+---------+---------+
3 rows in set
mysql> SELECT * FROM auth_role_permission;
+---------+---------------+
| role_id | permission_id |
+---------+---------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 4 |
| 2 | 1 |
| 2 | 4 |
| 2 | 3 |
| 3 | 4 |
+---------+---------------+
8 rows in set
常见的“权限”校验方法:
-
硬编码在每一个视图或某个业务方法中;
-
写在中间件中,根据“权限”所属的资源,如路由等,来进行校验。
封装用户权限校验
<?php
namespace App\Models;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable {
use Notifiable;
protected $hidden = [
'password', 'remember_token',
];
public function roles()
{
return $this->belongsToMany(Role::class);
}
public function hasPermission($permissionName)
{
foreach ($this->roles as $role) {
if ($role->permisssions()->where('name', $permissionName)->exists()) {
return true;
}
}
return false;
}
}
硬编码
例如,当用户进行“删除或创建商品”操作时,可以直接在控制器方法中进行限制:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
class ProductsController extends Controller {
public function store(Request $request)
{
// 判断当前登录的用户是否有权限
if (!$request->user()->hasPermission('create product')) {
abort(403);
}
// do something
return back()->with('status', '添加商品成功');
}
public function destroy(Product $product)
{
// 判断当前登录的用户是否有权限
if (!$request->user()->hasPermission('delete product')) {
abort(403);
}
// do something
return back()->with('status', '删除商品成功');
}
}
中间件
“硬编码”会因为路由的增加而使维护、扩展越来越麻烦,因此,通常将平行权限,或需要特定判断的业务才硬编码到视图逻辑中。
现在,我们往“权限”表中,加入路由字段,进行基于路由校验权限的扩展:
mysql> SELECT * FROM auth_permission;
+----+----------------+----------------+
| id | name | route |
+----+----------------+----------------+
| 1 | create product | product.create |
| 2 | delete product | product.delete |
| 3 | update product | product.update |
| 4 | select product | product.select |
+----+----------------+----------------+
4 rows in set
之后,我们便可在诸如Laravel 中间件、Codeigiter 钩子函数等,拦截请求,进行权限校验:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Route;
use App\Models\Permission;
class PermissionAuth {
public function handle($request, Closure $next) {
$route = Route::currentRouteName();
// 判断权限表中这条路由是否需要验证
if ($permission = Permission::where('route', $route)->first()) {
// 当前用户不拥有这个权限的名字
if (!auth()->user()->hasPermission($permission->name)) {
return response()->view('errors.403', [
'status' => "权限不足,需要:{$permission->name}权限"
]);
}
}
return $next($request);
}
}