思路:用户登录验证的时候将用户权限路由存储session,定义方法执行前行为,行为验证当前访问的方法路由是否属于用户所拥有的权限路由。
一、在制作站点权限之前我们需要准备好所需的数据表,数据表如下:(各表字段可酌情加减,视各自站点逻辑而定)
1.admin
-- 管理员账户表
2.
role
-- 角色表3.
admin_role
-- 管理员角色关系表4.
role_action
-- 角色方法表5.
admin_action
-- 管理员特殊方法表(酌情添加)6.
action
-- 方法表(方法数据通过站点采集而来。ps 采集方法,附上链接:https://www.jianshu.com/p/5e013fcb19aa)二、数据表大致准备如上,接下来进行我们的权限验证流程:
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代码块。我们只需要在首页相应的位置进行输出便可得到由我们菜单树所渲染出来的菜单栏
第六步:
以后用户的每一步操作,我们都会通过行为
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');
}
至此,权限的制作便已完成。
注:转载请注明出处,尊重原创。欢迎大家来简书关注笔者。也更加感谢大家的打赏与支持。谢谢!