AdminJwtSession.php 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Admin;
  4. use Illuminate\Support\Carbon;
  5. use Illuminate\Support\Facades\Cache;
  6. use Illuminate\Support\Str;
  7. /**
  8. * 后台登录态滑动续期:JWT 的 exp 只是绝对上限(config app.admin_jwt_max_lifetime),
  9. * 真正的有效期由缓存里按 sid 记的最后活跃时间决定,空闲超过 app.admin_jwt_idle 秒失效。
  10. * 每次通过 admin.jwt 的请求都会刷新活跃时间,前端无需换 token。
  11. * 不带 sid 的旧 token(上线前签发)仍按自身 exp 判断,自然过期后重新登录即可。
  12. */
  13. class AdminJwtSession
  14. {
  15. /** 活跃时间写缓存的最小间隔,避免每个请求都写 Redis */
  16. private const TOUCH_INTERVAL = 60;
  17. public function __construct(private JwtService $jwt)
  18. {
  19. }
  20. public static function idleSeconds(): int
  21. {
  22. return max(60, (int) config('app.admin_jwt_idle', 86400));
  23. }
  24. public function issue(Admin $admin): string
  25. {
  26. $sid = Str::random(32);
  27. $token = $this->jwt->generateToken($admin, max(self::idleSeconds(), (int) config('app.admin_jwt_max_lifetime', 2592000)), ['sid' => $sid]);
  28. Cache::put(self::key($sid), $this->now(), self::idleSeconds());
  29. return $token;
  30. }
  31. /** 校验 token:签名/exp 有效且未空闲超时,返回解码后的载荷,否则 null */
  32. public function validate(?string $token): ?object
  33. {
  34. $session = $this->jwt->validateToken((string) $token);
  35. if (!$session || !isset($session->user_id)) return null;
  36. if (!isset($session->sid)) return $session;
  37. return $this->lastActivity($session) === null ? null : $session;
  38. }
  39. /** 记一次活跃,续上空闲时长 */
  40. public function touch(object $session): void
  41. {
  42. if (!isset($session->sid)) return;
  43. $now = $this->now();
  44. $last = $this->lastActivity($session);
  45. if ($last !== null && $now - $last < self::TOUCH_INTERVAL) return;
  46. Cache::put(self::key((string) $session->sid), $now, self::idleSeconds());
  47. }
  48. /** 当前登录态的失效时间戳:min(JWT exp, 最后活跃 + 空闲时长) */
  49. public function expiresAt(object $session): ?int
  50. {
  51. $exp = isset($session->exp) ? (int) $session->exp : null;
  52. if (!isset($session->sid)) return $exp;
  53. $last = $this->lastActivity($session);
  54. if ($last === null) return null;
  55. $idleEnd = $last + self::idleSeconds();
  56. return $exp === null ? $idleEnd : min($exp, $idleEnd);
  57. }
  58. /**
  59. * 实时推送用的会话到期时间。同一 socket 上各频道的到期时间必须一致(见 RejectExpiredAdminRealtimeSubscription),
  60. * 所以首次授权时定下 min(exp, 最后活跃 + 空闲时长),到点前复用;到点后 relay 断开连接,
  61. * Echo 重连时若登录态仍有效则拿到新的到期时间。该值不晚于真实空闲失效时间(活跃只会顺延)。
  62. */
  63. public function realtimeExpiresAt(object $session): ?int
  64. {
  65. if (!isset($session->sid)) return $this->expiresAt($session);
  66. $key = self::key((string) $session->sid) . ':realtime';
  67. $cached = (int) Cache::get($key, 0);
  68. $now = $this->now();
  69. if ($cached > $now) return $cached;
  70. $expiresAt = $this->expiresAt($session);
  71. if ($expiresAt !== null && $expiresAt > $now) Cache::put($key, $expiresAt, $expiresAt - $now);
  72. return $expiresAt;
  73. }
  74. public function revoke(object $session): void
  75. {
  76. if (!isset($session->sid)) return;
  77. Cache::forget(self::key((string) $session->sid));
  78. Cache::forget(self::key((string) $session->sid) . ':realtime');
  79. }
  80. private function lastActivity(object $session): ?int
  81. {
  82. $value = Cache::get(self::key((string) $session->sid));
  83. return $value === null ? null : (int) $value;
  84. }
  85. private static function key(string $sid): string
  86. {
  87. return "admin_jwt_active:{$sid}";
  88. }
  89. private function now(): int
  90. {
  91. return Carbon::now()->getTimestamp();
  92. }
  93. }