tp5.1 权限模块 -- 权限实现
思路:用户登录验证的时候将用户权限路由存储session,定义方法执行前行为,行为验证当前访问的方法路由是否属于用户所拥有的权限路由。
一、在制作站点权限之前我们需要准备好所需的数据表,数据表如下:(各表字段可酌情加减,视各自站点逻辑而定)
1.admin
-- 管理员账户表
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
行为文件,进行权限验证
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()
方法究竟输出了什么值:(部分截图)
可见我们通过
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');
}
至此,权限的制作便已完成。
注:转载请注明出处,尊重原创。欢迎大家来简书关注笔者。也更加感谢大家的打赏与支持。谢谢!