154 lines
4.9 KiB
PHP
154 lines
4.9 KiB
PHP
<?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-token(clear 后需写回)
|
||
* @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
|
||
*/
|
||
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);
|
||
}
|
||
}
|