mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-28 02:02:14 +00:00
- composer: framework ^13.0、php ^8.3、laravel-s ~3.8.0、predis ^2.3、 phpunit ^11.5、tinker ^3、excel ^3.1.69、captcha ^3.5、avatar ^6.5、 ldaprecord-laravel ^4、pinyin ^5.3、notify 锁 ~1.28.0; 移除 fideloper/proxy、fruitcake/laravel-cors、facade/ignition、 laravel/sail、madnest/madzipper、手动钉的 symfony/mailer; symfony/console 锁 ^7.4(LaravelS Portal 与 console 8 的 configure(): void 类型断言不兼容) - $dates 移除:AbstractModel 改 getCasts() 合并默认 datetime 列, 3 个子模型改 $casts - Carbon 3:4 处 diffInSeconds 补 absolute 参数并取整 - LdapRecord v4:config use_ssl/use_tls→use_tls/use_starttls(env 变量名不变), LdapUser::$objectClasses 补类型声明 - Madzipper→原生 ZipArchive(Base::zipAddFiles,4 处调用) - pinyin v5 静态 API(Base::getFirstCharter/cn2pinyin) - laravolt/avatar 6.5:PatchedAvatar 修上游纵向对齐 bug (intervention 4.1.3 枚举无 middle),avatar 响应改 response()->file() - TrustProxies 改框架内置基类,CORS 改 Illuminate\Http\Middleware\HandleCors - Symfony Console 8 兼容:ManticoreSyncLock::handleSignal 新签名, pcntl 回调解耦 - 非 Swoole 运行时守卫:AbstractTask::task / PushTask::push / AbstractData(swoole table),artisan/测试上下文不再炸 Target class [swoole] does not exist - Laravel 11+ change() 丢修饰符:2023_12_07 与 2025_08_10 迁移重申 nullable/default/comment(修复 fresh 安装) - Setting/Ihttp 缺键访问加 ?? 守卫(PHP 8 警告在测试中转异常) - phpunit.xml 迁移 11 schema;UserImportParseTest 改为自建部门数据 验证:8.4 容器内 migrate:fresh --seed 213 全过;php artisan test 145 passed/1 skipped;LaravelS(Swoole 6.2.1) /health 200、登录、 token 认证、WebSocket 握手、Task 投递、头像、图片裁剪冒烟全过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
318 lines
8.7 KiB
PHP
318 lines
8.7 KiB
PHP
<?php
|
|
|
|
namespace App\Ldap;
|
|
|
|
use App\Exceptions\ApiException;
|
|
use App\Models\User;
|
|
use App\Module\Base;
|
|
use App\Services\RequestContext;
|
|
use LdapRecord\Configuration\ConfigurationException;
|
|
use LdapRecord\Container;
|
|
use LdapRecord\LdapRecordException;
|
|
use LdapRecord\Models\Model;
|
|
|
|
class LdapUser extends Model
|
|
{
|
|
/**
|
|
* The object classes of the LDAP model.
|
|
*/
|
|
public static array $objectClasses = [
|
|
'person',
|
|
'top',
|
|
];
|
|
|
|
private static $emailAttrs = ['mail', 'cn', 'uid', 'userPrincipalName'];
|
|
|
|
/**
|
|
* @return mixed|null
|
|
*/
|
|
public function getPhoto()
|
|
{
|
|
return $this->jpegPhoto && is_array($this->jpegPhoto) ? $this->jpegPhoto[0] : null;
|
|
}
|
|
|
|
/**
|
|
* @return mixed|null
|
|
*/
|
|
public function getDisplayName()
|
|
{
|
|
$nickname = $this->displayName ?: $this->uid;
|
|
return is_array($nickname) ? $nickname[0] : $nickname;
|
|
}
|
|
|
|
/**
|
|
* @return LdapUser
|
|
*/
|
|
public static function static(): LdapUser
|
|
{
|
|
return new static;
|
|
}
|
|
|
|
/**
|
|
* 服务是否打开
|
|
* @return bool
|
|
*/
|
|
public static function isOpen(): bool
|
|
{
|
|
return Base::settingFind('thirdAccessSetting', 'ldap_open') === 'open';
|
|
}
|
|
|
|
/**
|
|
* 同步本地是否打开
|
|
* @return bool
|
|
*/
|
|
public static function isSyncLocal(): bool
|
|
{
|
|
return Base::settingFind('thirdAccessSetting', 'ldap_sync_local') === 'open';
|
|
}
|
|
|
|
/**
|
|
* 获取登录属性名
|
|
* @return string
|
|
*/
|
|
public static function getLoginAttr(): string
|
|
{
|
|
$attr = Base::settingFind('thirdAccessSetting', 'ldap_login_attr');
|
|
return in_array($attr, ['cn', 'uid', 'mail', 'sAMAccountName', 'userPrincipalName']) ? $attr : 'cn';
|
|
}
|
|
|
|
/**
|
|
* 初始化配置
|
|
* @return bool
|
|
*/
|
|
public static function initConfig()
|
|
{
|
|
if (RequestContext::has('ldap_init')) {
|
|
return RequestContext::get('ldap_init');
|
|
}
|
|
//
|
|
$setting = Base::setting('thirdAccessSetting');
|
|
if ($setting['ldap_open'] !== 'open') {
|
|
return RequestContext::save('ldap_init', false);
|
|
}
|
|
//
|
|
$connection = Container::getDefaultConnection();
|
|
try {
|
|
$connection->setConfiguration([
|
|
"hosts" => [$setting['ldap_host']],
|
|
"port" => intval($setting['ldap_port']),
|
|
"base_dn" => $setting['ldap_base_dn'],
|
|
"username" => $setting['ldap_user_dn'],
|
|
"password" => $setting['ldap_password'],
|
|
]);
|
|
return RequestContext::save('ldap_init', true);
|
|
} catch (ConfigurationException $e) {
|
|
info($e->getMessage());
|
|
return RequestContext::save('ldap_init', false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 通过管理员绑定搜索用户,然后用用户 DN 做 Bind 认证
|
|
* @param $username
|
|
* @param $password
|
|
* @return Model|null
|
|
*/
|
|
public static function userFirst($username, $password): ?Model
|
|
{
|
|
if (!self::initConfig()) {
|
|
return null;
|
|
}
|
|
try {
|
|
$loginAttr = self::getLoginAttr();
|
|
$row = self::static()
|
|
->whereRaw($loginAttr, '=', $username)
|
|
->first();
|
|
if (!$row) {
|
|
return null;
|
|
}
|
|
$connection = Container::getDefaultConnection();
|
|
if (!$connection->auth()->attempt($row->getDn(), $password)) {
|
|
return null;
|
|
}
|
|
// Swoole 下连接共享,必须恢复管理员绑定
|
|
$connection->auth()->attempt(
|
|
$connection->getConfiguration()->get('username'),
|
|
$connection->getConfiguration()->get('password')
|
|
);
|
|
return $row;
|
|
} catch (\Exception $e) {
|
|
info("[LDAP] auth fail: " . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 通过邮箱查找 LDAP 用户
|
|
* @param $email
|
|
* @return Model|null
|
|
*/
|
|
public static function findByEmail($email): ?Model
|
|
{
|
|
if (!self::initConfig()) {
|
|
return null;
|
|
}
|
|
try {
|
|
foreach (self::$emailAttrs as $attr) {
|
|
$row = self::static()->whereRaw($attr, '=', $email)->first();
|
|
if ($row) {
|
|
return $row;
|
|
}
|
|
}
|
|
return null;
|
|
} catch (\Exception) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取用户的邮箱(从 LDAP 记录中提取)
|
|
* @param Model $row
|
|
* @return string|null
|
|
*/
|
|
public static function getUserEmail(Model $row): ?string
|
|
{
|
|
foreach (self::$emailAttrs as $attr) {
|
|
$val = $row->getFirstAttribute($attr);
|
|
if ($val && Base::isEmail($val)) {
|
|
return $val;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 登录
|
|
* @param $username
|
|
* @param $password
|
|
* @param User|null $user
|
|
* @return User|mixed|null
|
|
*/
|
|
public static function userLogin($username, $password, $user = null)
|
|
{
|
|
if (!self::initConfig()) {
|
|
return null;
|
|
}
|
|
$row = self::userFirst($username, $password);
|
|
if (!$row) {
|
|
return null;
|
|
}
|
|
if (empty($user)) {
|
|
$email = self::getUserEmail($row);
|
|
if (empty($email)) {
|
|
throw new ApiException('LDAP 用户缺少邮箱属性,请联系管理员配置');
|
|
}
|
|
$user = User::whereEmail($email)->first();
|
|
if (empty($user)) {
|
|
// LDAP 用户通过 LDAP 认证,本地密码用随机值以满足密码策略
|
|
$localPassword = Base::generatePassword(16) . 'Aa1!';
|
|
$user = User::reg($email, $localPassword);
|
|
} elseif (!$user->isLdap()) {
|
|
info("[LDAP] merged with existing local account: userid={$user->userid}, email={$email}");
|
|
}
|
|
}
|
|
if ($user) {
|
|
$userimg = $row->getPhoto();
|
|
if ($userimg) {
|
|
$path = "uploads/user/ldap/";
|
|
$file = "{$path}{$user->userid}.jpeg";
|
|
Base::makeDir(public_path($path));
|
|
if (Base::saveContentImage(public_path($file), $userimg)) {
|
|
$user->userimg = $file;
|
|
}
|
|
}
|
|
$user->nickname = $row->getDisplayName();
|
|
$user->save();
|
|
}
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* 同步
|
|
* @param User $user
|
|
* @param $password
|
|
* @return void
|
|
*/
|
|
public static function userSync(User $user, $password)
|
|
{
|
|
if ($user->isLdap()) {
|
|
return;
|
|
}
|
|
//
|
|
if (!self::initConfig()) {
|
|
return;
|
|
}
|
|
//
|
|
if (self::isSyncLocal()) {
|
|
$row = self::findByEmail($user->email);
|
|
if ($row) {
|
|
return;
|
|
}
|
|
try {
|
|
$userimg = public_path($user->getRawOriginal('userimg'));
|
|
if (file_exists($userimg)) {
|
|
$userimg = file_get_contents($userimg);
|
|
} else {
|
|
$userimg = '';
|
|
}
|
|
$attrs = [
|
|
'cn' => $user->email,
|
|
'sn' => $user->email,
|
|
'uid' => $user->email,
|
|
'userPassword' => $password,
|
|
'displayName' => $user->nickname,
|
|
'mail' => $user->email,
|
|
];
|
|
if ($userimg) {
|
|
$attrs['jpegPhoto'] = $userimg;
|
|
}
|
|
self::static()->create($attrs);
|
|
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap']));
|
|
$user->save();
|
|
} catch (LdapRecordException $e) {
|
|
info("[LDAP] sync fail: " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新
|
|
* @param $email
|
|
* @param $array
|
|
* @return void
|
|
*/
|
|
public static function userUpdate($email, $array)
|
|
{
|
|
if (empty($array)) {
|
|
return;
|
|
}
|
|
if (!self::initConfig()) {
|
|
return;
|
|
}
|
|
try {
|
|
$row = self::findByEmail($email);
|
|
$row?->update($array);
|
|
} catch (\Exception $e) {
|
|
info("[LDAP] update fail: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除
|
|
* @param $email
|
|
* @return void
|
|
*/
|
|
public static function userDelete($email)
|
|
{
|
|
if (!self::initConfig()) {
|
|
return;
|
|
}
|
|
try {
|
|
$row = self::findByEmail($email);
|
|
$row?->delete();
|
|
} catch (\Exception $e) {
|
|
info("[LDAP] delete fail: " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|