| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- <?php
- namespace think\facade {
- final class Cache {
- public static array $entries = [];
- public static function get($key, $default = null) {
- $entry = self::$entries[$key] ?? null;
- if (!$entry || $entry['until'] <= $GLOBALS['idle_token_now']) { unset(self::$entries[$key]); return $default; }
- return $entry['value'];
- }
- public static function set($key, $value, $ttl): bool { self::$entries[$key] = ['value' => $value, 'until' => $GLOBALS['idle_token_now'] + $ttl, 'ttl' => $ttl]; return true; }
- public static function delete($key): bool { unset(self::$entries[$key]); return true; }
- }
- }
- namespace app\common\cache { function time(): int { return $GLOBALS['idle_token_now']; } }
- namespace app\api\service { function time(): int { return $GLOBALS['idle_token_now']; } }
- namespace app\common\service {
- final class JsonService { public static function fail($msg, $data = [], $code = 0, $show = 1): array { return compact('msg', 'data', 'code', 'show'); } }
- }
- namespace {
- error_reporting(E_ALL & ~E_DEPRECATED);
- function app($name = null) { $app = \think\Container::getInstance(); return $name === null ? $app : $app->make($name); }
- function create_token($userId): string { static $sequence = 0; return 'token-' . $userId . '-' . ++$sequence; }
- $repo = getenv('TOKEN_SITE_REPO') ?: dirname(__DIR__, 3) . '/melbet_sport-api';
- $stage = getenv('TOKEN_SITE_STAGE') ?: $repo;
- $loader = require $repo . '/vendor/autoload.php';
- $loader->setPsr4('app\\', [$stage . '/app/', $repo . '/app/']);
- $app = new \think\App($repo . '/'); \think\Container::setInstance($app);
- $db = new \think\DbManager();
- $db->setConfig(['default' => 'isolated', 'connections' => ['isolated' => [
- 'type' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_', 'fields_strict' => true, 'debug' => false,
- ]], 'auto_timestamp' => false]);
- $app->instance(\think\DbManager::class, $db); $app->instance('log', new \Psr\Log\NullLogger());
- $db->execute('CREATE TABLE bot_user_session (id INTEGER PRIMARY KEY,user_id TEXT,token TEXT,expire_time INTEGER,create_time INTEGER,update_time INTEGER)');
- $db->execute('CREATE TABLE bot_users (id INTEGER PRIMARY KEY,user_id TEXT,account TEXT,avatar TEXT,"from" INTEGER,recharge_channel_group_id INTEGER,delete_time TEXT,created_at TEXT,updated_at TEXT)');
- \think\facade\Db::table('bot_users')->insert(['id' => 1, 'user_id' => '100001', 'account' => 'member', 'avatar' => '', 'from' => 1, 'recharge_channel_group_id' => 1]);
- $GLOBALS['idle_token_now'] = 1792912735;
- $start = $GLOBALS['idle_token_now'];
- $checks = 0;
- function check($actual, $expected, string $message): void {
- global $checks; ++$checks;
- if ($actual !== $expected) throw new \RuntimeException($message . ': actual=' . json_encode($actual) . ' expected=' . json_encode($expected));
- }
- function row(string $token): ?array { return \think\facade\Db::table('bot_user_session')->where('token', $token)->find(); }
- class TokenRequest {
- public $userInfo; public $userId;
- public object $controllerObject;
- public function __construct(private ?string $token, bool $public = false) {
- $this->controllerObject = new class($public) { public function __construct(private bool $public) {} public function isNotNeedLogin(): bool { return $this->public; } };
- }
- public function header($name) { return $this->token; }
- }
- function authenticate(?string $token, bool $public = false): array {
- $request = new TokenRequest($token, $public);
- return (new \app\api\http\middleware\LoginMiddleware())->handle($request, static fn($r) => ['code' => 1, 'user_id' => $r->userId, 'userInfo' => $r->userInfo]);
- }
- $cache = new \app\common\cache\UserTokenCache();
- $info = \app\api\service\UserTokenService::setToken('100001');
- $token = $info['token'];
- check($info['expire_time'], $start + 86400, 'Login issues 24-hour token');
- check(row($token)['update_time'], $start, 'Login records the activity clock');
- check(\think\facade\Cache::$entries['token_user_' . $token]['ttl'], 86400, 'Cache TTL equals the effective expiry');
- $GLOBALS['idle_token_now'] = $start + 3600;
- $unchanged = row($token);
- $readOnly = $cache->getUserInfo($token);
- check($readOnly['expire_time'], $start + 86400, 'Read-only crypto/WS lookup preserves deadline');
- check(row($token), $unchanged, 'Cache lookup never renews activity');
- \think\facade\Cache::$entries = [];
- check($cache->getUserInfo($token)['expire_time'], $start + 86400, 'Clearing cache does not restart the inactivity window');
- check(\think\facade\Cache::$entries['token_user_' . $token]['ttl'], 82800, 'Reloaded cache only gets remaining lifetime');
- $authenticated = authenticate($token);
- check($authenticated['code'], 1, 'Authenticated request succeeds');
- check($authenticated['userInfo']['expire_time'], $start + 3600 + 86400, 'Every valid request slides deadline by a full day');
- check(row($token)['update_time'], $start + 3600, 'Activity saved in database');
- check(\think\facade\Cache::$entries['token_user_' . $token]['ttl'], 86400, 'Renewal refreshes cache TTL');
- $GLOBALS['idle_token_now'] = $start + 25 * 3600 - 1;
- check(authenticate($token, true)['code'], 1, 'Valid authenticated polling on a public endpoint also renews');
- check(row($token)['expire_time'], $GLOBALS['idle_token_now'] + 86400, 'Active users stay logged in beyond an absolute login day');
- $lastActivity = $GLOBALS['idle_token_now'];
- $GLOBALS['idle_token_now'] = $lastActivity + 86400;
- $expiredBefore = row($token);
- \think\facade\Cache::set('token_user_' . $token, $info + ['unexpected' => 'old'], 7 * 86400);
- check(authenticate($token)['code'], 401, 'Exactly 24 hours without activity rejects authentication');
- check(\app\api\service\UserTokenService::overtimeToken($token), false, 'Expired token cannot be renewed directly');
- check(row($token), $expiredBefore, 'Expired request cannot revive the session');
- check($cache->getUserInfo($token), false, 'Stale cache cannot bypass expiry');
- check(authenticate($token, true)['user_id'], 0, 'Public endpoint treats invalid token as anonymous');
- check(authenticate(null)['code'], 401, 'Protected endpoint still requires a token');
- check(authenticate(null, true)['code'], 1, 'Anonymous public access stays available');
- // Interleave real logout/new-login between identity lookup and the conditional renewal.
- $raceMode = null; $raceToken = null; $replacementToken = null;
- $db->event('before_find', static function ($query) use (&$raceMode, &$raceToken, &$replacementToken) {
- if ($query->getTable() !== 'bot_users' || $raceMode === null) return;
- $mode = $raceMode; $raceMode = null;
- ++$GLOBALS['idle_token_now'];
- if ($mode === 'logout') \app\api\service\UserTokenService::expireToken($raceToken);
- else $replacementToken = \app\api\service\UserTokenService::setToken('100001')['token'];
- });
- foreach (['logout', 'replace'] as $mode) {
- $GLOBALS['idle_token_now'] += 60;
- $raceToken = \app\api\service\UserTokenService::setToken('100001')['token'];
- $GLOBALS['idle_token_now'] += 60;
- $raceMode = $mode;
- check(authenticate($raceToken)['code'], 401, 'Concurrent ' . $mode . ' wins over a stale renewal');
- if ($mode === 'logout') check(row($raceToken)['expire_time'], $GLOBALS['idle_token_now'], 'Concurrent logout is not overwritten');
- else {
- check(row($raceToken), null, 'Old token is not restored after a concurrent new login');
- check(row($replacementToken)['expire_time'], $GLOBALS['idle_token_now'] + 86400, 'Replacement token retains its deadline');
- }
- }
- // Seven-day legacy expiration cannot hide more than 24h inactivity.
- $GLOBALS['idle_token_now'] = $start + 3 * 86400;
- \think\facade\Db::table('bot_user_session')->insert(['id' => 2, 'user_id' => '100001', 'token' => 'legacy', 'expire_time' => $start + 7 * 86400, 'create_time' => $start, 'update_time' => $start]);
- $legacy = row('legacy');
- check(authenticate('legacy')['code'], 401, 'Legacy long expiry still obeys the last-activity cutoff');
- check(row('legacy'), $legacy, 'Legacy expired sessions are not silently reactivated');
- foreach ([null, 0] as $index => $updateTime) {
- $fallbackToken = 'fallback-' . $index;
- \think\facade\Db::table('bot_user_session')->insert(['user_id' => '100001', 'token' => $fallbackToken, 'expire_time' => $GLOBALS['idle_token_now'] + 7 * 86400, 'create_time' => $GLOBALS['idle_token_now'] - 3600, 'update_time' => $updateTime]);
- check($cache->getUserInfo($fallbackToken)['expire_time'], $GLOBALS['idle_token_now'] + 82800, 'Missing activity falls back to creation time');
- check(authenticate($fallbackToken)['userInfo']['expire_time'], $GLOBALS['idle_token_now'] + 86400, 'Valid legacy session can renew without a new login');
- }
- $fresh = \app\api\service\UserTokenService::setToken('100001');
- $newToken = $fresh['token'];
- \think\facade\Cache::set('token_user_' . $token, $info, 86400);
- check($cache->getUserInfo($token), false, 'Old token remains invalid after a new login even with stale cache');
- $GLOBALS['idle_token_now'] += 60;
- check(\app\api\service\UserTokenService::expireToken($newToken), true, 'Logout revokes current token');
- $revoked = row($newToken);
- check(authenticate($newToken)['code'], 401, 'Logout token cannot authenticate');
- check(row($newToken), $revoked, 'Logout token cannot be resurrected by renewal');
- check(\app\api\service\UserTokenService::overtimeToken('does-not-exist'), false, 'Missing session fails without a null dereference');
- $fresh = \app\api\service\UserTokenService::setToken('100001'); $newToken = $fresh['token'];
- $future = $GLOBALS['idle_token_now'] + 10;
- \think\facade\Db::table('bot_user_session')->where('token', $newToken)->update(['update_time' => $future, 'expire_time' => $future + 86400]);
- $laterRequest = row($newToken);
- check(authenticate($newToken)['code'], 1, 'Older concurrent request still sees the valid renewed session');
- check(row($newToken), $laterRequest, 'An older renewal never moves deadline backwards');
- \think\facade\Db::table('bot_users')->where('user_id', '100001')->update(['delete_time' => time()]);
- $beforeMissingUser = row($newToken);
- check(authenticate($newToken)['code'], 401, 'Deleted user cannot authenticate through a cached profile');
- check(row($newToken), $beforeMissingUser, 'Failed identity check does not count as activity');
- echo 'PASS ' . $checks . " member idle-token assertions (isolated SQLite and memory cache; no live sessions)\n";
- }
|