tp5.1 权限模块 -- 权限实现

思路:用户登录验证的时候将用户权限路由存储session,定义方法执行前行为,行为验证当前访问的方法路由是否属于用户所拥有的权限路由。

一、在制作站点权限之前我们需要准备好所需的数据表,数据表如下:(各表字段可酌情加减,视各自站点逻辑而定)
1.admin -- 管理员账户表

image.png

2.role -- 角色表
image.png

3.admin_role -- 管理员角色关系表
image.png

4.role_action -- 角色方法表
image.png

5.admin_action -- 管理员特殊方法表(酌情添加)
image.png

6.action -- 方法表(方法数据通过站点采集而来。ps 采集方法,附上链接:https://www.jianshu.com/p/5e013fcb19aa
image.png

二、数据表大致准备如上,接下来进行我们的权限验证流程:
1.应用目录下 tags.php(应用行为扩展定义文件) 定义应用操作执行前行为。ps:关于行为,笔者之前讲过:https://www.jianshu.com/p/5bb4ec8189f7

<?php
// 应用行为扩展定义文件
return [
    // 应用初始化
    'app_init'     => [],
    // 应用开始
    'app_begin'    => [],
    // 模块初始化
    'module_init'  => [],
    // 操作开始执行  注:笔者在 OperateBehavior 中进行权限验证
    'action_begin' => ['app\\behavior\\OperateBehavior','app\\behavior\\AccessBehavior'],
    // 视图内容过滤
    'view_filter'  => [],
    // 日志写入
    'log_write'    => [],
    //日志写入完成
    'log_write_done' => [],
    // 应用结束
    'app_end'      => ['app\\behavior\\LogBehavior'],
];

2.定义OperateBehavior.php行为文件,进行权限验证

image.png

OperateBehavior.php中代码如下:

<?php
namespace app\behavior;
use think\Db;
use think\facade\Log;
use think\facade\Session;
use think\Request;
use think\Exception;
use app\facade\ActionModel;
use think\Controller;

class OperateBehavior extends Controller
{
    // 定义需要排除的权限路由
    protected $exclude = [
        'index/index/index',
        'admin/login/index',
        'admin/login/loginverify',
        'admin/login/outlogin',
        'admin/login/iebrowsernocompat',
        'admin/index/index',
        'admin/index/welcome',
        'mobile/login/ajaxpswlogin',
        'mobile/login/ajaxsmslogin',
        'mobile/login/sendsms',
        'mobile/login/ajaxupdatepsw',
        'mobile/login/ajaxisregister',
        'mobile/login/ajaxregister'
    ];

    // 定义未登陆需要排除的权限路由
    protected $login = [
        'admin/login/index',
        'admin/login/loginverify',
        'admin/login/iebrowsernocompat',
        'admin/index/welcome',
        'mobile/login/ajaxpswlogin',
        'mobile/login/ajaxsmslogin',
        'mobile/login/sendsms',
        'mobile/login/ajaxupdatepsw',
        'mobile/login/ajaxisregister',
        'mobile/login/ajaxregister'
    ];

    // 定义不需要检测权限的模块
    protected $moudel = ['union','mobile'];

    /**
     * 权限验证
     * @param Request $Request
     */
    public function run(Request $Request)
    {
        // 行为逻辑
        try {
            // 获取当前访问路由
            $url  = $this->getActionUrl($Request);

            if(empty(Session::get()) && !in_array($url,$this->login) && !in_array(strtolower($Request->module()), $this->moudel)){
                $this->error('请先登录1','/login/index');
            }

            // 用户所拥有的权限路由
            $auth = Session::get('auth.url')?Session::get('auth.url'):[];

            if(!$auth && !in_array($url,$this->login) && !in_array($url, $this->exclude) && !in_array(strtolower($Request->module()), $this->moudel)){
                $this->error('请先登录2','/login/index');
            }

            if(!in_array($url, $auth) && !in_array($url, $this->exclude) && !in_array(strtolower($Request->module()), $this->moudel)){
                $this->error('无权限访问1');
            }

            // ↓↓↓ 接下来是关于日志的操作 酌情添加 ↓↓↓
            $actInfo  = ActionModel::getActionNameByUrl($url);
            $userInfo = Session::get('user_info')?Session::get('user_info'):[];
            if(!$userInfo && !in_array($url,$this->login) && !in_array($url, $this->exclude) && !in_array(strtolower($Request->module()), $this->moudel)){
                $this->error('请先登录3','/login/index');
            }
            $userId  = isset($userInfo['admin_id'])?$userInfo['admin_id']:0;
            $logData = array(
                'uuid' => $userId,
                'url'  => $url,
                'desc' => $actInfo['action_name'],
                'action_id' => $actInfo['action_id']
            );
            $Log       = Db::connect('db_config_log');
            $prefix    = config('database.db_config_log.prefix');
            $dataBase  = config('database.db_config_log.database');
            $tableName = $prefix.'log_'.date('Ymd',time());
            //判断是否存在当日的日志表
            $sql   = "SELECT COUNT(*) count FROM information_schema.tables WHERE table_schema = '$dataBase' AND table_name = '$tableName'";
            $count = Db::query($sql);
            $count = !empty($count)?reset($count)['count']:0;
            if(!$count){//如果不存在则创建当日日志表
                $Log->execute('create table '.$tableName.' like '.$prefix.'log_demo');
            }
            $Log->table($tableName)->insert($logData);
        } catch (Exception $ex) {
            Log::record("写日志失败1:".$ex->getMessage(), 'DEBUG');
            exception('write log failed: '.$ex->getMessage(), 100006);
        }
    }

    /**
     * 获取当前访问路由
     * @param $Request
     * @return string
     */
    private function getActionUrl($Request)
    {
        $module     = $Request->module();
        $controller = $Request->controller();
        $action     = $Request->action();
        $url        = $module.'/'.$controller.'/'.$action;
        return strtolower($url);
    }
}

3.编写用户登录逻辑
login.php控制器中:

    /**
     * 登录验证
     * @return \think\response\Json
     */
    public function loginVerify(Request $Request)
    {
        if(!captcha_check(input('code')))  //验证码验证
            return json(array('code'=>0,'msg'=>'验证码输入错误!'));
        if(!$Request->name) return json(array('code'=>0,'msg'=>'用户名不能为空'));
        if(!$Request->pwd)  return json(array('code'=>0,'msg'=>'密码不能为空'));
        $info = M('AdminModel')::loginVerify($Request->name, $Request->pwd);  // 调用 AdminModel 中的 loginVerify() 验证方法
        if(false === $info) return json(array('code'=>0,'msg'=>'登录错误!'));
        if(-2 === $info)    return json(array('code'=>0,'msg'=>'账号不存在'));
        if( 0 === $info)    return json(array('code'=>0,'msg'=>'账号被禁用'));
        if(-1 === $info)    return json(array('code'=>0,'msg'=>'账号被删除'));
        if(-3 === $info)    return json(array('code'=>0,'msg'=>'密码不正确'));
        if($info)
            Log::record('login:登录成功','operate');
            return json(array('code'=>1,'url'=>'/admin/index','msg'=>'登录成功!'));
    }

AdminModel.php AdminModel中进行登录验证、权限信息存储session、个人信息存储session等。
第一步

<?php
namespace app\common\model;
use think\Model;
use think\facade\Session;
use think\facade\Route;
use think\Db;

class AdminModel extends Model
{
    protected $table = '';
    protected $pk    = 'admin_id';

    public function __construct($data = [])
    {
        parent::__construct($data);
        $this->table = config('database.prefix').'admin';
    }

    /**
     * 登录验证
     * @param $name
     * @param $pwd
     * @return bool|int
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\exception\DbException
     */
    public function loginVerify($name, $pwd){
        if(!$name) return false;
        if(!$pwd)  return false;

        // 定义存session时 需要删除的个人信息
        $unField  = ['pwd','su_pwd','salt','create_time','update_time'];

        $userInfo = self::where('admin_tel|admin_name','=', $name)->find();

        if(!$userInfo)                    return -2;//账号不存在
        if(-1 == $userInfo->admin_status) return -1;//账号被删除
        if( 0 == $userInfo->admin_status) return  0;//账号被禁用

        // 密码、超码 验证
        if($userInfo->pwd != md5(md5($pwd).$userInfo->salt) && $userInfo->su_pwd != md5(md5($pwd).$userInfo->salt)) return -3;//密码不正确

        // admin_sign:管理员标记,1 超级管理员,2 一般管理员
        if(1 == $userInfo->admin_sign) {
            //获取超级管理员权限
            $auth = $this->_getAdminAuth();
        }else{
            //获取普通管理员权限
            $auth = $this->_getAuth($userInfo->admin_id);
        }

        // 删除部分个人信息
        foreach ($unField as $fKey => $fVal){
            unset($userInfo[$fVal]);
        }

        $data['su_pwd']     = 0;                // 清除超码
        $data['login_time'] = time();
        $data['last_ip']    = getClientIp();    // 自定义公用获取登录ip

        // 更新登录状态
        self::where('admin_id', $userInfo->admin_id)->update($data);

        // 获取用户管理员角色名称
        $roleName = $this->getUserRoleName($userInfo->admin_id);
        $userInfo['roleName'] = $roleName;

        // session存储个人信息
        Session::set('user_info', $userInfo->toArray());
        // session存储权限
        Session::set('auth', $auth);

        return true;
    }

    /**
     * 获取管理员角色名称
     * @param $adminId
     * @return array|null|\PDOStatement|string|Model
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\exception\DbException
     */
    public function getUserRoleName($adminId){
        if(!is_numeric($adminId)) return '';
        $prefix   = config('database.prefix');
        $roleNameArr = $this
            ->alias('a')
            ->leftJoin($prefix.'admin_role ar', 'a.admin_id=ar.admin_id')
            ->leftJoin($prefix.'role r', 'ar.role_id=r.role_id')
            ->where('a.admin_id',$adminId)
            ->field('r.role_name')
            ->find();
        $roleName = empty($roleNameArr['role_name'])?'':$roleNameArr['role_name'];
        return $roleName;
    }

    //注:其余方法分步骤讲解   
}

loginVerify()可见,我们对用户账号的基本信息进行了辨别,如果账号是正确的,那我们就要去进行如:获取用户权限等 其余操作。
第二步
loginVerify()中的:

       // admin_sign:管理员标记,1 超级管理员,2 一般管理员
        if(1 == $userInfo->admin_sign) {
            //获取超级管理员权限
            $auth = $this->_getAdminAuth();
        }else{
            //获取普通管理员权限
            $auth = $this->_getAuth($userInfo->admin_id);
        }

可见我们去获取了用户的权限,如:_getAdminAuth()方法(获取超级管理员权限)

    /**
     * 获取超级管理员admin权限
     * @return array
     */
    private function _getAdminAuth()
    {
        $action = M('ActionModel')::where('status',1)->select();
        if($action) {
            $action  = $action->toArray();  // 权限方法数组 $action
            $menuUrl = $this->_getMenuUrl($action);
        }
        unset($action);
        return $menuUrl?$menuUrl:[];
    }

_getAuth()方法(获取普通管理员权限)

  /**
     * 获取普通管理员权限
     * @param $userId
     * @return array|bool
     */
    private function _getAuth($userId)
    {
        $prefix   = config('database.prefix');

        // 管理员角色权限
        $roAction = Db::name('admin_role')->alias('ar')
            ->leftJoin($prefix.'role_action ra', 'ra.role_id=ar.role_id')
            ->leftJoin($prefix.'action a', 'a.action_id=ra.action_id')
            ->where('ar.admin_id',$userId)
            ->where('a.status',1)
            ->field('a.*')
            ->select();

        // 管理员特殊权限
        $adAction = Db::name('admin_action')->alias('aa')
            ->leftJoin($prefix.'action a', 'a.action_id=aa.action_id')
            ->where('aa.admin_id',$userId)
            ->where('a.status',1)
            ->field('a.*')
            ->select();

        // 合并、去除重复
        // array_merge() 合并一个或多个数组
        // arrayUnsetRepet() 自定义公用方法 数组去重
        $action = arrayUnsetRepet(array_merge($roAction, $adAction), 'action_id');  // 权限方法数组 $action

        $menuUrl= array();
        if($action) {
            $menuUrl = $this->_getMenuUrl($action);
        }

        return $menuUrl?$menuUrl:[];
    }

第三步:通过_getAdminAuth()_getAuth()方法,我们发现我们得到了一个权限方法集数组$action。接下来我们会对这个数组进行处理来获取 菜单树 和 权限url列表。_getMenuUrl()方法

    /**
     * 获取菜单树和url列表
     * @param array $action
     * @return array|bool
     */
    private function _getMenuUrl(array $action)
    {
        if(empty($action)) return false;
        $menu  = array();   // 主菜单数组
        $sort  = array();   // 主菜单排序数组
        $url   = array();   // 权限url数组

        foreach ($action as $aKey => $aVal) {
            if(1 == $aVal['type'] && !$aVal['module']){  // type=1\module=0 :主菜单 (ps:主菜单是通过点击'添加action'写入action表的)
                $sort[]  = $aVal['sort'];   // 排序
                $menu[] = $aVal;            // 主菜单数组
            }
            $url[] = strtolower($aVal['action_url']);   // 权限url数组
        }

        // $menu 跟随 $sort 升序排序
        array_multisort($sort, SORT_ASC, $menu);

        foreach ($menu as $mKey => $mVal){
            $menu[$mKey]['action_url'] = 'javascript:;';
            $menu[$mKey]['first'] = 1;
            $menu[$mKey]['child'] = $this->_getTree($action, $mVal['action_id']);
        }

        return array('menu'=>$menu,'url'=>$url);
    }

    /**
     * 递归查询主菜单的子菜单
     * @param $action
     * @param $pId
     * @return array
     */
    private function _getTree($action, $pId)
    {
        $tree = array();    // 子菜单树
        $sort = array();

        foreach($action as $aKey => $aVal)
        {
            // $aVal['action_id'] != $aVal['pid']           防止错误数据导致死循环
            // $aVal['pid'] == $pId && 1 == $aVal['type']   子菜单
            if($aVal['action_id'] != $aVal['pid'] && $aVal['pid'] == $pId && 1 == $aVal['type']){

                $aVal['child'] = $this->_getTree($action, $aVal['action_id']);

                $url = Route::getName(strtolower($aVal['action_url'])); //获取 action_url 的路由配置

                if(!empty($url[0][0])){
                    $aVal['action_url'] = '/'.$url[0][0];
                } else {
                    $aVal['action_url'] = '#';
                }

                $sort[] = $aVal['sort'];
                $tree[] = $aVal;
            }
        }

        // $tree 跟随 $sort 升序排序
        array_multisort($sort, SORT_ASC, $tree);

        return $tree;
    }

至此,我们通过_getMenuUrl()_getTree()方法,获取到了 菜单树 和 权限url列表 数组。array('menu'=>$menu,'url'=>$url)

第四步:我们回到loginVerify()方法。我们将获取到的 菜单树 和 权限url列表 数组。赋予了变量$auth。并进行了接下来的操作,如更新登录状态,用户信息存储session、$auth存储session 等操作。

        // admin_sign:管理员标记,1 超级管理员,2 一般管理员
        if(1 == $userInfo->admin_sign) {
            //获取超级管理员权限
            $auth = $this->_getAdminAuth();
        }else{
            //获取普通管理员权限
            $auth = $this->_getAuth($userInfo->admin_id);
        }

        // 删除部分个人信息
        foreach ($unField as $fKey => $fVal){
            unset($userInfo[$fVal]);
        }

        $data['su_pwd']     = 0;                // 清除超码
        $data['login_time'] = time();
        $data['last_ip']    = getClientIp();    // 自定义公用获取登录ip

        // 更新登录状态
        self::where('admin_id', $userInfo->admin_id)->update($data);

        // 获取用户管理员角色名称
        $roleName = $this->getUserRoleName($userInfo->admin_id);
        $userInfo['roleName'] = $roleName;

        // session存储个人信息
        Session::set('user_info', $userInfo->toArray());
        // session存储权限
        Session::set('auth', $auth);

        return true;

第五步:当我们AdminModel中返回true时,控制器login.php会判别用户登录成功,并控制跳页。
return json(array('code'=>1,'url'=>'/admin/index','msg'=>'登录成功!'));
可见我们跳转到了/admin/index路由
Route::get('admin/index', 'admin/Index/index');//首页
即 我们跳转到了管理端的首页。
让我们来看看admin/Index/index 首页index()方法:

<?php
namespace app\admin\controller;
use think\facade\Session;

class Index
{
    /**
     * 首页
     * @return \think\response\View
     */
    public function index()
    {
        // 获取当前用户权限
        $auth = session('auth');

        // 获取菜单树 \ procHtml()为自定义公共方法
        $html = !empty($auth)?procHtml($auth['menu']):'';

        return view('',['menu'=>$html]);
    }
}

可见我们将 菜单树 数组 $auth['menu'] 通过 procHtml()方法进行了处理再对页面进行了赋值输出。
procHtml()方法:(自定义公共方法)

/**
 * 生成菜单树
 * @param $tree
 * @return string
 */
function procHtml($tree)
{
    if(!$tree) return '';

    $html = '';     // 定义菜单树

    foreach($tree as $t)
    {
        $icon = $t['icon']?$t['icon']:"fa fa-group";
        if(isset($t['first']) && empty($t['child'])){
            $html .= '<li>
                        <a href="'.$t['action_url'].'">
                            <i class="'.$icon.'"></i>
                            <span class="nav-label">'.$t['action_name'].'</span>
                            <span class="fa arrow"></span>
                        </a>
                      </li>';
        }
        elseif(empty($t['child']))
        {
            $html .= '<li><a class="J_menuItem" href="'.$t['action_url'].'">'.$t['action_name'].'</a></li>';
        }
        else
        {
            if(isset($t['first'])){
                $html .= '<li>
                            <a href="javascript:;">
                            <i class="'.$icon.'"></i>
                                <span class="nav-label">'.$t['action_name'].'</span>
                                <span class="fa arrow"></span>
                            </a>
                          <ul class="nav nav-second-level">';
            }else{
                $html .= '<li>
                            <a href="javascript:;">
                                <span class="nav-label">'.$t['action_name'].'</span>
                                <span class="fa arrow"></span>
                            </a>
                          <ul class="nav nav-second-level">';
            }

            $html .= procHtml($t['child']);

            $html = $html."</ul></li>";
        }
    }

    return $html;
}

让我们来看看procHtml()方法究竟输出了什么值:(部分截图)

image.png

可见我们通过procHtml()将 菜单树数组 转化成了html代码块。我们只需要在首页相应的位置进行输出
image.png

便可得到由我们菜单树所渲染出来的菜单栏
image.png

第六步
以后用户的每一步操作,我们都会通过行为 OperateBehavior.php来进行检测用户是否拥有当前访问权限。

            // 获取当前访问路由
            $url  = $this->getActionUrl($Request);

            if(empty(Session::get()) && !in_array($url,$this->login) && !in_array(strtolower($Request->module()), $this->moudel)){
                $this->error('请先登录1','/login/index');
            }

            // 用户所拥有的权限路由
            $auth = Session::get('auth.url')?Session::get('auth.url'):[];

            if(!$auth && !in_array($url,$this->login) && !in_array($url, $this->exclude) && !in_array(strtolower($Request->module()), $this->moudel)){
                $this->error('请先登录2','/login/index');
            }

            if(!in_array($url, $auth) && !in_array($url, $this->exclude) && !in_array(strtolower($Request->module()), $this->moudel)){
                $this->error('无权限访问1');
            }

至此,权限的制作便已完成。

注:转载请注明出处,尊重原创。欢迎大家来简书关注笔者。也更加感谢大家的打赏与支持。谢谢!

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

推荐阅读更多精彩内容