672 lines
24 KiB
PHP
672 lines
24 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace ywxapp\library;
|
||
|
||
use think\App;
|
||
use think\exception\HttpException;
|
||
use think\facade\Cookie;
|
||
use think\facade\Db;
|
||
use think\facade\Session;
|
||
use think\facade\Validate;
|
||
use ywxapp\library\Result;
|
||
use ywxapp\model\BackendAdmin;
|
||
use ywxapp\model\MemberUser as UserModel;
|
||
use ywxapp\service\JwtService;
|
||
use ywxapp\utils\Random;
|
||
|
||
/**
|
||
* 前台(会员/API)鉴权类
|
||
*
|
||
* 负责会员中心的认证:按 Member 表(主键 uid)查用户、解析会员角色/权限,
|
||
* 以及登录/注册/改密等前台业务逻辑。
|
||
* 后台鉴权由 AdminAuth 继承本类,仅重写差异钩子(不同模型、权限集、登录事件)。
|
||
*/
|
||
class Auth
|
||
{
|
||
protected $app;
|
||
|
||
/**
|
||
* 是否登录
|
||
* @var bool
|
||
*/
|
||
protected $isLogin = false;
|
||
|
||
/**
|
||
* 是否管理员?
|
||
*/
|
||
protected $isAdmin = false;
|
||
|
||
/**
|
||
* 当前登录用户信息
|
||
* @var \ywxapp\model\Member
|
||
*/
|
||
protected $model;
|
||
|
||
/**
|
||
* 个人信息
|
||
* @var \ywxapp\model\Member
|
||
*/
|
||
protected $info;
|
||
|
||
/**
|
||
* 权限规则
|
||
* @var array
|
||
*/
|
||
protected $powers;
|
||
|
||
/**
|
||
* 角色
|
||
* @var array
|
||
*/
|
||
protected $roles;
|
||
|
||
/**
|
||
* 用户资料,用于临时存放一些用户数据,比如用户IP地址,需要使用后销毁
|
||
* @var array
|
||
*/
|
||
protected $profile;
|
||
|
||
/**
|
||
* 允许查询的字段(前台)
|
||
* @var array
|
||
*/
|
||
protected $allowFields = ['uid', 'account', 'nickname', 'roles', 'avatar', 'score'];
|
||
|
||
/**
|
||
* 构造方法
|
||
* @param App $app
|
||
*/
|
||
public function __construct(App $app, $options = [])
|
||
{
|
||
$this->app = $app;
|
||
}
|
||
|
||
/**
|
||
* 尝试用 access_token 解析并登录(对外可独立调用,如中间件)。
|
||
*
|
||
* 设计原则:**有 token 才解析并登录,没有 token 则静默跳过**(不报错、不登录),
|
||
* 从而天然支持「访客访问无需登录的页面」与「携带 token 自动登录」两种场景。
|
||
*
|
||
* @return bool 是否成功还原登录态
|
||
*/
|
||
public function tryInitByToken(): bool
|
||
{
|
||
// 已登录则直接返回,避免重复解析与重复 initUser
|
||
if ($this->isLogin) {
|
||
return true;
|
||
}
|
||
$tokenStr = $this->resolveAccessToken();
|
||
if (empty($tokenStr)) {
|
||
// 没有 token:不解析、不登录
|
||
return false;
|
||
}
|
||
try {
|
||
$claims = $this->parseClaims($tokenStr);
|
||
// 上下文一致性校验:token 的 isAdmin 声明必须与当前 auth 类型匹配
|
||
// (前台 isAdmin=false / 后台 AdminAuth isAdmin=true),防止前后台 token 串用越权。
|
||
if (($claims['isAdmin'] ?? false) !== $this->isAdmin) {
|
||
throw new \Exception('auth context mismatch');
|
||
}
|
||
$this->initUser($claims['uid']);
|
||
return true;
|
||
} catch (\Exception $e) {
|
||
// token 无效/过期:清除解析缓存
|
||
\think\facade\Cache::delete('auth_token_' . md5($tokenStr));
|
||
// 尝试用 refresh_token 自动续期(整页/iframe 场景也能续期,避免被踢回登录页)
|
||
$this->tryRefresh();
|
||
return $this->isLogin;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 登录态校验(供后台/前台/会员中心控制器层 verifyAuth 统一调用)
|
||
* 先尝试用 token 还原登录态,再返回是否登录。
|
||
*/
|
||
public function checkLogin(): bool
|
||
{
|
||
$this->tryInitByToken();
|
||
return (bool) $this->isLogin;
|
||
}
|
||
|
||
/**
|
||
* 鉴权前置:尝试用 token 还原登录态(兼容旧调用点 verifyAuth / checkLogin)。
|
||
*/
|
||
protected function tokenParse()
|
||
{
|
||
$this->tryInitByToken();
|
||
}
|
||
|
||
/**
|
||
* 初始化登录用户(模板方法)
|
||
* 公共流程:按主键查模型 → 登录连续性统计 → 解析角色/权限 → 标记登录态 → 触发事件。
|
||
* 与具体模型/角色/权限相关、前后台不同的部分,下沉到下方 protected 钩子,
|
||
* 从而天然适配前后台不同的主键(admin=id / user=uid)与不同的权限体系。
|
||
*
|
||
* @param mixed $id 用户主键(admin 为 id,user 为 uid)
|
||
* @return static
|
||
*/
|
||
public function initUser($id)
|
||
{
|
||
$modelClass = $this->getUserModel();
|
||
$info = $modelClass::findOrEmpty($id);
|
||
if ($info->isEmpty()) {
|
||
Result::instance()->error($this->notFoundMessage($id));
|
||
}
|
||
Db::startTrans();
|
||
try {
|
||
if ($info->update_at < \ywxapp\utils\Date::unixtime('day')) {
|
||
$info->successions = $info->logintime < \ywxapp\utils\Date::unixtime(
|
||
'day',
|
||
-1
|
||
) ? 1 : $info->successions + 1;
|
||
$info->maxsuccessions = max($info->successions, $info->maxsuccessions);
|
||
}
|
||
$info->save();
|
||
$this->roles = $this->resolveRoles($info);
|
||
$this->powers = $this->resolvePowers($info);
|
||
$this->isLogin = true;
|
||
$this->info = $info->toArray();
|
||
$this->model = $info;
|
||
$this->afterLogin($info);
|
||
Db::commit();
|
||
} catch (\think\Exception $e) {
|
||
Db::rollback();
|
||
throw new HttpException(4001, $e->getMessage());
|
||
}
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* 返回当前上下文的用户模型类(核心 PK 适配钩子)。
|
||
* 前台(Auth)默认 Member 模型(主键 uid);后台(AdminAuth)重写为 Backend 模型(主键 id)。
|
||
*/
|
||
protected function getUserModel(): string
|
||
{
|
||
return UserModel::class;
|
||
}
|
||
|
||
/**
|
||
* 根据模型实例解析角色集合。前台会员角色取自 groups 字段;后台在 AdminAuth 重写为 roles。
|
||
*/
|
||
protected function resolveRoles($info)
|
||
{
|
||
return $info->groups ?? [];
|
||
}
|
||
|
||
/**
|
||
* 根据模型实例解析权限集。前台会员默认无后台式权限集,由控制器 noNeedVerify 控制访问。
|
||
*/
|
||
protected function resolvePowers($info): array
|
||
{
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 用户不存在时的错误提示
|
||
*/
|
||
protected function notFoundMessage($id): string
|
||
{
|
||
return "UserId:$id is incorrect";
|
||
}
|
||
|
||
/**
|
||
* 登录成功后的事件钩子(前台触发 UserLogined)
|
||
*/
|
||
protected function afterLogin($info): void
|
||
{
|
||
event('UserLogined', $this->info);
|
||
}
|
||
|
||
/**
|
||
* 退出登录后的钩子。前台会员:清空整个 session 并触发退出事件(由 Auth::logout 调用)。
|
||
* 后台 AdminAuth 重写为空(仅清 token,保持与原 Backend 行为一致)。
|
||
*/
|
||
protected function afterLogout(): void
|
||
{
|
||
\think\facade\Session::clear();
|
||
event('user_logout_after', $this->model);
|
||
}
|
||
|
||
/**
|
||
* 解析 access_token 的 claims(带缓存)
|
||
* @param string $tokenStr
|
||
* @return array
|
||
*/
|
||
protected function parseClaims(string $tokenStr): array
|
||
{
|
||
$cacheKey = 'auth_token_' . md5($tokenStr);
|
||
$claims = \think\facade\Cache::get($cacheKey);
|
||
if (!$claims) {
|
||
$token = JwtService::instance()->parseAndValidate($tokenStr);
|
||
$claims = $token->claims()->all();
|
||
\think\facade\Cache::set($cacheKey, $claims, 300);
|
||
}
|
||
return $claims;
|
||
}
|
||
|
||
/**
|
||
* 从 header / Session / Cookie 中解析当前 access_token
|
||
*/
|
||
protected function resolveAccessToken(): string
|
||
{
|
||
$authHeader = $this->app->request->header('authorization');
|
||
if (!empty($authHeader) && str_starts_with($authHeader, 'Bearer ')) {
|
||
return (string) substr($authHeader, 7);
|
||
}
|
||
$token = Session::get('access_token');
|
||
if (!empty($token)) {
|
||
return (string) $token;
|
||
}
|
||
return (string) \think\facade\Cookie::get('access_token');
|
||
}
|
||
|
||
/**
|
||
* 解析可用于续期的 refresh_token(header / Session / Cookie)
|
||
*/
|
||
protected function resolveRefreshToken(): string
|
||
{
|
||
$authHeader = $this->app->request->header('authorization_refresh');
|
||
if (!empty($authHeader) && str_starts_with($authHeader, 'Bearer ')) {
|
||
return (string) substr($authHeader, 7);
|
||
}
|
||
$token = Session::get('refresh_token');
|
||
if (!empty($token)) {
|
||
return (string) $token;
|
||
}
|
||
return (string) \think\facade\Cookie::get('refresh_token');
|
||
}
|
||
|
||
/**
|
||
* 使用 refresh_token 自动续期 access_token。
|
||
* 用于整页/iframe 等非 ajax 场景:access_token 过期但 refresh_token 仍有效时,
|
||
* 后端透明生成新 access_token 并写回 Session / Cookie,避免子页面被踢回登录页。
|
||
*/
|
||
protected function tryRefresh(): void
|
||
{
|
||
$refreshToken = $this->resolveRefreshToken();
|
||
if (empty($refreshToken)) {
|
||
return;
|
||
}
|
||
try {
|
||
$data = JwtService::instance()->refreshAccessToken($refreshToken);
|
||
$newToken = $data['access_token'] ?? '';
|
||
if (empty($newToken)) {
|
||
return;
|
||
}
|
||
Session::set('access_token', $newToken);
|
||
// 同步写入非 httpOnly cookie,便于前端与 iframe 后续请求携带
|
||
\think\facade\Cookie::set('access_token', $newToken, ['httponly' => false, 'path' => '/']);
|
||
$claims = JwtService::instance()->parseAndValidate($newToken)->claims()->all();
|
||
// 续期得到的 token 也必须与当前上下文一致,否则视为无效
|
||
if (($claims['isAdmin'] ?? false) !== $this->isAdmin) {
|
||
return;
|
||
}
|
||
$this->initUser($claims['uid']);
|
||
} catch (\Exception $e) {
|
||
// 续期失败,保持未登录,交由 verifyAuth 返回 401
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户权限验证
|
||
*/
|
||
public function verifyAuth($noNeedLogin = [], $noNeedRight = [])
|
||
{
|
||
// 先尝试用 token 还原登录态:登录态由 tryInitByToken 写入 $this->isLogin,
|
||
// 必须由本方法自行触发,不能依赖外部(核心后台靠容器单例的历史初始化、
|
||
// 插件后台 new AdminAuth 全新实例都不可靠,会导致 isLogin 恒为默认 false 而误踢登录)。
|
||
$this->tokenParse();
|
||
$request = app()->request;
|
||
$action = strtolower($request->action());
|
||
if (in_array('*', $noNeedLogin) || in_array($action, $noNeedLogin)) {
|
||
return true;
|
||
}
|
||
// 规范化权限集合,避免 initUser 未赋值导致 in_array() 报 TypeError
|
||
$this->powers = $this->powers ?? [];
|
||
if ($this->isLogin == false) {
|
||
// 整页(非 AJAX)请求未登录:直接 302 跳转到登录页;
|
||
// AJAX 请求返回 401 JSON,交给前端 kernel.js 跳转,避免整页被踢。
|
||
if (! $request->isAjax() && ! $request->isPjax()) {
|
||
// 直接发送 302 并终止,确保构造期(_initialize 中调用)也能可靠跳转
|
||
$this->redirectToLogin();
|
||
}
|
||
Result::instance()->setStatusCode(401)->error(lang('Please login first'));
|
||
}
|
||
// 超级管理员直接放行(避免权限数据缺失时锁定后台)
|
||
if (! empty($this->model) && ! empty($this->model->id)
|
||
&& $this->model->id == config('ywxapp.superAdmin', 1)) {
|
||
return true;
|
||
}
|
||
// 权限集合含通配符 * 表示拥有全部权限(如超级管理员角色),直接放行
|
||
if (in_array('*', $this->powers, true)) {
|
||
return true;
|
||
}
|
||
if (in_array('*', $noNeedRight) || in_array($action, $noNeedRight)) {
|
||
return true;
|
||
}
|
||
$controller = strtolower($request->controller());
|
||
$path = str_replace('.', ':', $controller) . ':' . $action;
|
||
// 精确匹配 或 控制器级通配(controller:*)
|
||
if (in_array($path, $this->powers) || in_array($controller . ':*', $this->powers)) {
|
||
return true;
|
||
}
|
||
Result::instance()->setStatusCode(403)->error(lang('You have no permission'));
|
||
}
|
||
|
||
/**
|
||
* 跳转到登录页(生成完整绝对 URL 后 302)。
|
||
*
|
||
* 登录跳转链接使用「当前请求域名 + 子目录(root) + 登录路径」拼成绝对地址,
|
||
* 避免在子目录部署或中心站/客户机跨域分发时跳到错误位置。
|
||
* 后台路径跟随 config/app.php 的 app_map 中 backend 对应的别名(默认 admin),
|
||
* 前台为 /Login。config/ywxapp.php 的 backend_login_url / member_login_url 可强制覆盖。
|
||
*
|
||
* @return void
|
||
*/
|
||
/**
|
||
* 从 app 配置推导后台应用的 URL 别名(app_map 中 backend 对应的键)。
|
||
*
|
||
* 多应用模式下后台真实目录为 backend,对外 URL 段由 config/app.php 的
|
||
* app_map['<别名>'=>'backend'] 决定(默认 admin)。硬编码 /admin 会在别名
|
||
* 调整时失效,故反查配置,使登录跳转跟随部署配置。
|
||
*
|
||
* @return string 如 'backend'
|
||
*/
|
||
protected function UrlAlias($val): string
|
||
{
|
||
$appMap = config('app.app_map', []);
|
||
$alias = array_search($val, $appMap, true);
|
||
return $alias === false ? $val : (string) $alias;
|
||
}
|
||
|
||
public function redirectToLogin()
|
||
{
|
||
if ($this->isAdmin) {
|
||
// 优先读 config/ywxapp.php 的 backend_login_url 覆盖;留空则跟随 app_map 推导
|
||
$override = trim((string) config('ywxapp.backend_login_url', ''));
|
||
$path = $override !== ''
|
||
? $override
|
||
: '/' . $this->UrlAlias('backend') . '/Login/index';
|
||
} else {
|
||
$override = trim((string) config('ywxapp.member_login_url', ''));
|
||
$path = $override !== '' ? $override : '/' . $this->UrlAlias('member') . '/Login/index';
|
||
}
|
||
// 用 url() 助手生成完整绝对 URL(自动带域名 + 子目录 root),
|
||
// 避免在子目录部署或中心站/客户机跨域分发时跳到错误位置(原先为裸根路径)。
|
||
$loginUrl = (string) url($path, [], false, true);
|
||
// 直接发送 302 并终止,确保构造期(_initialize 中调用)也能可靠跳转
|
||
redirect($loginUrl)->send();
|
||
exit;
|
||
}
|
||
|
||
|
||
/**
|
||
* 登录成功后把 access/refresh token 持久化到共享 cookie(path=/) 与服务端 session,
|
||
* 保证整页跳转(iframe、菜单点击等非 AJAX 场景,不会自动带 Authorization 头)
|
||
* 也能通过 cookie/session 还原登录态,避免被踢回登录页。
|
||
* @param array $tokens JwtService::createToken() 的返回值
|
||
*/
|
||
protected function persistTokens(array $tokens): void
|
||
{
|
||
$at = $tokens['access_token'] ?? '';
|
||
$rt = $tokens['refresh_token'] ?? '';
|
||
if ($at !== '') {
|
||
Session::set('access_token', $at);
|
||
\think\facade\Cookie::set('access_token', $at, ['httponly' => false, 'path' => '/']);
|
||
}
|
||
if ($rt !== '') {
|
||
Session::set('refresh_token', $rt);
|
||
\think\facade\Cookie::set('refresh_token', $rt, ['httponly' => false, 'path' => '/']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户登出,清除 token 缓存与登录态
|
||
* @return void
|
||
*/
|
||
public function logout()
|
||
{
|
||
$tokenStr = $this->resolveAccessToken();
|
||
// 子类钩子:此时 model/info 尚未清空,可做事件触发、整 session 清除等
|
||
$this->afterLogout();
|
||
if ($tokenStr) {
|
||
\think\facade\Cache::delete('auth_token_' . md5($tokenStr));
|
||
}
|
||
// 清除服务端 session / cookie 中的 token
|
||
Session::delete('access_token');
|
||
Session::delete('refresh_token');
|
||
\think\facade\Cookie::delete('access_token');
|
||
\think\facade\Cookie::delete('refresh_token');
|
||
// 重置登录状态
|
||
$this->isLogin = false;
|
||
$this->info = null;
|
||
$this->model = null;
|
||
$this->powers = [];
|
||
$this->roles = [];
|
||
}
|
||
|
||
/**
|
||
* 魔术读取(兼容 $auth->info / $auth->model 等)
|
||
* @param mixed $name
|
||
* @return mixed
|
||
*/
|
||
public function __get($name)
|
||
{
|
||
return $this->$name;
|
||
}
|
||
|
||
/**
|
||
* 魔术赋值
|
||
* @param mixed $name
|
||
* @param mixed $value
|
||
* @return void
|
||
*/
|
||
public function __set($name, $value)
|
||
{
|
||
$this->$name = $value;
|
||
}
|
||
|
||
|
||
public static function instance($options = [])
|
||
{
|
||
return app()->auth;
|
||
}
|
||
|
||
/**
|
||
* 注册用户(前台)
|
||
*
|
||
* @param string $account 用户名
|
||
* @param string $password 密码
|
||
* @param string $email 邮箱
|
||
* @param string $mobile 手机号
|
||
* @param array $extend 扩展参数
|
||
* @return boolean
|
||
*/
|
||
public function register($account = '', $password = '', $email = '', $mobile = '', $extend = [])
|
||
{
|
||
//账号注册时需要开启事务,避免出现垃圾数据
|
||
Db::startTrans();
|
||
try {
|
||
$account = $account ?: Random::account() . mb_substr($mobile ?: strtoupper(Random::alnum(4)), -4);
|
||
$password = $password ?: Random::alnum(16);
|
||
$nickname = $extend['nickname'] ?? '用户' . mb_substr($mobile ?: strtoupper(Random::alnum(4)), -4);
|
||
// 检测用户名
|
||
if (UserModel::checkExists('account', $account)) {
|
||
Result::instance()->error('Account already exist');
|
||
Db::rollback();
|
||
return false;
|
||
}
|
||
// 检测邮箱
|
||
if ($email && UserModel::checkExists('email', $email)) {
|
||
Result::instance()->error('Email already exist');
|
||
Db::rollback();
|
||
return false;
|
||
}
|
||
// 检测手机号
|
||
if ($mobile && UserModel::checkExists('mobile', $mobile)) {
|
||
Result::instance()->error('Mobile already exist');
|
||
Db::rollback();
|
||
return false;
|
||
}
|
||
$ip = request()->ip();
|
||
$data = [
|
||
'gid' => config('fastadmin.user_default_group') ?: 0,
|
||
'account' => $account,
|
||
'password' => $password,
|
||
'email' => $email,
|
||
'mobile' => $mobile,
|
||
//'score' => config('fastadmin.user_initial_score') ?: 0,
|
||
'avatar' => '',
|
||
'nickname' => $nickname,
|
||
'create_ip' => $ip,
|
||
'update_ip' => $ip,
|
||
'status' => 1,
|
||
];
|
||
$params['password'] = $this->getEncryptPassword($password);
|
||
$params = array_merge($params, $extend);
|
||
$this->info = UserModel::create($params, true);
|
||
$newClaims = [
|
||
'uid' => $this->info->uid,
|
||
'account' => $this->info->account,
|
||
];
|
||
$tokens = JwtService::instance()->createToken($newClaims);
|
||
$this->persistTokens($tokens);
|
||
$this->isLogin = true;
|
||
event('user_register_after', $this->info);
|
||
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, $isAuthPass = true)
|
||
{
|
||
$field = Validate::checkRule($account, 'email') ? 'email' : (Validate::checkRule($account, 'mobile') ? 'mobile' : 'account');
|
||
|
||
$info = UserModel::where($field, $account)->findOrEmpty();
|
||
|
||
// 会员表查无此账号时,回退到后台管理员表校验(打通会员中心与后台同一账号登录)
|
||
if ($info->isEmpty()) {
|
||
$admin = BackendAdmin::where('account', $account)->find();
|
||
if ($admin && $admin->status == 1 && $admin->checkPassword($password)) {
|
||
$info = $this->syncFromAdmin($admin, $password);
|
||
}
|
||
}
|
||
|
||
if ($info->isEmpty()) {
|
||
Result::instance()->error('Account is incorrect');
|
||
return false;
|
||
}
|
||
if ($info->status != 1) {
|
||
Result::instance()->error('Member Account is locked');
|
||
return false;
|
||
}
|
||
if ($info->isLocked()) {
|
||
Result::instance()->error('Member Account is locked');
|
||
return false;
|
||
}
|
||
$info->resetPassword($password);
|
||
|
||
if (! $info->checkPassword($password)) {
|
||
$info->recordLoginFail($_SERVER['REMOTE_ADDR'] ?? '');
|
||
Result::instance()->error('Password is incorrect');
|
||
return false;
|
||
}
|
||
$newClaims = [
|
||
'uid' => $info->uid,
|
||
'account' => $info->account,
|
||
];
|
||
// createToken 同时生成 access/refresh;持久化到共享 cookie(path=/)+session,
|
||
// 使整页跳转(iframe/菜单点击,无 Authorization 头)也能还原登录态。
|
||
$tokens = JWTService::instance()->createToken($newClaims);
|
||
$this->persistTokens($tokens);
|
||
// 钩子点:会员/API 登录成功后触发,插件可在 info.php['events']['listen'] 中监听 user_login_after
|
||
event('user_login_after', $info);
|
||
$info->recordLogin($_SERVER['REMOTE_ADDR'] ?? '');
|
||
$this->initUser($info->uid);
|
||
}
|
||
|
||
/**
|
||
* 后台管理员账号首次登录会员中心时,在会员表自动创建一条对应记录,
|
||
* 密码按会员表规则(带 salt)加密,以便后续会员中心可独立登录。
|
||
* 仅在会员表无该 account 时创建。
|
||
*
|
||
* @param \ywxapp\model\Backend $admin 后台管理员模型(已通过密码校验)
|
||
* @param string $password 明文密码(用于会员表加密存储)
|
||
* @return \ywxapp\model\Member
|
||
*/
|
||
protected function syncFromAdmin($admin, $password)
|
||
{
|
||
$user = UserModel::where('account', $admin->account)->findOrEmpty();
|
||
if (! $user->isEmpty()) {
|
||
return $user;
|
||
}
|
||
$user = UserModel::create([
|
||
'account' => $admin->account,
|
||
'nickname' => $admin->nickname ?: $admin->account,
|
||
'password' => $password, // 触发 Member 模型的 setPasswordAttr 自动加盐加密
|
||
'gid' => config('fastadmin.user_default_group') ?: 0,
|
||
'status' => 1,
|
||
'create_ip' => request()->ip(),
|
||
'update_ip' => request()->ip(),
|
||
]);
|
||
return $user;
|
||
}
|
||
|
||
/**
|
||
* 修改密码(前台)
|
||
* @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::clear($this->_user->uid);
|
||
//修改密码成功的事件
|
||
event('user_changepwd_after', $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;
|
||
}
|
||
}
|
||
}
|