Files
zyt/server/app/adminapi/service/IdentitySessionService.php
2026-11-06 13:37:12 +08:00

161 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
namespace app\adminapi\service;
use app\adminapi\logic\LoginLogic;
use app\common\cache\AdminTokenCache;
use app\common\enum\AdminTerminalEnum;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminSession;
use app\common\service\JsonService;
use think\facade\Config;
use think\facade\Db;
/** 共用原账号密码与原会话,不信任桥接层提供的用户编号或root标记。 */
final class IdentitySessionService
{
/** 验证外部子系统的凭据,不创建、复用、刷新或注销原系统会话。 */
public static function verify(array $params): array
{
if (array_diff(array_keys($params), ['account', 'password']) !== []) {
JsonService::throw('请求包含不支持的字段');
}
$account = $params['account'] ?? null;
$password = $params['password'] ?? null;
if (!is_string($account) || $account === '' || strlen($account) > 4096 || str_contains($account, "\0")
|| !is_string($password) || $password === '' || strlen($password) > 4096 || str_contains($password, "\0")) {
JsonService::throw('登录参数不合法');
}
return Db::transaction(static function () use ($account, $password): array {
$admin = Admin::where('account', $account)->lock(true)->findOrEmpty();
$salt = (string) Config::get('project.unique_identification');
if ($admin->isEmpty() || (int) $admin['disable'] !== 0
|| !hash_equals((string) $admin['password'], create_password($password, $salt))) {
JsonService::throw('账号或密码错误,或账号已停用', [], -1);
}
return ['id' => (int) $admin['id'], 'account' => (string) $admin['account'],
'name' => (string) ($admin['name'] ?? ''), 'disable' => 0,
'requires_password_change' => (int) ($admin['is_paw'] ?? 1) === 0,
'requires_wecom_binding' => LoginLogic::adminMustBindWorkWechat([
'root' => (int) ($admin['root'] ?? 0),
'work_wechat_userid' => (string) ($admin['work_wechat_userid'] ?? ''),
])];
});
}
/** 与个人改密共用账号行锁,绝不信任事务前验证器的密码快照或调试免密结果。 */
public static function login(array $params): array
{
if (array_diff(array_keys($params), ['account', 'password', 'terminal']) !== []) {
JsonService::throw('请求包含不支持的字段');
}
$account = $params['account'] ?? null;
$password = $params['password'] ?? null;
$terminal = $params['terminal'] ?? null;
if (!is_string($account) || $account === '' || strlen($account) > 4096 || str_contains($account, "\0")
|| !is_string($password) || $password === '' || strlen($password) > 4096 || str_contains($password, "\0")
|| (!is_int($terminal) && !is_string($terminal))
|| !in_array((string) $terminal, ['1', '2', '7', '9', (string) AdminTerminalEnum::GROUP_IDENTITY], true)) {
JsonService::throw('登录参数不合法');
}
return Db::transaction(static function () use ($account, $password, $params): array {
$admin = Admin::where('account', $account)->lock(true)->findOrEmpty();
$salt = (string) Config::get('project.unique_identification');
if ($admin->isEmpty() || (int) $admin['disable'] !== 0
|| !hash_equals((string) $admin['password'], create_password($password, $salt))) {
JsonService::throw('账号或密码错误,或账号已停用', [], -1);
}
// 原登录的资料更新和会话签发均在该账号锁释放前完成。
return (new LoginLogic())->login($params);
});
}
public static function changePassword(string $token, array $params): void
{
if (array_diff(array_keys($params), ['old_password', 'password', 'password_confirm']) !== []) {
JsonService::throw('请求包含不支持的字段');
}
$password = $params['password'] ?? null;
if (!is_string($password) || strlen($password) < 6 || strlen($password) > 32 || str_contains($password, "\0")) {
JsonService::throw('密码长度须在6-32字节');
}
if ($password !== ($params['password_confirm'] ?? null)) {
JsonService::throw('两次输入的密码不一致');
}
$tokens = Db::transaction(static function () use ($token, $params, $password): array {
[$admin] = self::lockContext($token);
$firstPassword = (int) ($admin['is_paw'] ?? 1) === 0;
if (!$firstPassword && LoginLogic::adminMustBindWorkWechat([
'root' => (int) $admin['root'], 'work_wechat_userid' => (string) ($admin['work_wechat_userid'] ?? ''),
])) {
JsonService::throw('请先在原后台绑定企业微信', [], LoginLogic::CODE_NEED_BIND_WORK_WECHAT);
}
$salt = (string) Config::get('project.unique_identification');
if (!$firstPassword) {
$oldPassword = $params['old_password'] ?? null;
if (!is_string($oldPassword) || strlen($oldPassword) > 1024
|| !hash_equals((string) $admin['password'], create_password($oldPassword, $salt))) {
JsonService::throw('当前密码错误');
}
}
$id = (int) $admin['id'];
Admin::where('id', $id)->update([
'password' => create_password($password, $salt), 'is_paw' => 1, 'update_time' => time(),
]);
return self::expire($id, null);
});
self::clearTokens($tokens);
}
public static function logout(string $token, bool $all = false): void
{
$tokens = Db::transaction(static function () use ($token, $all): array {
[$admin] = self::lockContext($token);
return self::expire((int) $admin['id'], $all ? null : $token);
});
self::clearTokens($tokens);
}
/** 先锁账号再锁会话,避免两个会话同时改密时互相等待。 */
private static function lockContext(string $token): array
{
if ($token === '' || strlen($token) > 512 || preg_match('/[\x00-\x20\x7f]/', $token)) {
JsonService::throw('请先登录', [], -1);
}
$candidate = AdminSession::where('token', $token)->findOrEmpty();
if ($candidate->isEmpty()) {
JsonService::throw('登录已失效,请重新登录', [], -1);
}
$admin = Admin::where('id', (int) $candidate['admin_id'])->lock(true)->findOrEmpty();
if ($admin->isEmpty() || (int) $admin['disable'] !== 0) {
JsonService::throw('登录已失效,请重新登录', [], -1);
}
$session = AdminSession::where('admin_id', (int) $admin['id'])->where('token', $token)
->where('expire_time', '>', time())->lock(true)->findOrEmpty();
if ($session->isEmpty()) {
JsonService::throw('登录已失效,请重新登录', [], -1);
}
return [$admin, $session];
}
private static function expire(int $adminId, ?string $token): array
{
$query = static function () use ($adminId, $token) {
$builder = AdminSession::where('admin_id', $adminId);
return $token === null ? $builder : $builder->where('token', $token);
};
$tokens = $query()->column('token');
$query()->update(['expire_time' => time() - 1, 'update_time' => time()]);
return $tokens;
}
private static function clearTokens(array $tokens): void
{
$cache = new AdminTokenCache();
foreach ($tokens as $token) {
$cache->deleteAdminInfo((string) $token);
}
}
}