仓库初始化

This commit is contained in:
2025-08-13 10:43:56 +08:00
commit e8f9b46680
5180 changed files with 859303 additions and 0 deletions

View File

@@ -0,0 +1,583 @@
<?php
namespace app\common\library;
use app\common\model\User;
use app\common\model\UserRule;
use fast\Random;
use think\Config;
use think\Db;
use think\Exception;
use think\Hook;
use think\Request;
use think\Validate;
class Auth
{
protected static $instance = null;
protected $_error = '';
protected $_logined = false;
protected $_user = null;
protected $_token = '';
//Token默认有效时长
protected $keeptime = 2592000;
protected $requestUri = '';
protected $rules = [];
//默认配置
protected $config = [];
protected $options = [];
protected $allowFields = ['id', 'username', 'nickname', 'mobile', 'avatar', 'score'];
public function __construct($options = [])
{
if ($config = Config::get('user')) {
$this->config = array_merge($this->config, $config);
}
$this->options = array_merge($this->config, $options);
}
/**
*
* @param array $options 参数
* @return Auth
*/
public static function instance($options = [])
{
if (is_null(self::$instance)) {
self::$instance = new static($options);
}
return self::$instance;
}
/**
* 获取User模型
* @return User
*/
public function getUser()
{
return $this->_user;
}
/**
* 兼容调用user模型的属性
*
* @param string $name
* @return mixed
*/
public function __get($name)
{
return $this->_user ? $this->_user->$name : null;
}
/**
* 兼容调用user模型的属性
*/
public function __isset($name)
{
return isset($this->_user) ? isset($this->_user->$name) : false;
}
/**
* 根据Token初始化
*
* @param string $token Token
* @return boolean
*/
public function init($token)
{
if ($this->_logined) {
return true;
}
if ($this->_error) {
return false;
}
$data = Token::get($token);
if (!$data) {
return false;
}
$user_id = intval($data['user_id']);
if ($user_id > 0) {
$user = User::get($user_id);
if (!$user) {
$this->setError('Account not exist');
return false;
}
if ($user['status'] != 'normal') {
$this->setError('Account is locked');
return false;
}
$this->_user = $user;
$this->_logined = true;
$this->_token = $token;
//初始化成功的事件
Hook::listen("user_init_successed", $this->_user);
return true;
} else {
$this->setError('You are not logged in');
return false;
}
}
/**
* 注册用户
*
* @param string $username 用户名
* @param string $password 密码
* @param string $email 邮箱
* @param string $mobile 手机号
* @param array $extend 扩展参数
* @return boolean
*/
public function register($username, $password, $email = '', $mobile = '', $extend = [])
{
// 检测用户名、昵称、邮箱、手机号是否存在
if (User::getByUsername($username)) {
$this->setError('Username already exist');
return false;
}
if (User::getByNickname($username)) {
$this->setError('Nickname already exist');
return false;
}
if ($email && User::getByEmail($email)) {
$this->setError('Email already exist');
return false;
}
if ($mobile && User::getByMobile($mobile)) {
$this->setError('Mobile already exist');
return false;
}
$ip = request()->ip();
$time = time();
$data = [
'username' => $username,
'password' => $password,
'email' => $email,
'mobile' => $mobile,
'level' => 1,
'score' => 0,
'avatar' => '',
];
$params = array_merge($data, [
'nickname' => preg_match("/^1[3-9]{1}\d{9}$/", $username) ? substr_replace($username, '****', 3, 4) : $username,
'salt' => Random::alnum(),
'jointime' => $time,
'joinip' => $ip,
'logintime' => $time,
'loginip' => $ip,
'prevtime' => $time,
'status' => 'normal'
]);
$params['password'] = $this->getEncryptPassword($password, $params['salt']);
$params = array_merge($params, $extend);
//账号注册时需要开启事务,避免出现垃圾数据
Db::startTrans();
try {
$user = User::create($params, true);
$this->_user = User::get($user->id);
//设置Token
$this->_token = Random::uuid();
Token::set($this->_token, $user->id, $this->keeptime);
//设置登录状态
$this->_logined = true;
//注册成功的事件
Hook::listen("user_register_successed", $this->_user, $data);
Db::commit();
} catch (Exception $e) {
$this->setError($e->getMessage());
Db::rollback();
return false;
}
return true;
}
/**
* 用户登录
*
* @param string $account 账号,用户名、邮箱、手机号
* @param string $password 密码
* @return boolean
*/
public function login($account, $password)
{
$field = Validate::is($account, 'email') ? 'email' : (Validate::regex($account, '/^1\d{10}$/') ? 'mobile' : 'username');
$user = User::get([$field => $account]);
if (!$user) {
$this->setError('Account is incorrect');
return false;
}
if ($user->status != 'normal') {
$this->setError('Account is locked');
return false;
}
if ($user->loginfailure >= 10 && time() - $user->loginfailuretime < 86400) {
$this->setError('Please try again after 1 day');
return false;
}
if ($user->password != $this->getEncryptPassword($password, $user->salt)) {
$user->save(['loginfailure' => $user->loginfailure + 1, 'loginfailuretime' => time()]);
$this->setError('Password is incorrect');
return false;
}
//直接登录会员
return $this->direct($user->id);
}
/**
* 退出
*
* @return boolean
*/
public function logout()
{
if (!$this->_logined) {
$this->setError('You are not logged in');
return false;
}
//设置登录标识
$this->_logined = false;
//删除Token
Token::delete($this->_token);
//退出成功的事件
Hook::listen("user_logout_successed", $this->_user);
return true;
}
/**
* 修改密码
* @param string $newpassword 新密码
* @param string $oldpassword 旧密码
* @param bool $ignoreoldpassword 忽略旧密码
* @return boolean
*/
public function changepwd($newpassword, $oldpassword = '', $ignoreoldpassword = false)
{
if (!$this->_logined) {
$this->setError('You are not logged in');
return false;
}
//判断旧密码是否正确
if ($this->_user->password == $this->getEncryptPassword($oldpassword, $this->_user->salt) || $ignoreoldpassword) {
Db::startTrans();
try {
$salt = Random::alnum();
$newpassword = $this->getEncryptPassword($newpassword, $salt);
$this->_user->save(['loginfailure' => 0, 'password' => $newpassword, 'salt' => $salt]);
Token::delete($this->_token);
//修改密码成功的事件
Hook::listen("user_changepwd_successed", $this->_user);
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->setError($e->getMessage());
return false;
}
return true;
} else {
$this->setError('Password is incorrect');
return false;
}
}
/**
* 直接登录账号
* @param int $user_id
* @return boolean
*/
public function direct($user_id)
{
$user = User::get($user_id);
if ($user) {
Db::startTrans();
try {
$ip = request()->ip();
$time = time();
//判断连续登录和最大连续登录
if ($user->logintime < \fast\Date::unixtime('day')) {
$user->successions = $user->logintime < \fast\Date::unixtime('day', -1) ? 1 : $user->successions + 1;
$user->maxsuccessions = max($user->successions, $user->maxsuccessions);
}
$user->prevtime = $user->logintime;
//记录本次登录的IP和时间
$user->loginip = $ip;
$user->logintime = $time;
//重置登录失败次数
$user->loginfailure = 0;
$user->save();
$this->_user = $user;
$this->_token = Random::uuid();
Token::set($this->_token, $user->id, $this->keeptime);
$this->_logined = true;
//登录成功的事件
Hook::listen("user_login_successed", $this->_user);
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->setError($e->getMessage());
return false;
}
return true;
} else {
return false;
}
}
/**
* 检测是否是否有对应权限
* @param string $path 控制器/方法
* @param string $module 模块 默认为当前模块
* @return boolean
*/
public function check($path = null, $module = null)
{
if (!$this->_logined) {
return false;
}
$ruleList = $this->getRuleList();
$rules = [];
foreach ($ruleList as $k => $v) {
$rules[] = $v['name'];
}
$url = ($module ? $module : request()->module()) . '/' . (is_null($path) ? $this->getRequestUri() : $path);
$url = strtolower(str_replace('.', '/', $url));
return in_array($url, $rules);
}
/**
* 判断是否登录
* @return boolean
*/
public function isLogin()
{
if ($this->_logined) {
return true;
}
return false;
}
/**
* 获取当前Token
* @return string
*/
public function getToken()
{
return $this->_token;
}
/**
* 获取会员基本信息
*/
public function getUserinfo()
{
$data = $this->_user->toArray();
$allowFields = $this->getAllowFields();
$userinfo = array_intersect_key($data, array_flip($allowFields));
$userinfo = array_merge($userinfo, Token::get($this->_token));
return $userinfo;
}
/**
* 获取会员组别规则列表
* @return array|bool|\PDOStatement|string|\think\Collection
*/
public function getRuleList()
{
if ($this->rules) {
return $this->rules;
}
$group = $this->_user->group;
if (!$group) {
return [];
}
$rules = explode(',', $group->rules);
$this->rules = UserRule::where('status', 'normal')->where('id', 'in', $rules)->field('id,pid,name,title,ismenu')->select();
return $this->rules;
}
/**
* 获取当前请求的URI
* @return string
*/
public function getRequestUri()
{
return $this->requestUri;
}
/**
* 设置当前请求的URI
* @param string $uri
*/
public function setRequestUri($uri)
{
$this->requestUri = $uri;
}
/**
* 获取允许输出的字段
* @return array
*/
public function getAllowFields()
{
return $this->allowFields;
}
/**
* 设置允许输出的字段
* @param array $fields
*/
public function setAllowFields($fields)
{
$this->allowFields = $fields;
}
/**
* 删除一个指定会员
* @param int $user_id 会员ID
* @return boolean
*/
public function delete($user_id)
{
$user = User::get($user_id);
if (!$user) {
return false;
}
Db::startTrans();
try {
// 删除会员
User::destroy($user_id);
// 删除会员指定的所有Token
Token::clear($user_id);
Hook::listen("user_delete_successed", $user);
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->setError($e->getMessage());
return false;
}
return true;
}
/**
* 获取密码加密后的字符串
* @param string $password 密码
* @param string $salt 密码盐
* @return string
*/
public function getEncryptPassword($password, $salt = '')
{
return md5(md5($password) . $salt);
}
/**
* 检测当前控制器和方法是否匹配传递的数组
*
* @param array $arr 需要验证权限的数组
* @return boolean
*/
public function match($arr = [])
{
$request = Request::instance();
$arr = is_array($arr) ? $arr : explode(',', $arr);
if (!$arr) {
return false;
}
$arr = array_map('strtolower', $arr);
// 是否存在
if (in_array(strtolower($request->action()), $arr) || in_array('*', $arr)) {
return true;
}
// 没找到匹配
return false;
}
/**
* 设置会话有效时间
* @param int $keeptime 默认为永久
*/
public function keeptime($keeptime = 0)
{
$this->keeptime = $keeptime;
}
/**
* 渲染用户数据
* @param array $datalist 二维数组
* @param mixed $fields 加载的字段列表
* @param string $fieldkey 渲染的字段
* @param string $renderkey 结果字段
* @return array
*/
public function render(&$datalist, $fields = [], $fieldkey = 'user_id', $renderkey = 'userinfo')
{
$fields = !$fields ? ['id', 'nickname', 'level', 'avatar'] : (is_array($fields) ? $fields : explode(',', $fields));
$ids = [];
foreach ($datalist as $k => $v) {
if (!isset($v[$fieldkey])) {
continue;
}
$ids[] = $v[$fieldkey];
}
$list = [];
if ($ids) {
if (!in_array('id', $fields)) {
$fields[] = 'id';
}
$ids = array_unique($ids);
$selectlist = User::where('id', 'in', $ids)->column($fields);
foreach ($selectlist as $k => $v) {
$list[$v['id']] = $v;
}
}
foreach ($datalist as $k => &$v) {
$v[$renderkey] = $list[$v[$fieldkey]] ?? null;
}
unset($v);
return $datalist;
}
/**
* 设置错误信息
*
* @param string $error 错误信息
* @return Auth
*/
public function setError($error)
{
$this->_error = $error;
return $this;
}
/**
* 获取错误信息
* @return string
*/
public function getError()
{
return $this->_error ? __($this->_error) : '';
}
}

View File

@@ -0,0 +1,236 @@
<?php
namespace app\common\library;
use think\Config;
use Tx\Mailer;
use Tx\Mailer\Exceptions\CodeException;
use Tx\Mailer\Exceptions\SendException;
class Email
{
/**
* 单例对象
*/
protected static $instance;
/**
* phpmailer对象
*/
protected $mail = null;
/**
* 错误内容
*/
protected $error = '';
/**
* 默认配置
*/
public $options = [
'charset' => 'utf-8', //编码格式
'debug' => false, //调式模式
'mail_type' => 0, //状态
];
/**
* 初始化
* @access public
* @param array $options 参数
* @return Email
*/
public static function instance($options = [])
{
if (is_null(self::$instance)) {
self::$instance = new static($options);
}
return self::$instance;
}
/**
* 构造函数
* @param array $options
*/
public function __construct($options = [])
{
if ($config = Config::get('site')) {
$this->options = array_merge($this->options, $config);
}
$this->options = array_merge($this->options, $options);
$secureArr = [0 => '', 1 => 'tls', 2 => 'ssl'];
$secure = $secureArr[$this->options['mail_verify_type']] ?? '';
$logger = isset($this->options['debug']) && $this->options['debug'] ? new Log : null;
$this->mail = new Mailer($logger);
$this->mail->setServer($this->options['mail_smtp_host'], $this->options['mail_smtp_port'], $secure);
$this->mail->setAuth($this->options['mail_from'], $this->options['mail_smtp_pass']);
//设置发件人
$this->from($this->options['mail_from'], $this->options['mail_smtp_user']);
}
/**
* 设置邮件主题
* @param string $subject 邮件主题
* @return $this
*/
public function subject($subject)
{
$this->mail->setSubject($subject);
return $this;
}
/**
* 设置发件人
* @param string $email 发件人邮箱
* @param string $name 发件人名称
* @return $this
*/
public function from($email, $name = '')
{
$this->mail->setFrom($name, $email);
return $this;
}
/**
* 设置收件人
* @param mixed $email 收件人,多个收件人以,进行分隔
* @return $this
*/
public function to($email)
{
$emailArr = $this->buildAddress($email);
foreach ($emailArr as $address => $name) {
$this->mail->addTo($name, $address);
}
return $this;
}
/**
* 设置抄送
* @param mixed $email 收件人,多个收件人以,进行分隔
* @param string $name 收件人名称
* @return Email
*/
public function cc($email, $name = '')
{
$emailArr = $this->buildAddress($email);
if (count($emailArr) == 1 && $name) {
$emailArr[key($emailArr)] = $name;
}
foreach ($emailArr as $address => $name) {
$this->mail->addCC($name, $address);
}
return $this;
}
/**
* 设置密送
* @param mixed $email 收件人,多个收件人以,进行分隔
* @param string $name 收件人名称
* @return Email
*/
public function bcc($email, $name = '')
{
$emailArr = $this->buildAddress($email);
if (count($emailArr) == 1 && $name) {
$emailArr[key($emailArr)] = $name;
}
foreach ($emailArr as $address => $name) {
$this->mail->addBCC($name, $address);
}
return $this;
}
/**
* 设置邮件正文
* @param string $body 邮件下方
* @param boolean $ishtml 是否HTML格式
* @return $this
*/
public function message($body, $ishtml = true)
{
$this->mail->setBody($body);
return $this;
}
/**
* 添加附件
* @param string $path 附件路径
* @param string $name 附件名称
* @return Email
*/
public function attachment($path, $name = '')
{
$this->mail->addAttachment($name, $path);
return $this;
}
/**
* 构建Email地址
* @param mixed $emails Email数据
* @return array
*/
protected function buildAddress($emails)
{
if (!is_array($emails)) {
$emails = array_flip(explode(',', str_replace(";", ",", $emails)));
foreach ($emails as $key => $value) {
$emails[$key] = strstr($key, '@', true);
}
}
return $emails;
}
/**
* 获取最后产生的错误
* @return string
*/
public function getError()
{
return $this->error;
}
/**
* 设置错误
* @param string $error 信息信息
*/
protected function setError($error)
{
$this->error = $error;
}
/**
* 发送邮件
* @return boolean
*/
public function send()
{
$result = false;
if (in_array($this->options['mail_type'], [1, 2])) {
try {
$result = $this->mail->send();
} catch (SendException $e) {
$this->setError($e->getCode() . $e->getMessage());
} catch (CodeException $e) {
preg_match_all("/Expected: (\d+)\, Got: (\d+)( \| (.*))?\$/i", $e->getMessage(), $matches);
$code = $matches[2][0] ?? 0;
$message = isset($matches[2][0]) && isset($matches[4][0]) ? $matches[4][0] : $e->getMessage();
$message = mb_convert_encoding($message, 'UTF-8', 'GBK,GB2312,BIG5');
$this->setError($message);
} catch (\Exception $e) {
$this->setError($e->getMessage());
}
$this->setError($result ? '' : $this->getError());
} else {
//邮件功能已关闭
$this->setError(__('Mail already closed'));
}
return $result;
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace app\common\library;
use fast\Random;
use think\Hook;
/**
* 邮箱验证码类
*/
class Ems
{
/**
* 验证码有效时长
* @var int
*/
protected static $expire = 120;
/**
* 最大允许检测的次数
* @var int
*/
protected static $maxCheckNums = 10;
/**
* 获取最后一次邮箱发送的数据
*
* @param int $email 邮箱
* @param string $event 事件
* @return Ems|null
*/
public static function get($email, $event = 'default')
{
$ems = \app\common\model\Ems::where(['email' => $email, 'event' => $event])
->order('id', 'DESC')
->find();
Hook::listen('ems_get', $ems, null, true);
return $ems ?: null;
}
/**
* 发送验证码
*
* @param int $email 邮箱
* @param int $code 验证码,为空时将自动生成4位数字
* @param string $event 事件
* @return boolean
*/
public static function send($email, $code = null, $event = 'default')
{
$code = is_null($code) ? Random::numeric(config('captcha.length')) : $code;
$time = time();
$ip = request()->ip();
$ems = \app\common\model\Ems::create(['event' => $event, 'email' => $email, 'code' => $code, 'ip' => $ip, 'createtime' => $time]);
if (!Hook::get('ems_send')) {
//采用框架默认的邮件推送
Hook::add('ems_send', function ($params) {
$obj = new Email();
$result = $obj
->to($params->email)
->subject('请查收你的验证码!')
->message("你的验证码是:" . $params->code . "" . ceil(self::$expire / 60) . "分钟内有效。")
->send();
return $result;
});
}
$result = Hook::listen('ems_send', $ems, null, true);
if (!$result) {
$ems->delete();
return false;
}
return true;
}
/**
* 发送通知
*
* @param mixed $email 邮箱,多个以,分隔
* @param string $msg 消息内容
* @param string $template 消息模板
* @return boolean
*/
public static function notice($email, $msg = '', $template = null)
{
$params = [
'email' => $email,
'msg' => $msg,
'template' => $template
];
if (!Hook::get('ems_notice')) {
//采用框架默认的邮件推送
Hook::add('ems_notice', function ($params) {
$subject = '你收到一封新的邮件!';
$content = $params['msg'];
$email = new Email();
$result = $email->to($params['email'])
->subject($subject)
->message($content)
->send();
return $result;
});
}
$result = Hook::listen('ems_notice', $params, null, true);
return (bool)$result;
}
/**
* 校验验证码
*
* @param int $email 邮箱
* @param int $code 验证码
* @param string $event 事件
* @return boolean
*/
public static function check($email, $code, $event = 'default')
{
$time = time() - self::$expire;
$ems = \app\common\model\Ems::where(['email' => $email, 'event' => $event])
->order('id', 'DESC')
->find();
if ($ems) {
if ($ems['createtime'] > $time && $ems['times'] <= self::$maxCheckNums) {
$correct = $code == $ems['code'];
if (!$correct) {
$ems->times = $ems->times + 1;
$ems->save();
return false;
} else {
$result = Hook::listen('ems_check', $ems, null, true);
return true;
}
} else {
// 过期则清空该邮箱验证码
self::flush($email, $event);
return false;
}
} else {
return false;
}
}
/**
* 清空指定邮箱验证码
*
* @param int $email 邮箱
* @param string $event 事件
* @return boolean
*/
public static function flush($email, $event = 'default')
{
\app\common\model\Ems::where(['email' => $email, 'event' => $event])
->delete();
Hook::listen('ems_flush');
return true;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace app\common\library;
use Psr\Log\AbstractLogger;
use think\Hook;
/**
* 日志记录类
*/
class Log extends AbstractLogger
{
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*/
public function log($level, $message, array $context = [])
{
\think\Log::write($message);
}
}

View File

@@ -0,0 +1,225 @@
<?php
namespace app\common\library;
use app\admin\model\AuthRule;
use fast\Tree;
use think\addons\Service;
use think\Db;
use think\Exception;
use think\exception\PDOException;
class Menu
{
/**
* 创建菜单
* @param array $menu
* @param mixed $parent 父类的name或pid
*/
public static function create($menu = [], $parent = 0)
{
$old = [];
self::menuUpdate($menu, $old, $parent);
//菜单刷新处理
$info = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
preg_match('/addons\\\\([a-z0-9]+)\\\\/i', $info['class'], $matches);
if ($matches && isset($matches[1])) {
Menu::refresh($matches[1], $menu);
}
}
/**
* 删除菜单
* @param string $name 规则name
* @return boolean
*/
public static function delete($name)
{
$ids = self::getAuthRuleIdsByName($name);
if (!$ids) {
return false;
}
AuthRule::destroy($ids);
return true;
}
/**
* 启用菜单
* @param string $name
* @return boolean
*/
public static function enable($name)
{
$ids = self::getAuthRuleIdsByName($name);
if (!$ids) {
return false;
}
AuthRule::where('id', 'in', $ids)->update(['status' => 'normal']);
return true;
}
/**
* 禁用菜单
* @param string $name
* @return boolean
*/
public static function disable($name)
{
$ids = self::getAuthRuleIdsByName($name);
if (!$ids) {
return false;
}
AuthRule::where('id', 'in', $ids)->update(['status' => 'hidden']);
return true;
}
/**
* 升级菜单
* @param string $name 插件名称
* @param array $menu 新菜单
* @return bool
*/
public static function upgrade($name, $menu)
{
$ids = self::getAuthRuleIdsByName($name);
$old = AuthRule::where('id', 'in', $ids)->select();
$old = collection($old)->toArray();
$old = array_column($old, null, 'name');
Db::startTrans();
try {
self::menuUpdate($menu, $old);
$ids = [];
foreach ($old as $index => $item) {
if (!isset($item['keep'])) {
$ids[] = $item['id'];
}
}
if ($ids) {
//旧版本的菜单需要做删除处理
$config = Service::config($name);
$menus = $config['menus'] ?? [];
$where = ['id' => ['in', $ids]];
if ($menus) {
//必须是旧版本中的菜单,可排除用户自主创建的菜单
$where['name'] = ['in', $menus];
}
AuthRule::where($where)->delete();
}
Db::commit();
} catch (PDOException $e) {
Db::rollback();
return false;
}
Menu::refresh($name, $menu);
return true;
}
/**
* 刷新插件菜单配置缓存
* @param string $name
* @param array $menu
*/
public static function refresh($name, $menu = [])
{
if (!$menu) {
// $menu为空时表示首次安装首次安装需刷新插件菜单标识缓存
$menuIds = Menu::getAuthRuleIdsByName($name);
$menus = Db::name("auth_rule")->where('id', 'in', $menuIds)->column('name');
} else {
// 刷新新的菜单缓存
$getMenus = function ($menu) use (&$getMenus) {
$result = [];
foreach ($menu as $index => $item) {
$result[] = $item['name'];
$result = array_merge($result, isset($item['sublist']) && is_array($item['sublist']) ? $getMenus($item['sublist']) : []);
}
return $result;
};
$menus = $getMenus($menu);
}
//刷新新的插件核心菜单缓存
Service::config($name, ['menus' => $menus]);
}
/**
* 导出指定名称的菜单规则
* @param string $name
* @return array
*/
public static function export($name)
{
$ids = self::getAuthRuleIdsByName($name);
if (!$ids) {
return [];
}
$menuList = [];
$menu = AuthRule::getByName($name);
if ($menu) {
$ruleList = collection(AuthRule::where('id', 'in', $ids)->select())->toArray();
$menuList = Tree::instance()->init($ruleList)->getTreeArray($menu['id']);
}
return $menuList;
}
/**
* 菜单升级
* @param array $newMenu
* @param array $oldMenu
* @param int $parent
* @throws Exception
*/
private static function menuUpdate($newMenu, &$oldMenu, $parent = 0)
{
if (!is_numeric($parent)) {
$parentRule = AuthRule::getByName($parent);
$pid = $parentRule ? $parentRule['id'] : 0;
} else {
$pid = $parent;
}
$allow = array_flip(['file', 'name', 'title', 'url', 'icon', 'condition', 'remark', 'ismenu', 'menutype', 'extend', 'weigh', 'status']);
foreach ($newMenu as $k => $v) {
$hasChild = isset($v['sublist']) && $v['sublist'];
$data = array_intersect_key($v, $allow);
$data['ismenu'] = $data['ismenu'] ?? ($hasChild ? 1 : 0);
$data['icon'] = $data['icon'] ?? ($hasChild ? 'fa fa-list' : 'fa fa-circle-o');
$data['pid'] = $pid;
$data['status'] = $data['status'] ?? 'normal';
if (!isset($oldMenu[$data['name']])) {
$menu = AuthRule::create($data);
} else {
$menu = $oldMenu[$data['name']];
//更新旧菜单
AuthRule::update($data, ['id' => $menu['id']]);
$oldMenu[$data['name']]['keep'] = true;
}
if ($hasChild) {
self::menuUpdate($v['sublist'], $oldMenu, $menu['id']);
}
}
}
/**
* 根据名称获取规则IDS
* @param string $name
* @return array
*/
public static function getAuthRuleIdsByName($name)
{
$ids = [];
$menu = AuthRule::getByName($name);
if ($menu) {
// 必须将结果集转换为数组
$ruleList = collection(AuthRule::order('weigh', 'desc')->field('id,pid,name')->select())->toArray();
// 构造菜单数据
$ids = Tree::instance()->init($ruleList)->getChildrenIds($menu['id'], true);
}
return $ids;
}
}

View File

@@ -0,0 +1,872 @@
<?php
namespace app\common\library;
use Exception;
/**
* 安全过滤类
*
* @category Security
* @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
* @copyright Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @author EllisLab Dev Team
*/
class Security
{
protected static $instance = null;
/**
* List of sanitize filename strings
*
* @var array
*/
public $filename_bad_chars = array(
'../',
'<!--',
'-->',
'<',
'>',
"'",
'"',
'&',
'$',
'#',
'{',
'}',
'[',
']',
'=',
';',
'?',
'%20',
'%22',
'%3c', // <
'%253c', // <
'%3e', // >
'%0e', // >
'%28', // (
'%29', // )
'%2528', // (
'%26', // &
'%24', // $
'%3f', // ?
'%3b', // ;
'%3d' // =
);
/**
* Character set
*
* Will be overridden by the constructor.
*
* @var string
*/
public $charset = 'UTF-8';
/**
* XSS Hash
*
* Random Hash for protecting URLs.
*
* @var string
*/
protected $_xss_hash;
/**
* List of never allowed strings
*
* @var array
*/
protected $_never_allowed_str = array(
'document.cookie' => '[removed]',
'(document).cookie' => '[removed]',
'document.write' => '[removed]',
'(document).write' => '[removed]',
'.parentNode' => '[removed]',
'.innerHTML' => '[removed]',
'-moz-binding' => '[removed]',
'<!--' => '&lt;!--',
'-->' => '--&gt;',
'<![CDATA[' => '&lt;![CDATA[',
'<comment>' => '&lt;comment&gt;',
'<%' => '&lt;&#37;'
);
/**
* List of never allowed regex replacements
*
* @var array
*/
protected $_never_allowed_regex = array(
'javascript\s*:',
'(\(?document\)?|\(?window\)?(\.document)?)\.(location|on\w*)',
'expression\s*(\(|&\#40;)', // CSS and IE
'vbscript\s*:', // IE, surprise!
'wscript\s*:', // IE
'jscript\s*:', // IE
'vbs\s*:', // IE
'Redirect\s+30\d',
"([\"'])?data\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?"
);
protected $options = [
'placeholder' => '[removed]'
];
/**
* Class constructor
*
* @return void
*/
public function __construct($options = [])
{
$this->options = array_merge($this->options, $options);
foreach ($this->_never_allowed_str as $index => &$item) {
$item = str_replace('[removed]', $this->options['placeholder'], $item);
}
}
/**
*
* @param array $options 参数
* @return Security
*/
public static function instance($options = [])
{
if (is_null(self::$instance)) {
self::$instance = new static($options);
}
return self::$instance;
}
/**
* XSS Clean
*
* Sanitizes data so that Cross Site Scripting Hacks can be
* prevented. This method does a fair amount of work but
* it is extremely thorough, designed to prevent even the
* most obscure XSS attempts. Nothing is ever 100% foolproof,
* of course, but I haven't been able to get anything passed
* the filter.
*
* Note: Should only be used to deal with data upon submission.
* It's not something that should be used for general
* runtime processing.
*
* @link http://channel.bitflux.ch/wiki/XSS_Prevention
* Based in part on some code and ideas from Bitflux.
*
* @link http://ha.ckers.org/xss.html
* To help develop this script I used this great list of
* vulnerabilities along with a few other hacks I've
* harvested from examining vulnerabilities in other programs.
*
* @param string|string[] $str Input data
* @param bool $is_image Whether the input is an image
* @return string
*/
public function xss_clean($str, $is_image = false)
{
// Is the string an array?
if (is_array($str)) {
foreach ($str as $key => &$value) {
$str[$key] = $this->xss_clean($value);
}
return $str;
}
// Remove Invisible Characters
$str = $this->remove_invisible_characters($str);
/*
* URL Decode
*
* Just in case stuff like this is submitted:
*
* <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
*
* Note: Use rawurldecode() so it does not remove plus signs
*/
if (stripos($str, '%') !== false) {
do {
$oldstr = $str;
$str = rawurldecode($str);
$str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str);
} while ($oldstr !== $str);
unset($oldstr);
}
/*
* Convert character entities to ASCII
*
* This permits our tests below to work reliably.
* We only convert entities that are within tags since
* these are the ones that will pose security problems.
*/
$str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
$str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str);
// Remove Invisible Characters Again!
$str = $this->remove_invisible_characters($str);
/*
* Convert all tabs to spaces
*
* This prevents strings like this: ja vascript
* NOTE: we deal with spaces between characters later.
* NOTE: preg_replace was found to be amazingly slow here on
* large blocks of data, so we use str_replace.
*/
$str = str_replace("\t", ' ', $str);
// Capture converted string for later comparison
$converted_string = $str;
// Remove Strings that are never allowed
$str = $this->_do_never_allowed($str);
/*
* Makes PHP tags safe
*
* Note: XML tags are inadvertently replaced too:
*
* <?xml
*
* But it doesn't seem to pose a problem.
*/
if ($is_image === true) {
// Images have a tendency to have the PHP short opening and
// closing tags every so often so we skip those and only
// do the long opening tags.
$str = preg_replace('/<\?(php)/i', '&lt;?\\1', $str);
} else {
$str = str_replace(array('<?', '?' . '>'), array('&lt;?', '?&gt;'), $str);
}
/*
* Compact any exploded words
*
* This corrects words like: j a v a s c r i p t
* These words are compacted back to their correct state.
*/
$words = array(
'javascript',
'expression',
'vbscript',
'jscript',
'wscript',
'vbs',
'script',
'base64',
'applet',
'alert',
'document',
'write',
'cookie',
'window',
'confirm',
'prompt',
'eval'
);
foreach ($words as $word) {
$word = implode('\s*', str_split($word)) . '\s*';
// We only want to do this when it is followed by a non-word character
// That way valid stuff like "dealer to" does not become "dealerto"
$str = preg_replace_callback('#(' . substr($word, 0, -3) . ')(\W)#is', array($this, '_compact_exploded_words'), $str);
}
/*
* Remove disallowed Javascript in links or img tags
* We used to do some version comparisons and use of stripos(),
* but it is dog slow compared to these simplified non-capturing
* preg_match(), especially if the pattern exists in the string
*
* Note: It was reported that not only space characters, but all in
* the following pattern can be parsed as separators between a tag name
* and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C]
* ... however, $this->remove_invisible_characters() above already strips the
* hex-encoded ones, so we'll skip them below.
*/
do {
$original = $str;
if (preg_match('/<a/i', $str)) {
$str = preg_replace_callback('#<a(?:rea)?[^a-z0-9>]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str);
}
if (preg_match('/<img/i', $str)) {
$str = preg_replace_callback('#<img[^a-z0-9]+([^>]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str);
}
if (preg_match('/script|xss/i', $str)) {
$str = preg_replace('#</*(?:script|xss).*?>#si', $this->options['placeholder'], $str);
}
} while ($original !== $str);
unset($original);
/*
* Sanitize naughty HTML elements
*
* If a tag containing any of the words in the list
* below is found, the tag gets converted to entities.
*
* So this: <blink>
* Becomes: &lt;blink&gt;
*/
$pattern = '#'
. '<((?<slash>/*\s*)((?<tagName>[a-z0-9]+)(?=[^a-z0-9]|$)|.+)' // tag start and name, followed by a non-tag character
. '[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator
// optional attributes
. '(?<attributes>(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons
. '[^\s\042\047>/=]+' // attribute characters
// optional attribute-value
. '(?:\s*=' // attribute-value separator
. '(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value
. ')?' // end optional attribute-value group
. ')*)' // end optional attributes group
. '[^>]*)(?<closeTag>\>)?#isS';
// Note: It would be nice to optimize this for speed, BUT
// only matching the naughty elements here results in
// false positives and in turn - vulnerabilities!
do {
$old_str = $str;
$str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str);
} while ($old_str !== $str);
unset($old_str);
/*
* Sanitize naughty scripting elements
*
* Similar to above, only instead of looking for
* tags it looks for PHP and JavaScript commands
* that are disallowed. Rather than removing the
* code, it simply converts the parenthesis to entities
* rendering the code un-executable.
*
* For example: eval('some code')
* Becomes: eval&#40;'some code'&#41;
*/
$str = preg_replace(
'#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si',
'\\1\\2&#40;\\3&#41;',
$str
);
// Same thing, but for "tag functions" (e.g. eval`some code`)
$str = preg_replace(
'#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si',
'\\1\\2&#96;\\3&#96;',
$str
);
// Final clean up
// This adds a bit of extra precaution in case
// something got through the above filters
$str = $this->_do_never_allowed($str);
/*
* Images are Handled in a Special Way
* - Essentially, we want to know that after all of the character
* conversion is done whether any unwanted, likely XSS, code was found.
* If not, we return TRUE, as the image is clean.
* However, if the string post-conversion does not matched the
* string post-removal of XSS, then it fails, as there was unwanted XSS
* code found and removed/changed during processing.
*/
if ($is_image === true) {
return ($str === $converted_string);
}
return $str;
}
// --------------------------------------------------------------------
/**
* XSS Hash
*
* Generates the XSS hash if needed and returns it.
*
* @return string XSS hash
*/
public function xss_hash()
{
if ($this->_xss_hash === null) {
$rand = $this->get_random_bytes(16);
$this->_xss_hash = ($rand === false)
? md5(uniqid(mt_rand(), true))
: bin2hex($rand);
}
return $this->_xss_hash;
}
// --------------------------------------------------------------------
/**
* Get random bytes
*
* @param int $length Output length
* @return string
*/
public function get_random_bytes($length)
{
if (empty($length) or !ctype_digit((string)$length)) {
return false;
}
if (function_exists('random_bytes')) {
try {
// The cast is required to avoid TypeError
return random_bytes((int)$length);
} catch (Exception $e) {
// If random_bytes() can't do the job, we can't either ...
// There's no point in using fallbacks.
return false;
}
}
// Unfortunately, none of the following PRNGs is guaranteed to exist ...
if (defined('MCRYPT_DEV_URANDOM') && ($output = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)) !== false) {
return $output;
}
if (is_readable('/dev/urandom') && ($fp = fopen('/dev/urandom', 'rb')) !== false) {
// Try not to waste entropy ...
stream_set_chunk_size($fp, $length);
$output = fread($fp, $length);
fclose($fp);
if ($output !== false) {
return $output;
}
}
if (function_exists('openssl_random_pseudo_bytes')) {
return openssl_random_pseudo_bytes($length);
}
return false;
}
// --------------------------------------------------------------------
/**
* HTML Entities Decode
*
* A replacement for html_entity_decode()
*
* The reason we are not using html_entity_decode() by itself is because
* while it is not technically correct to leave out the semicolon
* at the end of an entity most browsers will still interpret the entity
* correctly. html_entity_decode() does not convert entities without
* semicolons, so we are left with our own little solution here. Bummer.
*
* @link https://secure.php.net/html-entity-decode
*
* @param string $str Input
* @param string $charset Character set
* @return string
*/
public function entity_decode($str, $charset = null)
{
if (strpos($str, '&') === false) {
return $str;
}
static $_entities;
isset($charset) or $charset = $this->charset;
isset($_entities) or $_entities = array_map('strtolower', get_html_translation_table(HTML_ENTITIES, ENT_COMPAT | ENT_HTML5, $charset));
do {
$str_compare = $str;
// Decode standard entities, avoiding false positives
if (preg_match_all('/&[a-z]{2,}(?![a-z;])/i', $str, $matches)) {
$replace = array();
$matches = array_unique(array_map('strtolower', $matches[0]));
foreach ($matches as &$match) {
if (($char = array_search($match . ';', $_entities, true)) !== false) {
$replace[$match] = $char;
}
}
$str = str_replace(array_keys($replace), array_values($replace), $str);
}
// Decode numeric & UTF16 two byte entities
$str = html_entity_decode(
preg_replace('/(&#(?:x0*[0-9a-f]{2,5}(?![0-9a-f;])|(?:0*\d{2,4}(?![0-9;]))))/iS', '$1;', $str),
ENT_COMPAT | ENT_HTML5,
$charset
);
} while ($str_compare !== $str);
return $str;
}
// --------------------------------------------------------------------
/**
* Sanitize Filename
*
* @param string $str Input file name
* @param bool $relative_path Whether to preserve paths
* @return string
*/
public function sanitize_filename($str, $relative_path = false)
{
$bad = $this->filename_bad_chars;
if (!$relative_path) {
$bad[] = './';
$bad[] = '/';
}
$str = $this->remove_invisible_characters($str, false);
do {
$old = $str;
$str = str_replace($bad, '', $str);
} while ($old !== $str);
return stripslashes($str);
}
// ----------------------------------------------------------------
/**
* Strip Image Tags
*
* @param string $str
* @return string
*/
public function strip_image_tags($str)
{
return preg_replace(
array(
'#<img[\s/]+.*?src\s*=\s*(["\'])([^\\1]+?)\\1.*?\>#i',
'#<img[\s/]+.*?src\s*=\s*?(([^\s"\'=<>`]+)).*?\>#i'
),
'\\2',
$str
);
}
// ----------------------------------------------------------------
/**
* URL-decode taking spaces into account
*
* @param array $matches
* @return string
*/
protected function _urldecodespaces($matches)
{
$input = $matches[0];
$nospaces = preg_replace('#\s+#', '', $input);
return ($nospaces === $input)
? $input
: rawurldecode($nospaces);
}
// ----------------------------------------------------------------
/**
* Compact Exploded Words
*
* Callback method for xss_clean() to remove whitespace from
* things like 'j a v a s c r i p t'.
*
* @param array $matches
* @return string
*/
protected function _compact_exploded_words($matches)
{
return preg_replace('/\s+/s', '', $matches[1]) . $matches[2];
}
// --------------------------------------------------------------------
/**
* Sanitize Naughty HTML
*
* Callback method for xss_clean() to remove naughty HTML elements.
*
* @param array $matches
* @return string
*/
protected function _sanitize_naughty_html($matches)
{
static $naughty_tags = array(
'alert',
'area',
'prompt',
'confirm',
'applet',
'audio',
'basefont',
'base',
'behavior',
'bgsound',
'blink',
'body',
'embed',
'expression',
'form',
'frameset',
'frame',
'head',
'html',
'ilayer',
'iframe',
'input',
'button',
'select',
'isindex',
'layer',
'link',
'meta',
'keygen',
'object',
'plaintext',
'style',
'script',
'textarea',
'title',
'math',
'video',
'svg',
'xml',
'xss'
);
static $evil_attributes = array(
'on\w+',
'style',
'xmlns',
'formaction',
'form',
'xlink:href',
'FSCommand',
'seekSegmentTime'
);
// First, escape unclosed tags
if (empty($matches['closeTag'])) {
return '&lt;' . $matches[1];
} // Is the element that we caught naughty? If so, escape it
elseif (in_array(strtolower($matches['tagName']), $naughty_tags, true)) {
return '&lt;' . $matches[1] . '&gt;';
} // For other tags, see if their attributes are "evil" and strip those
elseif (isset($matches['attributes'])) {
// We'll store the already filtered attributes here
$attributes = array();
// Attribute-catching pattern
$attributes_pattern = '#'
. '(?<name>[^\s\042\047>/=]+)' // attribute characters
// optional attribute-value
. '(?:\s*=(?<value>[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*)))' // attribute-value separator
. '#i';
// Blacklist pattern for evil attribute names
$is_evil_pattern = '#^(' . implode('|', $evil_attributes) . ')$#i';
// Each iteration filters a single attribute
do {
// Strip any non-alpha characters that may precede an attribute.
// Browsers often parse these incorrectly and that has been a
// of numerous XSS issues we've had.
$matches['attributes'] = preg_replace('#^[^a-z]+#i', '', $matches['attributes']);
if (!preg_match($attributes_pattern, $matches['attributes'], $attribute, PREG_OFFSET_CAPTURE)) {
// No (valid) attribute found? Discard everything else inside the tag
break;
}
if (
// Is it indeed an "evil" attribute?
preg_match($is_evil_pattern, $attribute['name'][0])
// Or does it have an equals sign, but no value and not quoted? Strip that too!
or (trim($attribute['value'][0]) === '')
) {
$attributes[] = 'xss=removed';
} else {
$attributes[] = $attribute[0][0];
}
$matches['attributes'] = substr($matches['attributes'], $attribute[0][1] + strlen($attribute[0][0]));
} while ($matches['attributes'] !== '');
$attributes = empty($attributes)
? ''
: ' ' . implode(' ', $attributes);
return '<' . $matches['slash'] . $matches['tagName'] . $attributes . '>';
}
return $matches[0];
}
// --------------------------------------------------------------------
/**
* JS Link Removal
*
* Callback method for xss_clean() to sanitize links.
*
* This limits the PCRE backtracks, making it more performance friendly
* and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
* PHP 5.2+ on link-heavy strings.
*
* @param array $match
* @return string
*/
protected function _js_link_removal($match)
{
return str_replace(
$match[1],
preg_replace(
'#href=.*?(?:(?:alert|prompt|confirm)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|<script|<xss|d\s*a\s*t\s*a\s*:)#si',
'',
$this->_filter_attributes($match[1])
),
$match[0]
);
}
// --------------------------------------------------------------------
/**
* JS Image Removal
*
* Callback method for xss_clean() to sanitize image tags.
*
* This limits the PCRE backtracks, making it more performance friendly
* and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
* PHP 5.2+ on image tag heavy strings.
*
* @param array $match
* @return string
*/
protected function _js_img_removal($match)
{
return str_replace(
$match[1],
preg_replace(
'#src=.*?(?:(?:alert|prompt|confirm|eval)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|<script|<xss|base64\s*,)#si',
'',
$this->_filter_attributes($match[1])
),
$match[0]
);
}
// --------------------------------------------------------------------
/**
* Attribute Conversion
*
* @param array $match
* @return string
*/
protected function _convert_attribute($match)
{
return str_replace(array('>', '<', '\\'), array('&gt;', '&lt;', '\\\\'), $match[0]);
}
// --------------------------------------------------------------------
/**
* Filter Attributes
*
* Filters tag attributes for consistency and safety.
*
* @param string $str
* @return string
*/
protected function _filter_attributes($str)
{
$out = '';
if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) {
foreach ($matches[0] as $match) {
$out .= preg_replace('#/\*.*?\*/#s', '', $match);
}
}
return $out;
}
// --------------------------------------------------------------------
/**
* HTML Entity Decode Callback
*
* @param array $match
* @return string
*/
protected function _decode_entity($match)
{
// Protect GET variables in URLs
// 901119URL5918AMP18930PROTECT8198
$match = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-/]+)|i', $this->xss_hash() . '\\1=\\2', $match[0]);
// Decode, then un-protect URL GET vars
return str_replace(
$this->xss_hash(),
'&',
$this->entity_decode($match, $this->charset)
);
}
// --------------------------------------------------------------------
/**
* Do Never Allowed
*
* @param string
* @return string
*/
protected function _do_never_allowed($str)
{
$str = str_replace(array_keys($this->_never_allowed_str), $this->_never_allowed_str, $str);
foreach ($this->_never_allowed_regex as $regex) {
$str = preg_replace('#' . $regex . '#is', $this->options['placeholder'], $str);
}
return $str;
}
/**
* Remove Invisible Characters
*/
public function remove_invisible_characters($str, $url_encoded = true)
{
$non_displayables = array();
// every control character except newline (dec 10),
// carriage return (dec 13) and horizontal tab (dec 09)
if ($url_encoded) {
$non_displayables[] = '/%0[0-8bcef]/i'; // url encoded 00-08, 11, 12, 14, 15
$non_displayables[] = '/%1[0-9a-f]/i'; // url encoded 16-31
$non_displayables[] = '/%7f/i'; // url encoded 127
}
$non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127
do {
$str = preg_replace($non_displayables, '', $str, -1, $count);
} while ($count);
return $str;
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace app\common\library;
use fast\Random;
use think\Hook;
/**
* 短信验证码类
*/
class Sms
{
/**
* 验证码有效时长
* @var int
*/
protected static $expire = 120;
/**
* 最大允许检测的次数
* @var int
*/
protected static $maxCheckNums = 10;
/**
* 获取最后一次手机发送的数据
*
* @param int $mobile 手机号
* @param string $event 事件
* @return Sms
*/
public static function get($mobile, $event = 'default')
{
$sms = \app\common\model\Sms::where(['mobile' => $mobile, 'event' => $event])
->order('id', 'DESC')
->find();
Hook::listen('sms_get', $sms, null, true);
return $sms ?: null;
}
/**
* 发送验证码
*
* @param int $mobile 手机号
* @param int $code 验证码,为空时将自动生成4位数字
* @param string $event 事件
* @return boolean
*/
public static function send($mobile, $code = null, $event = 'default')
{
$code = is_null($code) ? Random::numeric(config('captcha.length')) : $code;
$time = time();
$ip = request()->ip();
$sms = \app\common\model\Sms::create(['event' => $event, 'mobile' => $mobile, 'code' => $code, 'ip' => $ip, 'createtime' => $time]);
$result = Hook::listen('sms_send', $sms, null, true);
if (!$result) {
$sms->delete();
return false;
}
return true;
}
/**
* 发送通知
*
* @param mixed $mobile 手机号,多个以,分隔
* @param string $msg 消息内容
* @param string $template 消息模板
* @return boolean
*/
public static function notice($mobile, $msg = '', $template = null)
{
$params = [
'mobile' => $mobile,
'msg' => $msg,
'template' => $template
];
$result = Hook::listen('sms_notice', $params, null, true);
return (bool)$result;
}
/**
* 校验验证码
*
* @param int $mobile 手机号
* @param int $code 验证码
* @param string $event 事件
* @return boolean
*/
public static function check($mobile, $code, $event = 'default')
{
$time = time() - self::$expire;
$sms = \app\common\model\Sms::where(['mobile' => $mobile, 'event' => $event])
->order('id', 'DESC')
->find();
if ($sms) {
if ($sms['createtime'] > $time && $sms['times'] <= self::$maxCheckNums) {
$correct = $code == $sms['code'];
if (!$correct) {
$sms->times = $sms->times + 1;
$sms->save();
return false;
} else {
$result = Hook::listen('sms_check', $sms, null, true);
return $result;
}
} else {
// 过期则清空该手机验证码
self::flush($mobile, $event);
return false;
}
} else {
return false;
}
}
/**
* 清空指定手机号验证码
*
* @param int $mobile 手机号
* @param string $event 事件
* @return boolean
*/
public static function flush($mobile, $event = 'default')
{
\app\common\model\Sms::where(['mobile' => $mobile, 'event' => $event])
->delete();
Hook::listen('sms_flush');
return true;
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace app\common\library;
use app\common\library\token\Driver;
use think\App;
use think\Config;
use think\Log;
/**
* Token操作类
*/
class Token
{
/**
* @var array Token的实例
*/
public static $instance = [];
/**
* @var object 操作句柄
*/
public static $handler;
/**
* 连接Token驱动
* @access public
* @param array $options 配置数组
* @param bool|string $name Token连接标识 true 强制重新连接
* @return Driver
*/
public static function connect(array $options = [], $name = false)
{
$type = !empty($options['type']) ? $options['type'] : 'File';
if (false === $name) {
$name = md5(serialize($options));
}
if (true === $name || !isset(self::$instance[$name])) {
$class = false === strpos($type, '\\') ?
'\\app\\common\\library\\token\\driver\\' . ucwords($type) :
$type;
// 记录初始化信息
App::$debug && Log::record('[ TOKEN ] INIT ' . $type, 'info');
if (true === $name) {
return new $class($options);
}
self::$instance[$name] = new $class($options);
}
return self::$instance[$name];
}
/**
* 自动初始化Token
* @access public
* @param array $options 配置数组
* @return Driver
*/
public static function init(array $options = [])
{
if (is_null(self::$handler)) {
if (empty($options) && 'complex' == Config::get('token.type')) {
$default = Config::get('token.default');
// 获取默认Token配置并连接
$options = Config::get('token.' . $default['type']) ?: $default;
} elseif (empty($options)) {
$options = Config::get('token');
}
self::$handler = self::connect($options);
}
return self::$handler;
}
/**
* 判断Token是否可用(check别名)
* @access public
* @param string $token Token标识
* @param int $user_id 会员ID
* @return bool
*/
public static function has($token, $user_id)
{
return self::check($token, $user_id);
}
/**
* 判断Token是否可用
* @param string $token Token标识
* @param int $user_id 会员ID
* @return bool
*/
public static function check($token, $user_id)
{
return self::init()->check($token, $user_id);
}
/**
* 读取Token
* @access public
* @param string $token Token标识
* @param mixed $default 默认值
* @return mixed
*/
public static function get($token, $default = false)
{
return self::init()->get($token) ?: $default;
}
/**
* 写入Token
* @access public
* @param string $token Token标识
* @param mixed $user_id 会员ID
* @param int|null $expire 有效时间 0为永久
* @return boolean
*/
public static function set($token, $user_id, $expire = null)
{
return self::init()->set($token, $user_id, $expire);
}
/**
* 删除Token(delete别名)
* @access public
* @param string $token Token标识
* @return boolean
*/
public static function rm($token)
{
return self::delete($token);
}
/**
* 删除Token
* @param string $token 标签名
* @return bool
*/
public static function delete($token)
{
return self::init()->delete($token);
}
/**
* 清除Token
* @access public
* @param int $user_id 会员ID
* @return boolean
*/
public static function clear($user_id = null)
{
return self::init()->clear($user_id);
}
}

View File

@@ -0,0 +1,447 @@
<?php
namespace app\common\library;
use app\common\exception\UploadException;
use app\common\model\Attachment;
use fast\Random;
use FilesystemIterator;
use think\Config;
use think\File;
use think\Hook;
/**
* 文件上传类
*/
class Upload
{
protected $merging = false;
protected $chunkDir = null;
protected $config = [];
protected $error = '';
/**
* @var File
*/
protected $file = null;
protected $fileInfo = null;
public function __construct($file = null)
{
$this->config = Config::get('upload');
$this->chunkDir = RUNTIME_PATH . 'chunks';
if ($file) {
$this->setFile($file);
}
}
/**
* 设置分片目录
* @param $dir
*/
public function setChunkDir($dir)
{
$this->chunkDir = $dir;
}
/**
* 获取文件
* @return File
*/
public function getFile()
{
return $this->file;
}
/**
* 设置文件
* @param $file
* @throws UploadException
*/
public function setFile($file)
{
if (empty($file)) {
throw new UploadException(__('No file upload or server upload limit exceeded'));
}
$fileInfo = $file->getInfo();
$suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
$fileInfo['suffix'] = $suffix;
$fileInfo['imagewidth'] = 0;
$fileInfo['imageheight'] = 0;
$this->file = $file;
$this->fileInfo = $fileInfo;
$this->checkExecutable();
}
/**
* 检测是否为可执行脚本
* @return bool
* @throws UploadException
*/
protected function checkExecutable()
{
//禁止上传以.开头的文件
if (substr($this->fileInfo['name'], 0, 1) === '.') {
throw new UploadException(__('Uploaded file format is limited'));
}
//禁止上传PHP和HTML文件
if (in_array($this->fileInfo['type'], ['text/x-php', 'text/html']) || in_array($this->fileInfo['suffix'], ['php', 'html', 'htm', 'phar', 'phtml']) || preg_match("/^php(.*)/i", $this->fileInfo['suffix'])) {
throw new UploadException(__('Uploaded file format is limited'));
}
return true;
}
/**
* 检测文件类型
* @return bool
* @throws UploadException
*/
protected function checkMimetype()
{
$mimetypeArr = explode(',', strtolower($this->config['mimetype']));
$typeArr = explode('/', $this->fileInfo['type']);
//Mimetype值不正确
if (stripos($this->fileInfo['type'], '/') === false) {
throw new UploadException(__('Uploaded file format is limited'));
}
//验证文件后缀
if (in_array($this->fileInfo['suffix'], $mimetypeArr) || in_array('.' . $this->fileInfo['suffix'], $mimetypeArr)
|| in_array($typeArr[0] . "/*", $mimetypeArr) || (in_array($this->fileInfo['type'], $mimetypeArr) && stripos($this->fileInfo['type'], '/') !== false)) {
return true;
}
throw new UploadException(__('Uploaded file format is limited'));
}
/**
* 检测是否图片
* @param bool $force
* @return bool
* @throws UploadException
*/
protected function checkImage($force = false)
{
//验证是否为图片文件
if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) {
$imgInfo = getimagesize($this->fileInfo['tmp_name']);
if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
throw new UploadException(__('Uploaded file is not a valid image'));
}
$this->fileInfo['imagewidth'] = $imgInfo[0] ?? 0;
$this->fileInfo['imageheight'] = $imgInfo[1] ?? 0;
return true;
} else {
return !$force;
}
}
/**
* 检测文件大小
* @throws UploadException
*/
protected function checkSize()
{
preg_match('/([0-9\.]+)(\w+)/', $this->config['maxsize'], $matches);
$size = $matches ? $matches[1] : $this->config['maxsize'];
$type = $matches ? strtolower($matches[2]) : 'b';
$typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
$size = (int)($size * pow(1024, $typeDict[$type] ?? 0));
if ($this->fileInfo['size'] > $size) {
throw new UploadException(__(
'File is too big (%sMiB), Max filesize: %sMiB.',
round($this->fileInfo['size'] / pow(1024, 2), 2),
round($size / pow(1024, 2), 2)
));
}
}
/**
* 获取后缀
* @return string
*/
public function getSuffix()
{
return $this->fileInfo['suffix'] ?: 'file';
}
/**
* 获取存储的文件名
* @param string $savekey 保存路径
* @param string $filename 文件名
* @param string $md5 文件MD5
* @param string $category 分类
* @return mixed|null
*/
public function getSavekey($savekey = null, $filename = null, $md5 = null, $category = null)
{
if ($filename) {
$suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
} else {
$suffix = $this->fileInfo['suffix'] ?? '';
}
$suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
$filename = $filename ? $filename : ($this->fileInfo['name'] ?? 'unknown');
$filename = xss_clean(strip_tags(htmlspecialchars($filename)));
$fileprefix = substr($filename, 0, strripos($filename, '.'));
$md5 = $md5 ? $md5 : (isset($this->fileInfo['tmp_name']) ? md5_file($this->fileInfo['tmp_name']) : '');
$category = $category ? $category : request()->post('category');
$category = $category ? xss_clean($category) : 'all';
$replaceArr = [
'{year}' => date("Y"),
'{mon}' => date("m"),
'{day}' => date("d"),
'{hour}' => date("H"),
'{min}' => date("i"),
'{sec}' => date("s"),
'{random}' => Random::alnum(16),
'{random32}' => Random::alnum(32),
'{category}' => $category ? $category : '',
'{filename}' => substr($filename, 0, 100),
'{fileprefix}' => substr($fileprefix, 0, 100),
'{suffix}' => $suffix,
'{.suffix}' => $suffix ? '.' . $suffix : '',
'{filemd5}' => $md5,
];
$savekey = $savekey ? $savekey : $this->config['savekey'];
$savekey = str_replace(array_keys($replaceArr), array_values($replaceArr), $savekey);
return $savekey;
}
/**
* 清理分片文件
* @param $chunkid
*/
public function clean($chunkid)
{
if (!preg_match('/^[a-z0-9\-]{36}$/', $chunkid)) {
throw new UploadException(__('Invalid parameters'));
}
$iterator = new \GlobIterator($this->chunkDir . DS . $chunkid . '-*', FilesystemIterator::KEY_AS_FILENAME);
$array = iterator_to_array($iterator);
foreach ($array as $index => &$item) {
$sourceFile = $item->getRealPath() ?: $item->getPathname();
$item = null;
@unlink($sourceFile);
}
}
/**
* 合并分片文件
* @param string $chunkid
* @param int $chunkcount
* @param string $filename
* @return attachment|\think\Model
* @throws UploadException
*/
public function merge($chunkid, $chunkcount, $filename)
{
if (!preg_match('/^[a-z0-9\-]{36}$/', $chunkid)) {
throw new UploadException(__('Invalid parameters'));
}
$filePath = $this->chunkDir . DS . $chunkid;
$completed = true;
//检查所有分片是否都存在
for ($i = 0; $i < $chunkcount; $i++) {
if (!file_exists("{$filePath}-{$i}.part")) {
$completed = false;
break;
}
}
if (!$completed) {
$this->clean($chunkid);
throw new UploadException(__('Chunk file info error'));
}
//如果所有文件分片都上传完毕,开始合并
$uploadPath = $filePath;
if (!$destFile = @fopen($uploadPath, "wb")) {
$this->clean($chunkid);
throw new UploadException(__('Chunk file merge error'));
}
if (flock($destFile, LOCK_EX)) { // 进行排他型锁定
for ($i = 0; $i < $chunkcount; $i++) {
$partFile = "{$filePath}-{$i}.part";
if (!$handle = @fopen($partFile, "rb")) {
break;
}
while ($buff = fread($handle, filesize($partFile))) {
fwrite($destFile, $buff);
}
@fclose($handle);
@unlink($partFile); //删除分片
}
flock($destFile, LOCK_UN);
}
@fclose($destFile);
$attachment = null;
try {
$file = new File($uploadPath);
$info = [
'name' => $filename,
'type' => $file->getMime(),
'tmp_name' => $uploadPath,
'error' => 0,
'size' => $file->getSize()
];
$file->setSaveName($filename)->setUploadInfo($info);
$file->isTest(true);
//重新设置文件
$this->setFile($file);
unset($file);
$this->merging = true;
//允许大文件
$this->config['maxsize'] = "1024G";
$attachment = $this->upload();
} catch (\Exception $e) {
@unlink($uploadPath);
throw new UploadException($e->getMessage());
}
return $attachment;
}
/**
* 分片上传
* @throws UploadException
*/
public function chunk($chunkid, $chunkindex, $chunkcount, $chunkfilesize = null, $chunkfilename = null, $direct = false)
{
if ($this->fileInfo['type'] != 'application/octet-stream') {
throw new UploadException(__('Uploaded file format is limited'));
}
if (!preg_match('/^[a-z0-9\-]{36}$/', $chunkid)) {
throw new UploadException(__('Invalid parameters'));
}
$destDir = RUNTIME_PATH . 'chunks';
$fileName = $chunkid . "-" . $chunkindex . '.part';
$destFile = $destDir . DS . $fileName;
if (!is_dir($destDir)) {
@mkdir($destDir, 0755, true);
}
if (!move_uploaded_file($this->file->getPathname(), $destFile)) {
throw new UploadException(__('Chunk file write error'));
}
$file = new File($destFile);
$info = [
'name' => $fileName,
'type' => $file->getMime(),
'tmp_name' => $destFile,
'error' => 0,
'size' => $file->getSize()
];
$file->setSaveName($fileName)->setUploadInfo($info);
$this->setFile($file);
return $file;
}
/**
* 普通上传
* @return \app\common\model\attachment|\think\Model
* @throws UploadException
*/
public function upload($savekey = null)
{
if (empty($this->file)) {
throw new UploadException(__('No file upload or server upload limit exceeded'));
}
$this->checkSize();
$this->checkExecutable();
$this->checkMimetype();
$this->checkImage();
$savekey = $savekey ? $savekey : $this->getSavekey();
$savekey = '/' . ltrim($savekey, '/');
$uploadDir = substr($savekey, 0, strripos($savekey, '/') + 1);
$fileName = substr($savekey, strripos($savekey, '/') + 1);
$destDir = ROOT_PATH . 'public' . str_replace('/', DS, $uploadDir);
$sha1 = $this->file->hash();
//如果是合并文件
if ($this->merging) {
if (!$this->file->check()) {
throw new UploadException($this->file->getError());
}
$destFile = $destDir . $fileName;
$sourceFile = $this->file->getRealPath() ?: $this->file->getPathname();
$info = $this->file->getInfo();
$this->file = null;
if (!is_dir($destDir)) {
@mkdir($destDir, 0755, true);
}
rename($sourceFile, $destFile);
$file = new File($destFile);
$file->setSaveName($fileName)->setUploadInfo($info);
} else {
$file = $this->file->move($destDir, $fileName);
if (!$file) {
// 上传失败获取错误信息
throw new UploadException($this->file->getError());
}
}
$this->file = $file;
$category = request()->post('category');
$category = array_key_exists($category, config('site.attachmentcategory') ?? []) ? $category : '';
$auth = Auth::instance();
$params = array(
'admin_id' => (int)session('admin.id'),
'user_id' => (int)$auth->id,
'filename' => mb_substr(htmlspecialchars(strip_tags($this->fileInfo['name'])), 0, 100),
'category' => $category,
'filesize' => $this->fileInfo['size'],
'imagewidth' => $this->fileInfo['imagewidth'],
'imageheight' => $this->fileInfo['imageheight'],
'imagetype' => $this->fileInfo['suffix'],
'imageframes' => 0,
'mimetype' => $this->fileInfo['type'],
'url' => $uploadDir . $file->getSaveName(),
'uploadtime' => time(),
'storage' => 'local',
'sha1' => $sha1,
'extparam' => '',
);
$attachment = new Attachment();
$attachment->data(array_filter($params));
$attachment->save();
\think\Hook::listen("upload_after", $attachment);
return $attachment;
}
/**
* 设置错误信息
* @param $msg
*/
public function setError($msg)
{
$this->error = $msg;
}
/**
* 获取错误信息
* @return string
*/
public function getError()
{
return $this->error;
}
}

View File

@@ -0,0 +1,92 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace app\common\library\token;
/**
* Token基础类
*/
abstract class Driver
{
protected $handler = null;
protected $options = [];
/**
* 存储Token
* @param string $token Token
* @param int $user_id 会员ID
* @param int $expire 过期时长,0表示无限,单位秒
* @return bool
*/
abstract function set($token, $user_id, $expire = 0);
/**
* 获取Token内的信息
* @param string $token
* @return array
*/
abstract function get($token);
/**
* 判断Token是否可用
* @param string $token Token
* @param int $user_id 会员ID
* @return boolean
*/
abstract function check($token, $user_id);
/**
* 删除Token
* @param string $token
* @return boolean
*/
abstract function delete($token);
/**
* 删除指定用户的所有Token
* @param int $user_id
* @return boolean
*/
abstract function clear($user_id);
/**
* 返回句柄对象,可执行其它高级方法
*
* @access public
* @return object
*/
public function handler()
{
return $this->handler;
}
/**
* 获取加密后的Token
* @param string $token Token标识
* @return string
*/
protected function getEncryptedToken($token)
{
$config = \think\Config::get('token');
$token = $token ?? ''; // 为兼容 php8
return hash_hmac($config['hashalgo'], $token, $config['key']);
}
/**
* 获取过期剩余时长
* @param $expiretime
* @return float|int|mixed
*/
protected function getExpiredIn($expiretime)
{
return $expiretime ? max(0, $expiretime - time()) : 365 * 86400;
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace app\common\library\token\driver;
use app\common\library\token\Driver;
/**
* Token操作类
*/
class Mysql extends Driver
{
/**
* 默认配置
* @var array
*/
protected $options = [
'table' => 'user_token',
'expire' => 2592000,
'connection' => [],
];
/**
* 构造函数
* @param array $options 参数
* @access public
*/
public function __construct($options = [])
{
if (!empty($options)) {
$this->options = array_merge($this->options, $options);
}
if ($this->options['connection']) {
$this->handler = \think\Db::connect($this->options['connection'])->name($this->options['table']);
} else {
$this->handler = \think\Db::name($this->options['table']);
}
$time = time();
$tokentime = cache('tokentime');
if (!$tokentime || $tokentime < $time - 86400) {
cache('tokentime', $time);
$this->handler->where('expiretime', '<', $time)->where('expiretime', '>', 0)->delete();
}
}
/**
* 存储Token
* @param string $token Token
* @param int $user_id 会员ID
* @param int $expire 过期时长,0表示无限,单位秒
* @return bool
*/
public function set($token, $user_id, $expire = null)
{
$expiretime = !is_null($expire) && $expire !== 0 ? time() + $expire : 0;
$token = $this->getEncryptedToken($token);
$this->handler->insert(['token' => $token, 'user_id' => $user_id, 'createtime' => time(), 'expiretime' => $expiretime]);
return true;
}
/**
* 获取Token内的信息
* @param string $token
* @return array
*/
public function get($token)
{
$data = $this->handler->where('token', $this->getEncryptedToken($token))->find();
if ($data) {
if (!$data['expiretime'] || $data['expiretime'] > time()) {
//返回未加密的token给客户端使用
$data['token'] = $token;
//返回剩余有效时间
$data['expires_in'] = $this->getExpiredIn($data['expiretime']);
return $data;
} else {
self::delete($token);
}
}
return [];
}
/**
* 判断Token是否可用
* @param string $token Token
* @param int $user_id 会员ID
* @return boolean
*/
public function check($token, $user_id)
{
$data = $this->get($token);
return $data && $data['user_id'] == $user_id ? true : false;
}
/**
* 删除Token
* @param string $token
* @return boolean
*/
public function delete($token)
{
$this->handler->where('token', $this->getEncryptedToken($token))->delete();
return true;
}
/**
* 删除指定用户的所有Token
* @param int $user_id
* @return boolean
*/
public function clear($user_id)
{
$this->handler->where('user_id', $user_id)->delete();
return true;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace app\common\library\token\driver;
use app\common\library\token\Driver;
/**
* Token操作类
*/
class Redis extends Driver
{
protected $options = [
'host' => '127.0.0.1',
'port' => 6379,
'password' => '',
'select' => 0,
'timeout' => 0,
'expire' => 0,
'persistent' => false,
'userprefix' => 'up:',
'tokenprefix' => 'tp:',
];
/**
* 构造函数
* @param array $options 缓存参数
* @throws \BadFunctionCallException
* @access public
*/
public function __construct($options = [])
{
if (!extension_loaded('redis')) {
throw new \BadFunctionCallException('not support: redis');
}
if (!empty($options)) {
$this->options = array_merge($this->options, $options);
}
$this->handler = new \Redis;
if ($this->options['persistent']) {
$this->handler->pconnect($this->options['host'], $this->options['port'], $this->options['timeout'], 'persistent_id_' . $this->options['select']);
} else {
$this->handler->connect($this->options['host'], $this->options['port'], $this->options['timeout']);
}
if ('' != $this->options['password']) {
$this->handler->auth($this->options['password']);
}
if (0 != $this->options['select']) {
$this->handler->select($this->options['select']);
}
}
/**
* 获取加密后的Token
* @param string $token Token标识
* @return string
*/
protected function getEncryptedToken($token)
{
$config = \think\Config::get('token');
$token = $token ?? ''; // 为兼容 php8
return $this->options['tokenprefix'] . hash_hmac($config['hashalgo'], $token, $config['key']);
}
/**
* 获取会员的key
* @param $user_id
* @return string
*/
protected function getUserKey($user_id)
{
return $this->options['userprefix'] . $user_id;
}
/**
* 存储Token
* @param string $token Token
* @param int $user_id 会员ID
* @param int $expire 过期时长,0表示无限,单位秒
* @return bool
*/
public function set($token, $user_id, $expire = 0)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
$key = $this->getEncryptedToken($token);
if ($expire) {
$result = $this->handler->setex($key, $expire, $user_id);
} else {
$result = $this->handler->set($key, $user_id);
}
//写入会员关联的token
$this->handler->sAdd($this->getUserKey($user_id), $key);
return $result;
}
/**
* 获取Token内的信息
* @param string $token
* @return array
*/
public function get($token)
{
$key = $this->getEncryptedToken($token);
$value = $this->handler->get($key);
if (is_null($value) || false === $value) {
return [];
}
//获取有效期
$expire = $this->handler->ttl($key);
$expire = $expire < 0 ? 365 * 86400 : $expire;
$expiretime = time() + $expire;
//解决使用redis方式储存token时api接口Token刷新与检测因expires_in拼写错误报错的BUG
$result = ['token' => $token, 'user_id' => $value, 'expiretime' => $expiretime, 'expires_in' => $expire];
return $result;
}
/**
* 判断Token是否可用
* @param string $token Token
* @param int $user_id 会员ID
* @return boolean
*/
public function check($token, $user_id)
{
$data = self::get($token);
return $data && $data['user_id'] == $user_id ? true : false;
}
/**
* 删除Token
* @param string $token
* @return boolean
*/
public function delete($token)
{
$data = $this->get($token);
if ($data) {
$key = $this->getEncryptedToken($token);
$user_id = $data['user_id'];
$this->handler->del($key);
$this->handler->sRem($this->getUserKey($user_id), $key);
}
return true;
}
/**
* 删除指定用户的所有Token
* @param int $user_id
* @return boolean
*/
public function clear($user_id)
{
$keys = $this->handler->sMembers($this->getUserKey($user_id));
$this->handler->del($this->getUserKey($user_id));
$this->handler->del($keys);
return true;
}
}