| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- <?php
- namespace App\Services;
- use App\Models\Admin;
- use Illuminate\Support\Carbon;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\Support\Str;
- /**
- * 后台登录态滑动续期:JWT 的 exp 只是绝对上限(config app.admin_jwt_max_lifetime),
- * 真正的有效期由缓存里按 sid 记的最后活跃时间决定,空闲超过 app.admin_jwt_idle 秒失效。
- * 每次通过 admin.jwt 的请求都会刷新活跃时间,前端无需换 token。
- * 不带 sid 的旧 token(上线前签发)仍按自身 exp 判断,自然过期后重新登录即可。
- */
- class AdminJwtSession
- {
- /** 活跃时间写缓存的最小间隔,避免每个请求都写 Redis */
- private const TOUCH_INTERVAL = 60;
- public function __construct(private JwtService $jwt)
- {
- }
- public static function idleSeconds(): int
- {
- return max(60, (int) config('app.admin_jwt_idle', 86400));
- }
- public function issue(Admin $admin): string
- {
- $sid = Str::random(32);
- $token = $this->jwt->generateToken($admin, max(self::idleSeconds(), (int) config('app.admin_jwt_max_lifetime', 2592000)), ['sid' => $sid]);
- Cache::put(self::key($sid), $this->now(), self::idleSeconds());
- return $token;
- }
- /** 校验 token:签名/exp 有效且未空闲超时,返回解码后的载荷,否则 null */
- public function validate(?string $token): ?object
- {
- $session = $this->jwt->validateToken((string) $token);
- if (!$session || !isset($session->user_id)) return null;
- if (!isset($session->sid)) return $session;
- return $this->lastActivity($session) === null ? null : $session;
- }
- /** 记一次活跃,续上空闲时长 */
- public function touch(object $session): void
- {
- if (!isset($session->sid)) return;
- $now = $this->now();
- $last = $this->lastActivity($session);
- if ($last !== null && $now - $last < self::TOUCH_INTERVAL) return;
- Cache::put(self::key((string) $session->sid), $now, self::idleSeconds());
- }
- /** 当前登录态的失效时间戳:min(JWT exp, 最后活跃 + 空闲时长) */
- public function expiresAt(object $session): ?int
- {
- $exp = isset($session->exp) ? (int) $session->exp : null;
- if (!isset($session->sid)) return $exp;
- $last = $this->lastActivity($session);
- if ($last === null) return null;
- $idleEnd = $last + self::idleSeconds();
- return $exp === null ? $idleEnd : min($exp, $idleEnd);
- }
- /**
- * 实时推送用的会话到期时间。同一 socket 上各频道的到期时间必须一致(见 RejectExpiredAdminRealtimeSubscription),
- * 所以首次授权时定下 min(exp, 最后活跃 + 空闲时长),到点前复用;到点后 relay 断开连接,
- * Echo 重连时若登录态仍有效则拿到新的到期时间。该值不晚于真实空闲失效时间(活跃只会顺延)。
- */
- public function realtimeExpiresAt(object $session): ?int
- {
- if (!isset($session->sid)) return $this->expiresAt($session);
- $key = self::key((string) $session->sid) . ':realtime';
- $cached = (int) Cache::get($key, 0);
- $now = $this->now();
- if ($cached > $now) return $cached;
- $expiresAt = $this->expiresAt($session);
- if ($expiresAt !== null && $expiresAt > $now) Cache::put($key, $expiresAt, $expiresAt - $now);
- return $expiresAt;
- }
- public function revoke(object $session): void
- {
- if (!isset($session->sid)) return;
- Cache::forget(self::key((string) $session->sid));
- Cache::forget(self::key((string) $session->sid) . ':realtime');
- }
- private function lastActivity(object $session): ?int
- {
- $value = Cache::get(self::key((string) $session->sid));
- return $value === null ? null : (int) $value;
- }
- private static function key(string $sid): string
- {
- return "admin_jwt_active:{$sid}";
- }
- private function now(): int
- {
- return Carbon::now()->getTimestamp();
- }
- }
|