Files
webman-buildadmin/app/common/service/MobileAuthDeviceService.php
2026-05-29 14:25:59 +08:00

169 lines
5.2 KiB
PHP
Raw Permalink 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
declare(strict_types=1);
namespace app\common\service;
use app\common\facade\Token;
use app\common\library\Auth;
use support\Redis;
use Throwable;
/**
* 移动端单设备登录auth-token 绑定 device_id用户登录态绑定当前设备异设备踢下线。
*/
final class MobileAuthDeviceService
{
private const PREFIX_AUTH_DEVICE = 'dfw:v1:auth_token:device:';
private const PREFIX_USER_DEVICE = 'dfw:v1:user:active_device:';
public static function bindAuthTokenDevice(string $authToken, string $deviceId, int $ttlSeconds): void
{
$authToken = trim($authToken);
$deviceId = trim($deviceId);
if ($authToken === '' || $deviceId === '' || $ttlSeconds <= 0) {
return;
}
try {
Redis::setEx(self::PREFIX_AUTH_DEVICE . self::tokenStorageKey($authToken), $ttlSeconds, $deviceId);
} catch (Throwable) {
// Redis 不可用时跳过绑定后续校验将拒绝已登录请求fail closed
}
}
public static function getAuthTokenDevice(string $authToken): string
{
$authToken = trim($authToken);
if ($authToken === '') {
return '';
}
try {
$raw = Redis::get(self::PREFIX_AUTH_DEVICE . self::tokenStorageKey($authToken));
} catch (Throwable) {
return '';
}
if ($raw === false || $raw === null) {
return '';
}
return trim((string) $raw);
}
public static function bindUserActiveDevice(int $userId, string $deviceId, int $ttlSeconds): void
{
if ($userId <= 0) {
return;
}
$deviceId = trim($deviceId);
if ($deviceId === '' || $ttlSeconds <= 0) {
return;
}
try {
Redis::setEx(self::PREFIX_USER_DEVICE . $userId, $ttlSeconds, $deviceId);
} catch (Throwable) {
// ignore
}
}
public static function getUserActiveDevice(int $userId): string
{
if ($userId <= 0) {
return '';
}
try {
$raw = Redis::get(self::PREFIX_USER_DEVICE . $userId);
} catch (Throwable) {
return '';
}
if ($raw === false || $raw === null) {
return '';
}
return trim((string) $raw);
}
/**
* 用户登录/注册成功后:清除其它会话,绑定当前 auth-token 对应设备,并恢复本次登录签发的 token。
*
* @param string $userToken 本次登录的 user-tokenclear 后需写回)
* @param string $refreshToken 本次登录的 refresh_token可为空
*/
public static function onUserLogin(
int $userId,
string $authToken,
string $userToken,
string $refreshToken = '',
?int $userTokenTtl = null,
?int $refreshTokenTtl = null
): void {
if ($userId <= 0) {
return;
}
Token::clear(Auth::TOKEN_TYPE, $userId);
Token::clear(Auth::TOKEN_TYPE . '-refresh', $userId);
$userTtl = $userTokenTtl ?? (int) config('buildadmin.user_token_keep_time', 259200);
$refreshTtl = $refreshTokenTtl ?? 2592000;
if ($userToken !== '') {
Token::set($userToken, Auth::TOKEN_TYPE, $userId, $userTtl);
}
if ($refreshToken !== '') {
Token::set($refreshToken, Auth::TOKEN_TYPE . '-refresh', $userId, $refreshTtl);
}
$deviceId = self::getAuthTokenDevice($authToken);
if ($deviceId === '') {
return;
}
self::bindUserActiveDevice($userId, $deviceId, $userTtl);
}
/**
* 已登录请求校验auth-token 设备须与用户当前绑定设备一致。
*
* @return string|null 失败时返回多语言 message key成功返回 null
*/
/**
* 用户主动退出:清除 Redis 中的活跃设备绑定。
*/
public static function onUserLogout(int $userId): void
{
if ($userId <= 0) {
return;
}
try {
Redis::del(self::PREFIX_USER_DEVICE . $userId);
} catch (Throwable) {
// ignore
}
}
public static function validateUserDeviceSession(string $authToken, int $userId): ?string
{
if ($userId <= 0) {
return null;
}
$authDevice = self::getAuthTokenDevice($authToken);
if ($authDevice === '') {
return 'auth-token is invalid or expired';
}
$activeDevice = self::getUserActiveDevice($userId);
if ($activeDevice === '') {
$ttl = (int) config('buildadmin.user_token_keep_time', 259200);
self::bindUserActiveDevice($userId, $authDevice, $ttl);
return null;
}
if (!hash_equals($activeDevice, $authDevice)) {
return 'Logged in on another device, please login again.';
}
return null;
}
private static function tokenStorageKey(string $token): string
{
$config = config('buildadmin.token');
$algo = is_array($config) && isset($config['algo']) ? (string) $config['algo'] : 'ripemd160';
$key = is_array($config) && isset($config['key']) ? (string) $config['key'] : '';
return hash_hmac($algo, $token, $key);
}
}