secretKey = $secret; } /** * 获取实例 * @return null|static */ public static function getInstance(): null|static { $name = get_called_class(); if (!isset(self::$_instance[$name])) { self::$_instance[$name] = new static(); } return self::$_instance[$name]; } /** * 获取 Token * @return $this|null * @throws Exception */ public function getToken(): null|static { $token = request()->bearerToken(); if (empty($token)) return UtilsService::getInstance()->notAuth('请先登录'); $this->token = $token; return $this; } /** * 生成 JWT Token * @param array $data 要嵌入到 token 中的数据 * @return string */ public function generateToken(array $data): string { $issuedAt = time(); $expirationTime = $issuedAt + config('nl.redis.jwt_ttl', 600000); $payload = [ 'iat' => $issuedAt, 'exp' => $expirationTime, 'data' => $data ]; RedisService::getInstance()->init(config('nl.redis.jwt'))->set($data['id'], json_encode($data)); return JWT::encode($payload, $this->secretKey, 'HS256'); } /** * 解析 JWT Token * @return object|null 返回解码后的 payload 或者 null 如果验证失败 * @throws Exception */ public function parseToken(): ?object { try { if (empty($this->token)) return UtilsService::getInstance()->notAuth('请先登录'); return JWT::decode($this->token, new Key($this->secretKey, 'HS256')); } catch (Exception $e) { return UtilsService::getInstance()->notAuth('【1】Token解析失败!请重新登录:'. $e->getMessage()); } } /** * 解析 JWT Token * @return array|null 返回解码后的 payload 或者 null 如果验证失败 * @throws Exception */ public function getUserInfo(): ?array { try { $jwt = $this->parseToken(); $user = RedisService::getInstance()->init(config('nl.redis.jwt'))->get($jwt->data->id); if (empty($user)) return UtilsService::getInstance()->notAuth('登录状态过期'); return json_decode($user, true); } catch (Exception $e) { return UtilsService::getInstance()->notAuth('【2】Token解析失败!请重新登录:'. $e->getMessage()); } } /** * 续签 JWT Token * @return string|null 新的 JWT 字符串或者 null 如果原 token 已过期或无效 * @throws Exception */ public function refreshToken(): ?string { $decoded = $this->parseToken(); if ($decoded === null || !property_exists($decoded, 'data')) { return null; } return $this->generateToken((array)$decoded->data); } /** * 解析已过期但签名有效的 token * * 续签场景下 token 必然已经过期,正常 decode 会直接抛 ExpiredException。 * 这里临时放宽 leeway 让 exp 校验通过,签名与 Redis 会话仍然照常校验, * 所以过期的 token 依旧不能凭空续签——会话被登出或超过宽限期就必须重新登录。 * * @param int $leeway 允许的过期宽限秒数 */ public function parseExpiringToken(int $leeway): ?object { $token = request()->bearerToken(); if (empty($token)) { return null; } $origin = JWT::$leeway; JWT::$leeway = max(0, $leeway); try { return JWT::decode($token, new Key($this->secretKey, 'HS256')); } catch (Exception $e) { return null; } finally { JWT::$leeway = $origin; } } /** * 作废某个用户的登录态:删掉 Redis 会话,手里的 token 立即失效 * (getUserInfo 拿不到会话就会 notAuth,所以不需要维护黑名单) */ public function revoke(int $userId): bool { if ($userId <= 0) { return false; } return RedisService::getInstance()->init(config('nl.redis.jwt'))->del($userId); } }