161 lines
7.8 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|