Files

167 lines
4.9 KiB
PHP
Raw Permalink Normal View History

2025-05-12 00:19:35 +08:00
<?php
namespace App\Service\common;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Exception;
2026-08-10 13:58:14 +08:00
/**
* JWT 签发与解析服务
* firebase/php-jwt HS256 要求密钥至少 256 bit32 字节),过短会抛 Provided key is too short
*/
2025-05-12 00:19:35 +08:00
class JWTService
{
private static mixed $_instance;
2026-08-10 13:58:14 +08:00
private string $secretKey;
2025-05-12 00:19:35 +08:00
private string $token;
2026-08-10 13:58:14 +08:00
/**
* 构造时从配置读取密钥,并兜底保证满足 HS256 最小长度
*/
public function __construct()
{
// 优先读环境变量 JWT_SECRET长度不足时用 hash 派生,避免库直接报错
$secret = (string) config('nl.jwt.secret', '');
if (strlen($secret) < 32) {
$secret = hash('sha256', $secret !== '' ? $secret : 'nl_admin_jwt_fallback_secret');
}
$this->secretKey = $secret;
}
2025-05-12 00:19:35 +08:00
/**
* 获取实例
* @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();
2025-05-12 16:13:46 +08:00
$expirationTime = $issuedAt + config('nl.redis.jwt_ttl', 600000);
2025-05-12 00:19:35 +08:00
$payload = [
'iat' => $issuedAt,
'exp' => $expirationTime,
'data' => $data
];
2025-05-12 16:13:46 +08:00
RedisService::getInstance()->init(config('nl.redis.jwt'))->set($data['id'], json_encode($data));
2025-05-12 00:19:35 +08:00
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();
2025-05-12 16:13:46 +08:00
$user = RedisService::getInstance()->init(config('nl.redis.jwt'))->get($jwt->data->id);
2025-05-12 00:19:35 +08:00
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);
}
2025-05-12 00:19:35 +08:00
}