在后台开发中,对管理员的权限组(角色)的管理,以及对应的菜单按权限加载是基本的需求,之前基于 TP5 框架参考 Auth 类在上一家公司将这套系实践上线,目前由于更换工作,新公司使用的是 CI 框架,后台的这个需求依然存在,于是在 CI 上又实践一次,记录一下。

预期
这套简易的权限系统达到的预期效果:对每个管理员可以指定多个角色,每个角色分配不同的权限,权限对应每个控制器的每个对外调用的方法,菜单的跳转与权限挂钩,后台菜单根据管理员拥有的权限来加载,从而实现管理员=>多角色=>权限=>菜单的后台权限菜单管理系统。
环境:CodeIgniter 3.1.9 + PHP 7.2 + MariaDB 10.1.36
数据表:
管理员表:
CREATE TABLE `admin` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(100) NOT NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码',
  `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态',
  `createtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '添加时间',
  `updatetime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='管理员表';
权限表:
CREATE TABLE `auth_rule` (
  `name` varchar(200) NOT NULL DEFAULT '' COMMENT '验证规则',
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(200) DEFAULT '' COMMENT '权限名称',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1正常0关闭',
  `cat_id` tinyint(2) NOT NULL DEFAULT '1' COMMENT '权限分类',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='权限表';
权限分类表:
CREATE TABLE `auth_rule_cat` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(200) DEFAULT '' COMMENT '权限分类名称',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1正常0删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='权限分类表';
角色组表:
CREATE TABLE `et_auth_group` (
  `id` int(4) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(200) NOT NULL DEFAULT '' COMMENT '角色名',
  `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1正常0禁用',
  `rules` text NOT NULL COMMENT '权限id,英文逗号间隔',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色组';
用户角色关系表:
CREATE TABLE `auth_group_access` (
  `uid` int(10) unsigned NOT NULL COMMENT '管理员id',
  `group_id` int(10) unsigned NOT NULL COMMENT '角色组id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色关系表';
后台菜单表:
CREATE TABLE `et_auth_menu` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `icon` varchar(50) DEFAULT '' COMMENT '菜单字体图标icon',
  `title` varchar(100) NOT NULL DEFAULT '' COMMENT '菜单的中文名称',
  `rule_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '对应的权限id',
  `pid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '菜单pid所属分类父级id,0表示顶级',
  `url` varchar(100) DEFAULT '' COMMENT '菜单对应的uri',
  `et_order` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '排序',
  `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '1正常0删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='后台菜单';
实现
对应数据表的增删改查就是基本的条件,这里只讲一下权限验证以及菜单加载的实现。
添加需要验证的权限:
封装验证类
在 libraries/common 下添加文件 Authhandler.php ,代码:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
/**
 * 权限验证
 * hsu1943 20181112
 */
class Authhandler
{
    protected $_CI;
    // 默认配置
    public $_config = array(
        'AUTH_ON'               =>  true,       // 验证开关
        'SUPER_ADMINISTRATOR'   =>  'admin',    // 超级管理员用户名
    );
    public function __construct()
    {
        $this->_CI =& get_instance();
    }
    /**
     * 权限验证
     * @param  int          $uid        用户id
     * @param  array        $names      待验证的权限,支持逗号分隔的权限规则或索引数组
     * @param  string       $relation   待验证权限是否都需要满足,'or'|'and'
     * @return boolean                  true|false
     */
    public function check($uid, $names, $relation = 'or')
    {
        // 关闭验证跳过
        if (!$this->_config['AUTH_ON']) {
            return true;
        }
        // 超管跳过
        if($_SESSION['admin']['username'] === $this->_config['SUPER_ADMINISTRATOR'])
        {
            return true;
        }
        // 获取用户权限
        $authList = $this->getAuthList($uid);
        // 待验证权限
        if (is_string($names)) {
            $names = strtolower($names);
            if (strpos($names, ',') !== false) {
                $names = explode(',', $names);
            } else {
                $names = array($names);
            }
        }
        // 通过验证的权限数组
        $list = [];
        foreach ($names as $name) {
            if(in_array($name, $authList))
            {
                $list[] = $name;
            }
        }
        if ('or' == $relation and !empty($list)) {
            return true;
        }
        $diff = array_diff($names, $list);
        if ('and' == $relation and empty($diff)) {
            return true;
        }
        return false;
    }
    /**
     * 获取用户所有角色组下的权限name集合
     * @param  int      $uid        管理员id
     * @return array    $authList   权限name集合
     */
    public function getAuthList($uid)
    {
        $this->_CI->load->library('mysql/authdb');
        $groups = $this->_CI->authdb->getGroupsByUID($uid);
        $ids = [];
        foreach ($groups as $g) {
           $ids = array_merge($ids, explode(',', trim($g['rules'], ',')));
        }
        $ids = array_unique($ids);
        $rules = $this->_CI->authdb->getRulesByIds($ids);
        $_SESSION['admin']['AUTH_'.$uid] = $rules;
        $authList = array_column($rules, 'name');
        return $authList;
    }
}
公共代码中添加验证
这里的公共代码在CI框架中有多重选择,比如 post_controller_constructor 钩子中验证,或者控制器的核心父类 core/MY_admin_controller.php 中。
我选择在后者,在公共父类中验证用户 SESSION 通过后做验证:
// 验证权限
    $url_name = 'admin/' . $this->router->fetch_class() . '/' . $this->router->fetch_method(); 
    $this->load->library('common/authhandler');
    $check = $this->authhandler->check($_SESSION['admin']['id'], $url_name);
    if(!$check){
        show_error('Sorry, Permission denied!');
    }
添加权限菜单
添加进来的权限菜单类似这样的:

菜单对应的 跳转URL 实际上跟权限中的规则是一一对应的,根据这个可以检索到对应的权限与菜单绑定。
根据用户权限加载对应菜单
用户登录成功,查询用户对应权限组,合并权限组拥有的权限,去重后得到该用户所拥有的全部权限,根据权限查询对应的菜单,然后在模版公共模块中循环加载出来就行了。

千里之外不离开,一直在用thinkphp
thinkphp不错的,目前中小企业用得最多的框架