tp5-rbac
插件的使用
背景说明:
- 前后端分离架构,前端使用
Vue
,后端使用Thinkphp5.1.*
- 登录认证使用
JWT
- 权限认证使用
rbac
- 接口采用
RESFUL
风格需求说明:
- 有两种类型的用户,一个是管理员
test_admin
,一个是普通用户test_user
- 有两个接口,一个是商品列表
test/spus
,一个是商品详情test/spu/:spu_id
- 管理员只能访问商品列表接口,普通用户只能访问商品详情接口
准备工作
安装
thinkphp5.1.*
composer create-project topthink/think=5.1.* tp5
安装
tp5-rbac
composer require gmars/tp5-rbac
根据自己的业务逻辑修改
tp5-rbac
权限验证默认表
/* vendor/gmars/tp5-rbac/gmars_rbac.sql */
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `###permission_category`;
CREATE TABLE `###permission_category` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '权限分组名称',
`description` varchar(200) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '权限分组描述',
`status` smallint(4) unsigned NOT NULL DEFAULT '1' COMMENT '权限分组状态1有效2无效',
`create_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '权限分组创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='权限分组表';
DROP TABLE IF EXISTS `###permission`;
CREATE TABLE `###permission` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '权限节点名称',
`type` smallint(4) unsigned NOT NULL DEFAULT '0' COMMENT '权限类型1后台2前端PC3前端MOBILE',
`category_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '权限分组id',
`path` varchar(100) NOT NULL DEFAULT '' COMMENT '权限路径',
`path_id` varchar(100) NOT NULL DEFAULT '' COMMENT '路径唯一编码',
`description` varchar(200) NOT NULL DEFAULT '' COMMENT '描述信息',
`status` smallint(4) unsigned NOT NULL DEFAULT '0' COMMENT '状态0未启用1正常',
`create_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `index_path_id` (`path_id`),
KEY `index_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限节点';
DROP TABLE IF EXISTS `###role`;
CREATE TABLE `###role` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '角色名',
`description` varchar(200) NOT NULL DEFAULT '' COMMENT '角色描述',
`status` smallint(4) unsigned NOT NULL DEFAULT '0' COMMENT '状态1正常0未启用',
`sort_num` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '排序值',
PRIMARY KEY (`id`),
KEY `index_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色';
DROP TABLE IF EXISTS `###role_permission`;
CREATE TABLE `###role_permission` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`role_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '角色编号',
`permission_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '权限编号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限对应表';
DROP TABLE IF EXISTS `###user`;
CREATE TABLE `###user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名',
`phone` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号码',
`pwd` varchar(64) NOT NULL DEFAULT '' COMMENT '用户密码',
`last_login_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最后一次登录时间',
`last_login_ip` varchar(20) NOT NULL DEFAULT '0.0.0.0' COMMENT '最后一次登录IP',
`status` smallint(4) unsigned NOT NULL DEFAULT '0' COMMENT '状态0禁用1正常',
`create_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '账号创建时间',
`is_delete` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_phone` (`phone`),
KEY `index_name` (`name`),
KEY `index_phone` (`phone`),
KEY `index_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
DROP TABLE IF EXISTS `###user_role`;
CREATE TABLE `###user_role` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
`role_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色对应关系'
修改
tp5-rbac
创建表时的BUG
/* vendor/gmars/tp5-rbac/src/CreateTable.php */
// 第57行
// $prefix = empty($prefix)? '' : $prefix . '_';
// 修改为:
// $prefix = empty($prefix)? '' : $prefix;
配置工作
设置全局的
rbac
对象
<?php
namespace app\index\controller;
use gmars\rbac\Rbac;
use think\Controller;
class Index extends Controller {
protected $rbac;
public function initialize() {
$this->rbac = new Rbac();
}
}
初始化
rbac
所需的表
// 可传入参数$db为数据库配置项默认为空则为默认数据库(考虑到多库的情形)
// 该方法会生成rbac所需要的表,一般只执行一次,为了安全,执行后会加锁,下次要执行需要删除锁文件再执行
$this->rbac->createTable();
创建用户
$model_user = model('user');
$db_data = [
[
'name' => 'test_admin',
'phone' => '18888888888',
'pwd' => md5('123456'),
'status' => 1,
'create_time' => time(),
], [
'name' => 'test_user',
'phone' => '16666666666',
'pwd' => md5('123456'),
'status' => 1,
'create_time' => time(),
],
];
$user_info = $model_user
->saveAll($db_data);
dump($user_info);
创建权限分组
// 编辑和修改调用同一个方法编辑时请在参数中包含主键id的值
$res = $this->rbac->savePermissionCategory([
'name' => '管理员组',
'description' => '管理员组',
'status' => 1,
'create_time' => time(),
]);
dump($res);
$res = $this->rbac->savePermissionCategory([
'name' => '普通用户组',
'description' => '普通用户组',
'status' => 1,
'create_time' => time(),
]);
dump($res);
创建权限节点
// 如果为修改则在传入参数数组中加入主键id的键值
// type为权限类型1为后端权限2为前端PC权限3为前端MOBILE权限
// category_id为权限分组的id
// 创建成功返回添加的该条权限数据,错误抛出异常
$res = $this->rbac->createPermission([
'name' => '商品列表',
'type' => 1,
'category_id' => 1,
'path' => 'index/getskus',
'description' => '商品列表',
'status' => 1,
'create_time' => time(),
]);
dump($res);
$res = $this->rbac->createPermission([
'name' => '商品详情',
'type' => 1,
'category_id' => 1,
'path' => 'undex/getku',
'description' => '商品详情',
'status' => 1,
'create_time' => time(),
]);
dump($res);
创建角色&给角色分配权限
// 如果修改请在第一个参数中传入主键的键值
// 第二个参数为权限节点的id拼接的字符串请使用英文逗号
$res = $this->rbac->createRole([
'name' => '管理员',
'description' => '管理员',
'status' => 1,
], '1');
dump($res);
$res = $this->rbac->createRole([
'name' => '普通用户',
'description' => '普通用户',
'status' => 1,
], '1');
dump($res);
给用户分配角色
// 该方法会删除用户之前被分配的角色
// 第一个参数为用户id
// 第二个参数为角色id的数组
$res = $this->rbac->assignUserRole(1, [1]);
dump($res);
$res = $this->rbac->assignUserRole(2, [2]);
dump($res);
效果测试
定义接口和权限验证
<?php
namespace app\index\controller;
use gmars\rbac\Rbac;
use think\Controller;
class Index extends Controller {
protected $rbac;
protected $user_id;
public function initialize() {
$this->rbac = new Rbac();
$controller_name = strtolower(request()->controller());
$model_name = strtolower(request()->action());
$url = $controller_name . '/' . $model_name;
// 白名单,不需要进行验证的路径列表
$white_list = [
'index/getlogin',
'index/index',
];
if (!in_array($url, $white_list)) {
try {
// 验证权限
$res = $this->rbac->can($url);
// 获取用户ID
$config_rbac = config('rbac');
$token_key = $config_rbac['token_key'];
$token = request()->header($token_key);
$permission_list = cache($token);
$this->user_id = $permission_list[$url]['user_id'];
} catch (\Throwable $th) {
json(['errno' => 2, 'msg' => $th->getMessage()])->send();exit;
}
if (false === $res) {
json(['errno' => 2, 'msg' => '无权限访问'])->send();exit;
}
}
}
public function index() {
return 'hello world';
}
public function getLogin() {
$model_user = model('user');
try {
$user_info = $model_user
->field('id,name,phone,last_login_time,last_login_ip,create_time')
->where(['phone' => '18888888888', 'pwd' => md5('123456')])
->find();
} catch (\Throwable $th) {
return json(['errno' => 1, 'msg' => '数据库错误']);
}
if (is_null($user_info)) {
return json(['errno' => 1, 'msg' => '该用户不存在']);
}
try {
// 获取token信息
// 第一个参数为登录的用户id
// 第二个参数为token有效期默认为7200秒
// 第三个参数为token前缀
$token_info = $this->rbac->generateToken($user_info->id, 7 * 24 * 3600);
} catch (\Throwable $th) {
return json(['errno' => 1, 'msg' => '数据库错误']);
}
$resp = [
'user_id' => $user_info->id,
'user_name' => $user_info->name,
'user_phone' => $user_info->phone,
'last_login_time' => date('Y-m-d H:i:s', $user_info->last_login_time),
'last_login_ip' => $user_info->last_login_ip,
'create_time' => date('Y-m-d H:i:s', $user_info->create_time),
'token' => $token_info['token'],
];
$user_info->last_login_time = time();
$user_info->last_login_ip = request()->ip();
try {
$user_info->save();
} catch (\Throwable $th) {
return json(['errno' => 1, 'msg' => '数据库错误']);
}
return json(['errno' => 0, 'msg' => '登录成功', 'data' => $resp]);
}
public function getSkus() {
return json(['errno' => 0, 'msg' => '仅test_admin用户可访问']);
}
public function getSku($sku_id) {
return json(['errno' => 0, 'msg' => '仅test_user用户可访问', 'data' => ['sku_id' => $sku_id]]);
}
}
访问接口
- 访问登录接口拿到
token
值 - 前端使用
ajax
分别请求test/skus
接口和test/sku/1
接口 - 前端发送
ajax
请求时,需要在headers
中添加以Authorization
为键,以token
为值的键值对
其它操作
获取权限分组列表
// 参数支持传入id查询单条数据和标准的where表达式查询列表传为空数组则查询所有
$this->rbac->getPermissionCategory([['status', '=', 1]]);
获取权限列表
// 参数支持传入id查询单条数据和标准的where表达式查询列表传为空数组则查询所有
$this->rbac->getPermission([['status', '=', 1]]);
获取角色列表
// 第一个参数支持传入id查询单条数据和标准的where表达式查询列表传为空数组则查询所有
// 第二个参数选择是否查询角色分配的所有权限id默认为true
$this->rbac->getRole([], true);
删除权限分组
// 参数支持传入单个id或者id列表
$this->rbac->delPermissionCategory([1,2,3,4]);
删除权限
// 参数支持传入单个id或者id列表
$this->rbac->delPermission([1,2,3,4]);
删除角色
// 参数支持传入单个id或者id列表
// 删除角色会删除给角色分配的权限[关联关系]
$this->rbac->delRole([1,2,3,4]);
使用refresh_token刷新权限
$this->rbac->refreshToken('17914241bde6bfc46b20e643b2c58279');
验证流程
user
表 =>role
表 =>role_permission
表 =>permission
表登录成功后,
rbac
会根据随机数配合时间戳生成token
,将token
作为键,将有效期作为值存入缓存根据用户id查询
permission
,得到一个以path
为键以权限详情为值的权限数组将
token
作为键,将权限数组作为值存入缓存访问接口时,根据前台传递的
token
,获取缓存中的token
,若获取不到则登录过期从缓存中获取到
token
后,再根据token
从缓存中获取权限数组,若获取不到则登录过期从缓存中获取到权限数组后,会判断权限数组中是否包含需要验证的
URL
,若没有则无权限如果包含,则权限验证通过
遇到的坑
- 路由需要设置跨域,要不然会接收不到
headers
中Authorization
的值 -
rbac
最根本的验证方式就是判断路径是否完全相等,因此无法使用路由作为路径存入数据库 -
request->controller()
获取的值是类似于Index
的,大小写没有转换 -
request()->action()
获取的值是类似于getskus
的,大写全部转换成了小写 - 路径存入数据库的时候需要全部转换为小写字母
-
rbac
只有在登录成功,设置token
的时候才查询数据库,其他都是操作缓存 - 修改数据库中权限相关的表后,需要删除本地缓存,否则有可能会照成修改不生效