Files
zyt/server/app/adminapi/logic/LoginLogic.php
T
2026-04-07 18:13:03 +08:00

358 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | likeadmin快速开发前后端分离管理后台(PHP版)
// +----------------------------------------------------------------------
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
// | 开源版本可自由商用,可去除界面版权logo
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
// | github下载:https://github.com/likeshop-github/likeadmin
// | 访问官网:https://www.likeadmin.cn
// | likeadmin团队 版权所有 拥有最终解释权
// +----------------------------------------------------------------------
// | author: likeadminTeam
// +----------------------------------------------------------------------
namespace app\adminapi\logic;
use app\common\logic\BaseLogic;
use app\common\model\auth\Admin;
use app\adminapi\service\AdminTokenService;
use app\common\service\FileService;
use think\facade\Config;
use think\facade\Cache;
use think\facade\Log;
/**
* 登录逻辑
* Class LoginLogic
* @package app\adminapi\logic
*/
class LoginLogic extends BaseLogic
{
/** 非 root 未绑定企微时接口返回,与 axios 约定一致 */
public const CODE_NEED_BIND_WORK_WECHAT = 10;
/**
* 企业微信网页授权 / 扫码绑定所需配置是否齐全
*/
public static function isWorkWechatOAuthConfigured(): bool
{
$corpId = (string) env('work_wechat.corp_id', '');
$secret = (string) env('work_wechat.secret', '');
$agentId = (string) env('work_wechat.agent_id', '');
return $corpId !== '' && $secret !== '' && $agentId !== '';
}
/**
* .env 是否开启「非 root 须绑定企微」。
* 推荐在 [work_wechat] 下写 FORCE_BIND_LOGIN=true;勿在节内写 WORK_WECHAT_FORCE_BIND_LOGIN(会变成双前缀键读不到)。
*/
public static function isForceBindWorkWechatFromEnv(): bool
{
$candidates = [
env('WORK_WECHAT_FORCE_BIND_LOGIN', false),
env('work_wechat.force_bind_login', false),
env('work_wechat.work_wechat_force_bind_login', false),
];
$v = false;
foreach ($candidates as $c) {
if ($c !== false && $c !== null && $c !== '') {
$v = $c;
break;
}
}
if (is_bool($v)) {
return $v;
}
$v = strtolower(trim((string) $v));
return in_array($v, ['1', 'true', 'yes', 'on'], true);
}
/**
* .env 开启强制绑定 + 企微 OAuth 已配置 + 非 root + 未绑定 work_wechat_userid
*
* @param array{root?:int|string,work_wechat_userid?:string} $adminInfo
*/
public static function adminMustBindWorkWechat(array $adminInfo): bool
{
if (!self::isForceBindWorkWechatFromEnv()) {
return false;
}
if (!self::isWorkWechatOAuthConfigured()) {
return false;
}
if ((int) ($adminInfo['root'] ?? 0) === 1) {
return false;
}
return trim((string) ($adminInfo['work_wechat_userid'] ?? '')) === '';
}
/**
* 开启「须绑定企微」时,这些 action 仍须放行(与 $request->action() 比较,不区分大小写)。
* 路由/网关若把 action 变成全小写,严格 in_array('bindWorkWechat') 会失败,导致绑定接口被误判为 code=10,扫码页反复刷新。
*/
public static function isWorkWechatBindExemptActionName(string $action): bool
{
$a = strtolower(trim($action));
return in_array($a, [
'bindworkwechat',
'unbindworkwechat',
'myself',
'logout',
], true);
}
/**
* @notes 管理员账号登录
* @param $params
* @return false|mixed
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 令狐冲
* @date 2021/6/30 17:00
*/
public function login($params)
{
$time = time();
$admin = Admin::where('account', '=', $params['account'])->find();
//用户表登录信息更新
$admin->login_time = $time;
$admin->login_ip = request()->ip();
$admin->save();
//设置token
$adminInfo = AdminTokenService::setToken($admin->id, $params['terminal'], $admin->multipoint_login);
//返回登录信息
$avatar = $admin->avatar ? $admin->avatar : Config::get('project.default_image.admin_avatar');
$avatar = FileService::getFileUrl($avatar);
$row = [
'name' => $adminInfo['name'],
'avatar' => $avatar,
'role_name' => $adminInfo['role_name'],
'token' => $adminInfo['token'],
'is_paw' => $admin->is_paw ?? 1, // 0=需要修改密码,1=正常
];
$row['need_bind_work_wechat'] = self::adminMustBindWorkWechat([
'root' => (int) ($admin->root ?? 0),
'work_wechat_userid' => (string) ($admin->work_wechat_userid ?? ''),
]);
return $row;
}
/**
* 从 auth/getuserinfo 响应解析企业成员 userid。
* 非通讯录成员时接口可能只返回 openid / external_userid,无 userid。
*/
public static function workWechatUserIdFromAuthResponse(array $response): string
{
return trim((string) ($response['userid'] ?? $response['UserId'] ?? ''));
}
/**
* @notes 企业微信授权登录
*/
public function workWechatLogin($params)
{
$code = $params['code'];
$terminal = $params['terminal'] ?? 1;
$corpId = env('work_wechat.corp_id', '');
$secret = env('work_wechat.secret', '');
if (empty($corpId) || empty($secret)) {
self::setError('企业微信未配置');
return false;
}
// 获取 access_token
$accessToken = self::getWorkWechatAccessToken($corpId, $secret);
if (!$accessToken) {
return false;
}
// 用 code 换取用户身份
$url = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token={$accessToken}&code={$code}";
$response = self::httpGet($url);
if (!$response || $response['errcode'] != 0) {
$errMsg = $response['errmsg'] ?? '未知错误';
Log::error('企业微信获取用户身份失败: ' . $errMsg);
self::setError('企业微信授权失败: ' . $errMsg);
return false;
}
$wxUserId = self::workWechatUserIdFromAuthResponse($response);
if ($wxUserId === '') {
$hasOpenId = trim((string) ($response['openid'] ?? $response['OpenId'] ?? '')) !== '';
self::setError(
$hasOpenId
? '当前身份非企业通讯录成员(或未同步到通讯录),无法用此账号登录后台,请使用企业内成员账号或联系管理员将你加入通讯录'
: '未获取到企业微信成员 userid,请检查自建应用 Secret、可信域名是否与当前扫码应用一致'
);
return false;
}
// 通过 work_wechat_userid 匹配管理员(SoftDelete trait 自动排除已删除记录)
$admin = Admin::where('work_wechat_userid', '=', $wxUserId)->find();
if (!$admin) {
self::setError('该企业微信账号未绑定管理员,请联系管理员绑定(UserId: ' . $wxUserId . '');
return false;
}
if ($admin->disable === 1) {
self::setError('账号已被禁用');
return false;
}
// 更新登录信息
$time = time();
$admin->login_time = $time;
$admin->login_ip = request()->ip();
$admin->save();
// 设置 token
$adminInfo = AdminTokenService::setToken($admin->id, $terminal, $admin->multipoint_login);
$avatar = $admin->avatar ?: Config::get('project.default_image.admin_avatar');
$avatar = FileService::getFileUrl($avatar);
return [
'name' => $adminInfo['name'],
'avatar' => $avatar,
'role_name' => $adminInfo['role_name'],
'token' => $adminInfo['token'],
'is_paw' => $admin->is_paw ?? 1, // 0=需要修改密码,1=正常
// 企微扫码登录即已绑定 userid
'need_bind_work_wechat' => false,
];
}
/**
* @notes 获取企业微信 access_token(带缓存),供外部(如绑定流程)调用
*/
public static function getWorkWechatAccessTokenStatic(string $corpId, string $secret)
{
return self::getWorkWechatAccessToken($corpId, $secret);
}
/**
* @notes 获取企业微信 access_token(带缓存)
*/
private static function getWorkWechatAccessToken(string $corpId, string $secret)
{
$cacheKey = 'work_wechat_access_token_' . md5($corpId . $secret);
$cached = Cache::get($cacheKey);
if ($cached) {
return $cached;
}
$url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$corpId}&corpsecret={$secret}";
$response = self::httpGet($url);
if (!$response || $response['errcode'] != 0) {
$errMsg = $response['errmsg'] ?? '未知错误';
Log::error('获取企业微信access_token失败: ' . $errMsg);
self::setError('获取企业微信凭证失败: ' . $errMsg);
return false;
}
$accessToken = $response['access_token'];
Cache::set($cacheKey, $accessToken, $response['expires_in'] - 200);
return $accessToken;
}
/**
* @notes HTTP GET 请求
*/
private static function httpGet(string $url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$result = curl_exec($ch);
curl_close($ch);
if ($result === false) {
return null;
}
return json_decode($result, true);
}
/**
* @notes 退出登录
* @param $adminInfo
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
* @author 令狐冲
* @date 2021/7/5 14:34
*/
public function logout($adminInfo)
{
//token不存在,不注销
if (!isset($adminInfo['token'])) {
return false;
}
//设置token过期
return AdminTokenService::expireToken($adminInfo['token']);
}
/**
* @notes 首次登录修改密码
* @param string $token
* @param string $password
* @return bool
*/
public function changeFirstPassword($token, $password)
{
try {
// 通过 token 获取管理员信息
$adminSession = \app\common\model\auth\AdminSession::where('token', '=', $token)
->where('expire_time', '>', time())
->find();
if (!$adminSession) {
self::setError('登录已过期,请重新登录');
return false;
}
$admin = Admin::find($adminSession->admin_id);
if (!$admin) {
self::setError('管理员不存在');
return false;
}
// 使用系统的密码加密方式
$passwordSalt = Config::get('project.unique_identification');
$encryptedPassword = create_password($password, $passwordSalt);
// 更新密码和 is_paw 标记
$admin->password = $encryptedPassword;
$admin->is_paw = 1;
$admin->save();
// 使当前 token 过期,要求重新登录
AdminTokenService::expireToken($token);
return true;
} catch (\Exception $e) {
self::setError($e->getMessage());
return false;
}
}
}