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对象

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);

效果测试

定义接口和权限验证

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官方文档

你可能感兴趣的:(thinkphp5.1中使用tp5-rbac插件)