1.优化设备只能登录一个
This commit is contained in:
@@ -12,6 +12,7 @@ use app\common\model\UserScoreLog;
|
|||||||
use app\common\model\UserMoneyLog;
|
use app\common\model\UserMoneyLog;
|
||||||
use app\common\controller\Frontend;
|
use app\common\controller\Frontend;
|
||||||
use app\common\facade\Token as TokenFacade;
|
use app\common\facade\Token as TokenFacade;
|
||||||
|
use app\common\service\MobileAuthDeviceService;
|
||||||
use support\think\Db;
|
use support\think\Db;
|
||||||
use support\validation\Validator;
|
use support\validation\Validator;
|
||||||
use support\validation\ValidationException;
|
use support\validation\ValidationException;
|
||||||
@@ -44,6 +45,10 @@ class Account extends Frontend
|
|||||||
|
|
||||||
$user = $this->auth->getUser();
|
$user = $this->auth->getUser();
|
||||||
$userId = intval(strval($user->id));
|
$userId = intval(strval($user->id));
|
||||||
|
$deviceError = MobileAuthDeviceService::validateUserDeviceSession($authToken, $userId);
|
||||||
|
if ($deviceError !== null) {
|
||||||
|
return $this->mobileResult(1101, $deviceError);
|
||||||
|
}
|
||||||
$coinBalance = WithdrawFlow::amountString($user->coin ?? '0');
|
$coinBalance = WithdrawFlow::amountString($user->coin ?? '0');
|
||||||
|
|
||||||
// 打码量 / 提现配额快照
|
// 打码量 / 提现配额快照
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace app\api\controller;
|
|||||||
use app\common\facade\Token;
|
use app\common\facade\Token;
|
||||||
use app\common\library\Auth as UserAuth;
|
use app\common\library\Auth as UserAuth;
|
||||||
use app\common\model\User;
|
use app\common\model\User;
|
||||||
|
use app\common\service\MobileAuthDeviceService;
|
||||||
use ba\Random;
|
use ba\Random;
|
||||||
use support\think\Db;
|
use support\think\Db;
|
||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
@@ -83,6 +84,8 @@ class Auth extends MobileBase
|
|||||||
return $this->mobileError(2000, 'Registered successfully but login failed');
|
return $this->mobileError(2000, 'Registered successfully but login failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->bindMobileDeviceSession($request);
|
||||||
|
|
||||||
return $this->mobileSuccess($this->buildLoginPayload());
|
return $this->mobileSuccess($this->buildLoginPayload());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +109,9 @@ class Auth extends MobileBase
|
|||||||
if (!$ok) {
|
if (!$ok) {
|
||||||
return $this->mobileError(1101, 'Incorrect account or password');
|
return $this->mobileError(1101, 'Incorrect account or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->bindMobileDeviceSession($request);
|
||||||
|
|
||||||
return $this->mobileSuccess($this->buildLoginPayload());
|
return $this->mobileSuccess($this->buildLoginPayload());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,14 +132,41 @@ class Auth extends MobileBase
|
|||||||
return $this->mobileError(1101, 'Login status has expired');
|
return $this->mobileError(1101, 'Login status has expired');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$authToken = trim((string) $request->header('auth-token', ''));
|
||||||
|
$userId = filter_var($tokenData['user_id'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
|
if ($userId === false || $userId <= 0) {
|
||||||
|
return $this->mobileError(1101, 'Login status has expired');
|
||||||
|
}
|
||||||
|
$deviceError = MobileAuthDeviceService::validateUserDeviceSession($authToken, (int) $userId);
|
||||||
|
if ($deviceError !== null) {
|
||||||
|
return $this->mobileError(1101, $deviceError);
|
||||||
|
}
|
||||||
|
|
||||||
$newToken = Random::uuid();
|
$newToken = Random::uuid();
|
||||||
Token::set($newToken, UserAuth::TOKEN_TYPE, $tokenData['user_id'], config('buildadmin.user_token_keep_time', 259200));
|
Token::set($newToken, UserAuth::TOKEN_TYPE, (int) $userId, config('buildadmin.user_token_keep_time', 259200));
|
||||||
return $this->mobileSuccess([
|
return $this->mobileSuccess([
|
||||||
'user-token' => $newToken,
|
'user-token' => $newToken,
|
||||||
'expires_in' => config('buildadmin.user_token_keep_time', 259200),
|
'expires_in' => config('buildadmin.user_token_keep_time', 259200),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function bindMobileDeviceSession(Request $request): void
|
||||||
|
{
|
||||||
|
if (!$this->auth->isLogin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$authToken = trim((string) $request->header('auth-token', ''));
|
||||||
|
if ($authToken === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MobileAuthDeviceService::onUserLogin(
|
||||||
|
(int) $this->auth->id,
|
||||||
|
$authToken,
|
||||||
|
$this->auth->getToken(),
|
||||||
|
$this->auth->getRefreshToken()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function buildLoginPayload(): array
|
private function buildLoginPayload(): array
|
||||||
{
|
{
|
||||||
$userInfo = $this->auth->getUserInfo();
|
$userInfo = $this->auth->getUserInfo();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace app\api\controller;
|
|||||||
|
|
||||||
use app\common\controller\Frontend;
|
use app\common\controller\Frontend;
|
||||||
use app\common\facade\Token;
|
use app\common\facade\Token;
|
||||||
|
use app\common\service\MobileAuthDeviceService;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
use function response;
|
use function response;
|
||||||
@@ -28,6 +29,7 @@ abstract class MobileBase extends Frontend
|
|||||||
$parts = explode('/', $path);
|
$parts = explode('/', $path);
|
||||||
$action = $parts[array_key_last($parts)] ?? '';
|
$action = $parts[array_key_last($parts)] ?? '';
|
||||||
$needAuthToken = !action_in_arr($this->noNeedAuthToken, $action);
|
$needAuthToken = !action_in_arr($this->noNeedAuthToken, $action);
|
||||||
|
$authToken = '';
|
||||||
if ($needAuthToken) {
|
if ($needAuthToken) {
|
||||||
$authToken = trim((string) $request->header('auth-token', ''));
|
$authToken = trim((string) $request->header('auth-token', ''));
|
||||||
if ($authToken === '') {
|
if ($authToken === '') {
|
||||||
@@ -41,7 +43,20 @@ abstract class MobileBase extends Frontend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->initializeFrontend($request);
|
$response = $this->initializeFrontend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$needLogin = !action_in_arr($this->noNeedLogin, $action);
|
||||||
|
if ($needAuthToken && $needLogin && $this->auth->isLogin()) {
|
||||||
|
$deviceError = MobileAuthDeviceService::validateUserDeviceSession($authToken, (int) $this->auth->id);
|
||||||
|
if ($deviceError !== null) {
|
||||||
|
return $this->mobileError(1101, $deviceError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function mobileSuccess(array $data = [], string $message = 'ok'): Response
|
protected function mobileSuccess(array $data = [], string $message = 'ok'): Response
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace app\api\controller;
|
|||||||
|
|
||||||
use app\common\controller\Api;
|
use app\common\controller\Api;
|
||||||
use app\common\facade\Token;
|
use app\common\facade\Token;
|
||||||
|
use app\common\service\MobileAuthDeviceService;
|
||||||
use ba\Random;
|
use ba\Random;
|
||||||
use Webman\Http\Request;
|
use Webman\Http\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
@@ -64,6 +65,7 @@ class V1 extends Api
|
|||||||
$token = Random::uuid();
|
$token = Random::uuid();
|
||||||
$expire = 60 * 60 * 24;
|
$expire = 60 * 60 * 24;
|
||||||
Token::set($token, 'auth-token', 0, $expire);
|
Token::set($token, 'auth-token', 0, $expire);
|
||||||
|
MobileAuthDeviceService::bindAuthTokenDevice($token, $deviceId, $expire);
|
||||||
|
|
||||||
return $this->mobileResult(1, 'ok', [
|
return $this->mobileResult(1, 'ok', [
|
||||||
'auth_token' => $token,
|
'auth_token' => $token,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
'Login expired, please login again.' => 'Login expired, please login again.',
|
'Login expired, please login again.' => 'Login expired, please login again.',
|
||||||
|
'Logged in on another device, please login again.' => 'Your account was logged in on another device. Please sign in again.',
|
||||||
'Account not exist' => 'Account does not exist',
|
'Account not exist' => 'Account does not exist',
|
||||||
'Account disabled' => 'Account is disabled',
|
'Account disabled' => 'Account is disabled',
|
||||||
'Token login failed' => 'Token login failed',
|
'Token login failed' => 'Token login failed',
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ return [
|
|||||||
'Mobile' => '手机号',
|
'Mobile' => '手机号',
|
||||||
'Password' => '密码',
|
'Password' => '密码',
|
||||||
'Login expired, please login again.' => '登录过期,请重新登录。',
|
'Login expired, please login again.' => '登录过期,请重新登录。',
|
||||||
|
'Logged in on another device, please login again.' => '您的账号已在其他设备登录,请重新登录',
|
||||||
'Account not exist' => '帐户不存在',
|
'Account not exist' => '帐户不存在',
|
||||||
'Account disabled' => '帐户已禁用',
|
'Account disabled' => '帐户已禁用',
|
||||||
'Token login failed' => '令牌登录失败',
|
'Token login failed' => '令牌登录失败',
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ final class GameWebSocketAuthHelper
|
|||||||
return self::deny('user-token has no user_id', $authToken, $userToken, '');
|
return self::deny('user-token has no user_id', $authToken, $userToken, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$deviceError = MobileAuthDeviceService::validateUserDeviceSession($authToken, (int) $userId);
|
||||||
|
if ($deviceError !== null) {
|
||||||
|
return self::deny($deviceError, $authToken, $userToken, '');
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'user_id' => (int) $userId,
|
'user_id' => (int) $userId,
|
||||||
|
|||||||
153
app/common/service/MobileAuthDeviceService.php
Normal file
153
app/common/service/MobileAuthDeviceService.php
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user