thinkphp5.1中使用tp5-rbac插件

tp5-rbac插件的使用

背景说明:

  1. 前后端分离架构,前端使用Vue,后端使用Thinkphp5.1.*
  2. 登录认证使用JWT
  3. 权限认证使用rbac
  4. 接口采用RESFUL风格

需求说明:

  1. 有两种类型的用户,一个是管理员test_admin,一个是普通用户test_user
  2. 有两个接口,一个是商品列表test/spus,一个是商品详情test/spu/:spu_id
  3. 管理员只能访问商品列表接口,普通用户只能访问商品详情接口

准备工作

安装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]]);
    }
}

访问接口

  1. 访问登录接口拿到token
  2. 前端使用ajax分别请求test/skus接口和test/sku/1接口
  3. 前端发送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,若没有则无权限

  • 如果包含,则权限验证通过

遇到的坑

  • 路由需要设置跨域,要不然会接收不到headersAuthorization的值
  • rbac最根本的验证方式就是判断路径是否完全相等,因此无法使用路由作为路径存入数据库
  • request->controller()获取的值是类似于Index的,大小写没有转换
  • request()->action()获取的值是类似于getskus的,大写全部转换成了小写
  • 路径存入数据库的时候需要全部转换为小写字母
  • rbac只有在登录成功,设置token的时候才查询数据库,其他都是操作缓存
  • 修改数据库中权限相关的表后,需要删除本地缓存,否则有可能会照成修改不生效

参考文档

gmars/tp5-rbac官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 理工寝室商店-微信小程序 疑问小结 当时在XAMMP下mysql目录下的bin下 php -v 不起作用.到ph...
    这个超人不会飞阿阅读 1,692评论 1 1
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,160评论 1 23
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,849评论 6 13
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • 一整天的轰炸街道各个角落,遍地都是梵音单页,这说明,我们的计划,一天天的在实现,我们也一天天的在成长,生活就是这样...
    国民小扎西阅读 267评论 0 3