初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
+855
View File
@@ -0,0 +1,855 @@
<?php
declare(strict_types=1);
namespace core\jwt;
use core\jwt\enum\ClientType;
use core\jwt\enum\LoginMode;
use core\jwt\enum\TokenStatus;
use core\jwt\enum\TokenType;
use core\jwt\ex\JwtException;
use core\jwt\ex\JwtRefreshTokenExpiredException;
use core\jwt\ex\JwtTokenExpiredException;
use core\jwt\ex\JwtTokenInBlacklistException;
use core\jwt\ex\JwtTokenInvalidException;
use core\jwt\interfaces\BlacklistStorageInterface;
use core\jwt\interfaces\TokenStorageInterface;
use core\jwt\storage\RedisTokenStorage;
use core\jwt\storage\RedisBlacklistStorage;
use Firebase\JWT\BeforeValidException;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\SignatureInvalidException;
use UnexpectedValueException;
class JwtToken
{
/**
* @var string 密钥
*/
protected string $secret;
/**
* @var string 加密算法
*/
protected string $algo;
/**
* @var array 配置
*/
protected array $config;
/**
* @var TokenStorageInterface Token 存储
*/
protected TokenStorageInterface $tokenStorage;
/**
* @var BlacklistStorageInterface 黑名单存储
*/
protected BlacklistStorageInterface $blacklistStorage;
/**
* @var Token|null 当前 Token
*/
protected ?Token $currentToken = null;
/**
* @var array|null 当前载荷缓存
*/
protected ?array $payloadCache = null;
public function __construct(
?TokenStorageInterface $tokenStorage = null,
?BlacklistStorageInterface $blacklistStorage = null,
array $config = []
)
{
$this->config = $config ?: config('core.jwt.app', []);
$this->secret = $this->config['secret'] ?? '';
$this->algo = $this->config['algo'] ?? 'HS256';
if (empty($this->secret) || $this->secret === 'your-secret-key-change-in-production') {
$this->secret = 'default-secret-key-for-development';
}
$this->tokenStorage = $tokenStorage ?? new RedisTokenStorage($this->config);
$this->blacklistStorage = $blacklistStorage ?? new RedisBlacklistStorage($this->config);
}
/**
* 获取 Token 存储
*/
public function getTokenStorage(): TokenStorageInterface
{
return $this->tokenStorage;
}
/**
* 获取黑名单存储
*/
public function getBlacklistStorage(): BlacklistStorageInterface
{
return $this->blacklistStorage;
}
/**
* 生成 Token
*
* @param string $id 用户ID(雪花ID,需用string)
* @param string $clientType 客户端类型(可扩展)
* @param array $extraData 额外数据
* @param string|null $loginMode 登录模式(null 使用配置默认)
*
* @return Token
* @throws \core\jwt2\ex\JwtException
*/
public function generate(
string $id,
string $clientType,
array $extraData = [],
?string $loginMode = null
): Token
{
// 验证客户端类型
if (!ClientType::isValid($clientType)) {
throw new JwtException("Invalid client type: {$clientType}");
}
$loginMode = $loginMode ?? ($this->config['login_mode'] ?? LoginMode::MULTI->value);
// 根据登录模式处理现有 Token
$this->handleLoginMode($id, $clientType, $loginMode);
// 生成 JTI
$jti = $this->generateJti();
// 生成 Access Token
$accessTtl = $this->config['ttl']['access'] ?? 7200;
$accessPayload = $this->buildPayload(
id: $id,
client: $clientType,
jti: $jti,
type: TokenType::ACCESS,
extra: $extraData
);
$accessToken = $this->makeToken($accessPayload);
// 生成 Refresh Token
$refreshTtl = $this->config['ttl']['refresh'] ?? 604800;
$refreshJti = $this->generateJti();
$refreshPayload = $this->buildPayload(
id: $id,
client: $clientType,
jti: $refreshJti,
type: TokenType::REFRESH,
extra: $extraData
);
$refreshToken = $this->makeToken($refreshPayload);
// 存储 Token 信息
$this->tokenStorage->save($jti, [
'id' => $id,
'client_type' => $clientType,
'refresh_jti' => $refreshJti,
'extra' => $extraData,
'login_mode' => $loginMode,
'created_at' => time(),
], $accessTtl);
// 创建 Token 对象
$fullPayload = array_merge($accessPayload, ['id' => $id]);
return Token::create($accessToken, $refreshToken, $accessTtl, $fullPayload);
}
/**
* 刷新 Token
*
* @param string|null $refreshToken 刷新令牌(null 使用请求中的)
*
* @return Token
* @throws \core\jwt\ex\JwtException
* @throws \core\jwt\ex\JwtRefreshTokenExpiredException
* @throws \core\jwt\ex\JwtTokenInBlacklistException
*/
public function refresh(?string $refreshToken = null): Token
{
$token = $refreshToken ?? $this->getRefreshTokenFromRequest();
if (empty($token)) {
throw new JwtException('Refresh token is required');
}
// 解析 Token
try {
$payload = $this->parseToken($token);
} catch (ExpiredException $e) {
throw new JwtRefreshTokenExpiredException('Refresh token has expired');
}
// 验证 Token 类型
if (($payload['type'] ?? '') !== TokenType::REFRESH->value) {
throw new JwtException('Invalid token type for refresh');
}
$id = (string)($payload['id'] ?? '');
$clientType = $payload['client'] ?? '';
$oldJti = $payload['jti'] ?? '';
if (empty($id)) {
throw new JwtException('Invalid user in token');
}
// 检查刷新令牌是否在黑名单
if ($this->blacklistStorage->has($oldJti)) {
throw new JwtTokenInBlacklistException('Token has been revoked');
}
// 通过 refresh_jti 反查对应的 access token 会话
[$oldAccessJti, $oldTokenInfo] = $this->findAccessTokenByRefreshJti($id, $clientType, $oldJti);
if ($oldTokenInfo === null) {
throw new JwtRefreshTokenExpiredException('Refresh token has been revoked');
}
// 获取额外数据
$extraData = $oldTokenInfo['extra'] ?? [];
$loginMode = $this->config['login_mode'] ?? $oldTokenInfo['login_mode'] ?? LoginMode::MULTI->value;
// 将旧 access_token 和 refresh_token 加入黑名单
$blacklistTtl = $this->config['ttl']['refresh'] ?? 604800;
if ($oldAccessJti !== '') {
$this->blacklistStorage->add($oldAccessJti, $blacklistTtl);
$this->tokenStorage->delete($oldAccessJti);
}
$this->blacklistStorage->add($oldJti, $blacklistTtl);
// 生成新 Token
return $this->generate($id, $clientType, $extraData, $loginMode);
}
/**
* 解析 Token
*
* @param string $token
*
* @return array
*/
public function parse(string $token): array
{
return $this->parseToken($token);
}
/**
* 获取当前用户
*
* @return array|null
*/
public function user(): ?array
{
$payload = $this->getPayloadFromRequest();
if (empty($payload)) {
return null;
}
return [
'id' => (string)($payload['id'] ?? ''),
'client_type' => $payload['client'] ?? '',
'jti' => $payload['jti'] ?? '',
'extra' => $payload['extra'] ?? [],
];
}
/**
* 获取当前用户ID(雪花ID)
*/
public function id(): ?string
{
$user = $this->user();
return $user ? $user['id'] : null;
}
/**
* 获取当前 Token
*/
public function getToken(): ?Token
{
if ($this->currentToken !== null) {
return $this->currentToken;
}
$payload = $this->getPayloadFromRequest();
if (empty($payload)) {
return null;
}
$accessToken = $this->getAccessTokenFromRequest();
if (empty($accessToken)) {
return null;
}
$accessTtl = $this->config['ttl']['access'] ?? 7200;
$this->currentToken = Token::create($accessToken, '', $accessTtl, $payload);
return $this->currentToken;
}
/**
* 验证 Token
*
* @param string|null $token
*
* @return array|null
* @throws \core\jwt\ex\JwtException
* @throws \core\jwt\ex\JwtTokenExpiredException
* @throws \core\jwt\ex\JwtTokenInBlacklistException
* @throws \core\jwt\ex\JwtTokenInvalidException
*/
public function verify(?string $token = null): ?array
{
$token = $token ?? $this->getAccessTokenFromRequest();
if (empty($token)) {
return null;
}
try {
$payload = $this->parseToken($token);
// 检查是否在黑名单
$jti = $payload['jti'] ?? '';
if (!empty($jti) && $this->blacklistStorage->has($jti)) {
throw new JwtTokenInBlacklistException('Token has been revoked');
}
// 检查 Token 类型
if (($payload['type'] ?? '') !== TokenType::ACCESS->value) {
throw new JwtException('Invalid token type');
}
// 检查用户是否存在
$id = (string)($payload['id'] ?? '');
if (empty($id)) {
throw new JwtException('Invalid user in token');
}
return $payload;
} catch (ExpiredException $e) {
throw new JwtTokenExpiredException('Token has expired');
} catch (SignatureInvalidException $e) {
throw new JwtTokenInvalidException('Invalid token signature');
} catch (BeforeValidException $e) {
throw new JwtTokenInvalidException('Token not yet valid');
} catch (UnexpectedValueException $e) {
throw new JwtTokenInvalidException('Invalid token format');
}
}
/**
* 退出登录(当前设备)
*
* @param string|null $token
*
* @return bool
*/
public function logout(?string $token = null): bool
{
$token = $token ?? $this->getAccessTokenFromRequest();
if (empty($token)) {
return false;
}
try {
$payload = $this->parseToken($token);
$jti = $payload['jti'] ?? '';
if (!empty($jti)) {
// 将 Token 加入黑名单
$this->blacklistStorage->add($jti, $this->config['ttl']['access'] ?? 7200);
// 获取并删除 refresh_token
$tokenInfo = $this->tokenStorage->get($jti);
if ($tokenInfo && !empty($tokenInfo['refresh_jti'])) {
$this->tokenStorage->delete($tokenInfo['refresh_jti']);
}
// 删除 Token 信息
$this->tokenStorage->delete($jti);
}
return true;
} catch (\Throwable $e) {
return false;
}
}
/**
* 退出所有设备
*
* @param string $id
* @param string|null $clientType
*
* @return int 退出设备数
*/
/**
* 退出所有设备
*
* @param string $id
* @param string|null $clientType
*
* @return int
*/
public function logoutAll(string $id, ?string $clientType = null): int
{
// 获取用户的所有 token 信息
$tokens = $this->tokenStorage->getUserTokens($id, $clientType);
if (empty($tokens)) {
return 0;
}
$count = 0;
// 将所有 token 加入黑名单并删除
foreach ($tokens as $jti => $info) {
// 将 access_token 加入黑名单(使用 refresh_ttl 确保足够长)
$blacklistTtl = $this->config['ttl']['refresh'] ?? 604800;
$this->blacklistStorage->add($jti, $blacklistTtl);
// 将 refresh_token 也加入黑名单
if (!empty($info['refresh_jti'])) {
$this->blacklistStorage->add($info['refresh_jti'], $blacklistTtl);
// 删除 refresh_token 存储
$this->tokenStorage->delete($info['refresh_jti']);
}
// 删除 access_token 存储
$this->tokenStorage->delete($jti);
$count++;
}
// 清理用户索引(deleteByUser 只会清理索引,不会删除已删除的 token)
$this->tokenStorage->deleteByUser($id, $clientType);
return $count;
}
/**
* 退出其他设备(保留当前)
*
* @param string|null $id
* @param string|null $clientType
* @param string|null $exceptJti 保留的 JTI
*
* @return int 退出设备数
*/
public function logoutOthers(?string $id = null, ?string $clientType = null, ?string $exceptJti = null): int
{
$id = $id ?? $this->id();
if ($id === null) {
return 0;
}
$exceptJti = $exceptJti ?? ($this->getPayloadFromRequest()['jti'] ?? null);
// 获取用户的所有 token 信息
$tokens = $this->tokenStorage->getUserTokens($id, $clientType);
if (empty($tokens)) {
return 0;
}
$blacklistTtl = $this->config['ttl']['refresh'] ?? 604800;
$count = 0;
// 遍历并处理需要删除的 token
foreach ($tokens as $jti => $info) {
// 跳过保留的 JTI
if ($exceptJti !== null && $jti === $exceptJti) {
continue;
}
// 将 access_token 加入黑名单
$this->blacklistStorage->add($jti, $blacklistTtl);
// 将 refresh_token 加入黑名单并删除
if (!empty($info['refresh_jti'])) {
$this->blacklistStorage->add($info['refresh_jti'], $blacklistTtl);
$this->tokenStorage->delete($info['refresh_jti']);
}
// 删除 access_token 存储
$this->tokenStorage->delete($jti);
$count++;
}
// 清理用户索引
$this->tokenStorage->deleteByUser($id, $clientType, $exceptJti);
return $count;
}
/**
* 获取用户的所有会话列表
*
* @param string $id
* @param string|null $clientType
*
* @return array
*/
public function getSessions(string $id, ?string $clientType = null): array
{
// 使用 tokenStorage 获取用户的所有 token 信息
$tokens = $this->tokenStorage->getUserTokens($id, $clientType);
if (empty($tokens)) {
return [];
}
$sessions = [];
foreach ($tokens as $jti => $info) {
// 获取完整的 token 数据
$tokenData = $this->tokenStorage->get($jti);
if ($tokenData !== null) {
$sessions[] = [
'jti' => $jti,
'client_type' => $info['client_type'] ?? $tokenData['client_type'] ?? '',
'created_at' => $info['created_at'] ?? $tokenData['created_at'] ?? 0,
'extra' => $tokenData['extra'] ?? [],
];
}
}
return $sessions;
}
/**
* 获取所有在线会话列表
*
* @param string|null $clientType
*
* @return array
*/
public function getAllSessions(?string $clientType = null): array
{
$tokens = $this->tokenStorage->getAllTokens($clientType);
return array_values($tokens);
}
/**
* 刷新当前 Token 的最后活跃时间
*
* @return bool
*/
public function touchCurrentToken(): bool
{
$payload = $this->getPayloadFromRequest();
$jti = $payload['jti'] ?? '';
if (empty($jti)) {
return false;
}
return $this->tokenStorage->touch($jti);
}
/**
* 通过 JTI 踢出指定会话
*
* @param string $jti
*
* @return bool
*/
public function kickoutByJti(string $jti): bool
{
// 获取 token 信息
$tokenData = $this->tokenStorage->get($jti);
if ($tokenData === null) {
return $this->tokenStorage->markOffline($jti);
}
// 将 access_token 加入黑名单
$blacklistTtl = $this->config['ttl']['refresh'] ?? 604800;
$this->blacklistStorage->add($jti, $blacklistTtl);
// 将 refresh_token 也加入黑名单
$refreshJti = $tokenData['refresh_jti'] ?? '';
if (!empty($refreshJti)) {
$this->blacklistStorage->add($refreshJti, $blacklistTtl);
}
// 标记会话离线,保留监控列表记录
return $this->tokenStorage->markOffline($jti);
}
/**
* 获取登录设备数
*
* @param string $id
* @param string|null $clientType
*
* @return int
*/
public function deviceCount(string $id, ?string $clientType = null): int
{
return $this->tokenStorage->countByUser($id, $clientType);
}
/**
* 检查用户是否在黑名单
*
* @param string $id
*
* @return bool
*/
public function isUserBlacklisted(string $id): bool
{
return $this->blacklistStorage->hasUser($id);
}
/**
* 将用户加入黑名单
*
* @param string $id
* @param int|null $ttl 有效期(秒)
*
* @return bool
*/
public function blacklistUser(string $id, ?int $ttl = null): bool
{
$ttl = $ttl ?? ($this->config['ttl']['refresh'] ?? 604800);
// 退出用户所有设备
$this->logoutAll($id);
// 加入黑名单
return $this->blacklistStorage->addUser($id, $ttl);
}
/**
* 撤销 Token
*
* @param string $token
*
* @return bool
*/
public function revoke(string $token): bool
{
try {
$payload = $this->parseToken($token);
$jti = $payload['jti'] ?? '';
if (!empty($jti)) {
$this->blacklistStorage->add($jti, $this->config['ttl']['access'] ?? 7200);
$this->tokenStorage->delete($jti);
return true;
}
return false;
} catch (\Throwable $e) {
return false;
}
}
/**
* 获取 Token 状态
*
* @param string $token
*
* @return TokenStatus
*/
public function getTokenStatus(string $token): TokenStatus
{
try {
$payload = $this->parseToken($token);
$jti = $payload['jti'] ?? '';
// 检查是否在黑名单
if (!empty($jti) && $this->blacklistStorage->has($jti)) {
return TokenStatus::REVOKED;
}
// 检查是否过期
$exp = $payload['exp'] ?? 0;
if ($exp > 0 && $exp < time()) {
return TokenStatus::EXPIRED;
}
return TokenStatus::ACTIVE;
} catch (ExpiredException $e) {
return TokenStatus::EXPIRED;
} catch (\Throwable $e) {
return TokenStatus::REVOKED;
}
}
// ==================== 私有方法 ====================
/**
* 处理登录模式
*/
protected function handleLoginMode(string $id, string $clientType, string $loginMode): void
{
switch ($loginMode) {
case LoginMode::SINGLE->value:
// 单端登录:踢掉所有现有设备
$this->logoutAll($id);
break;
case LoginMode::CLIENT->value:
// 客户端模式:踢掉同类型设备的登录
$this->logoutAll($id, $clientType);
break;
case LoginMode::MULTI->value:
// 多端登录:检查最大设备数
$maxDevices = $this->config['security']['max_devices'] ?? 0;
if ($maxDevices > 0) {
$count = $this->tokenStorage->countByUser($id);
if ($count >= $maxDevices) {
// 删除最早的设备
$this->tokenStorage->deleteOldest($id);
}
}
break;
}
}
/**
* 通过 refresh_jti 反查 access token 会话
*/
protected function findAccessTokenByRefreshJti(string $id, string $clientType, string $refreshJti): array
{
$tokens = $this->tokenStorage->getUserTokens($id, $clientType);
foreach ($tokens as $accessJti => $info) {
if (($info['refresh_jti'] ?? '') !== $refreshJti) {
continue;
}
$tokenInfo = $this->tokenStorage->get($accessJti);
if ($tokenInfo !== null) {
return [$accessJti, $tokenInfo];
}
}
return ['', null];
}
/**
* 构建 Token 载荷
*/
protected function buildPayload(
string $id,
string $client,
string $jti,
TokenType $type,
array $extra = []
): array
{
$now = time();
$ttl = match ($type) {
TokenType::ACCESS => $this->config['ttl']['access'] ?? 7200,
TokenType::REFRESH => $this->config['ttl']['refresh'] ?? 604800,
};
$payload = [
'iss' => $this->config['issuer'] ?? 'webman',
'id' => $id,
'client' => $client,
'jti' => $jti,
'type' => $type->value,
'iat' => $now,
'exp' => $now + $ttl,
];
if (!empty($extra)) {
$payload['extra'] = $extra;
}
return $payload;
}
/**
* 生成 JWT Token
*/
protected function makeToken(array $payload): string
{
return JWT::encode($payload, $this->secret, $this->algo);
}
/**
* 解析 Token
*/
protected function parseToken(string $token): array
{
// 处理带有前缀的 token
if (str_starts_with($token, 'Bearer ')) {
$token = substr($token, 7);
}
return (array)JWT::decode($token, new Key($this->secret, $this->algo));
}
/**
* 生成 JTI
*/
protected function generateJti(): string
{
return bin2hex(random_bytes(16));
}
/**
* 从请求中获取 Access Token
*/
protected function getAccessTokenFromRequest(): ?string
{
$auth = request()->header('Authorization', '');
if (str_starts_with($auth, 'Bearer ')) {
return substr($auth, 7);
}
return request()->input('token');
}
/**
* 从请求中获取 Refresh Token
*/
protected function getRefreshTokenFromRequest(): ?string
{
// 先从 Authorization 头中获取
$tokenName = config('core.jwt.app.token_name', 'Authorization');
$authorization = request()->header($tokenName);
if (!empty($authorization)) {
if (str_starts_with($authorization, 'Bearer ')) {
$token = substr($authorization, 7);
if (!empty($token)) {
return $token;
}
}
}
// 再从请求输入中获取
return request()->input('refresh_token');
}
/**
* 从请求中获取 Token 载荷
*/
public function getPayloadFromRequest(): array
{
if ($this->payloadCache !== null) {
return $this->payloadCache;
}
try {
$token = $this->getAccessTokenFromRequest();
if (empty($token)) {
return [];
}
$this->payloadCache = $this->parseToken($token);
return $this->payloadCache;
} catch (\Throwable $e) {
return [];
}
}
}
+225
View File
@@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
namespace core\jwt;
use DateTimeImmutable;
use JsonSerializable;
/**
* Token 数据对象
*/
readonly class Token implements JsonSerializable
{
/**
* @param string $accessToken 访问令牌
* @param string $refreshToken 刷新令牌
* @param int $expiresIn 有效期(秒)
* @param string $tokenType 令牌类型
* @param string $jti 令牌唯一标识
* @param string $id 用户ID(雪花ID)
* @param string $clientType 客户端类型
* @param DateTimeImmutable $issuedAt 签发时间
* @param DateTimeImmutable $expiresAt 过期时间
* @param array $payload 载荷数据
*/
public function __construct(
public string $accessToken,
public string $refreshToken,
public int $expiresIn,
public string $tokenType = 'Bearer',
public string $jti = '',
public string $id = '',
public string $clientType = '',
public DateTimeImmutable $issuedAt = new DateTimeImmutable(),
public DateTimeImmutable $expiresAt = new DateTimeImmutable(),
public array $payload = [],
) {}
/**
* 创建 Token 实例
*
* @param string $accessToken 访问令牌
* @param string $refreshToken 刷新令牌
* @param int $expiresIn 有效期(秒)
* @param array $payload 完整载荷
* @return static
*/
public static function create(
string $accessToken,
string $refreshToken,
int $expiresIn,
array $payload = []
): static {
$now = new DateTimeImmutable();
$expiresAt = (new DateTimeImmutable())->setTimestamp($now->getTimestamp() + $expiresIn);
return new static(
accessToken: $accessToken,
refreshToken: $refreshToken,
expiresIn: $expiresIn,
jti: $payload['jti'] ?? '',
id: (string)($payload['id'] ?? ''),
clientType: $payload['client'] ?? '',
issuedAt: $now,
expiresAt: $expiresAt,
payload: $payload,
);
}
/**
* 从请求中解析 Token
*
* 支持多种格式:
* 1. Header: Authorization: Bearer <token>
* 2. Header: Authorization: <token>
* 3. 参数: access_token=<token>
* 4. 参数: token=<token>
*
* @return string|null
*/
public static function fromRequest(): ?string
{
// 1. Header Authorization Bearer
$auth = request()->header('Authorization', '');
if (!empty($auth)) {
if (str_starts_with($auth, 'Bearer ')) {
return substr($auth, 7);
}
// 直接是 token
return $auth;
}
// 2. 参数 access_token
$token = request()->input('access_token');
if (!empty($token)) {
return $token;
}
// 3. 参数 token
$token = request()->input('token');
if (!empty($token)) {
return $token;
}
return null;
}
/**
* 解析 Token 字符串
*
* @param string $tokenString JWT token 字符串
* @param int $expiresIn 有效期(秒)
* @param array $payload 载荷数据
* @return static
*/
public static function parse(string $tokenString, int $expiresIn, array $payload = []): static
{
return static::create($tokenString, '', $expiresIn, $payload);
}
/**
* 获取访问令牌
*/
public function getAccessToken(): string
{
return $this->accessToken;
}
/**
* 获取刷新令牌
*/
public function getRefreshToken(): string
{
return $this->refreshToken;
}
/**
* 获取用户ID(雪花ID)
*/
public function getId(): string
{
return $this->id;
}
/**
* 获取客户端类型
*/
public function getClientType(): string
{
return $this->clientType;
}
/**
* 获取令牌唯一标识
*/
public function getJti(): string
{
return $this->jti;
}
/**
* 获取过期时间戳
*/
public function getExpiresAt(): int
{
return $this->expiresAt->getTimestamp();
}
/**
* 检查是否已过期
*/
public function isExpired(): bool
{
return time() >= $this->expiresAt->getTimestamp();
}
/**
* 获取剩余有效期(秒)
*/
public function getRemainingTime(): int
{
$remaining = $this->expiresAt->getTimestamp() - time();
return max(0, $remaining);
}
/**
* 转换为数组
*/
public function toArray(): array
{
return [
'token_type' => $this->tokenType,
'access_token' => $this->accessToken,
'refresh_token' => $this->refreshToken,
'expires_in' => $this->expiresIn,
'expires_at' => $this->expiresAt->format('Y-m-d H:i:s'),
'jti' => $this->jti,
'id' => $this->id,
'client_type' => $this->clientType,
'issued_at' => $this->issuedAt->format('Y-m-d H:i:s'),
];
}
/**
* 转换为前端格式
*/
public function toFrontend(): array
{
return [
'token' => $this->accessToken,
'refresh_token' => $this->refreshToken,
'expires_in' => $this->expiresIn,
'expires_at' => $this->expiresAt->getTimestamp(),
'token_type' => $this->tokenType,
];
}
/**
* JSON 序列化
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
/**
* JWT 配置文件
*/
return [
'enable' => true,
'token_name' => 'Authorization',
// ========== 密钥配置 ==========
// 必须修改为您的实际密钥,建议使用 openssl_random_pseudo_bytes(32) 生成
'secret' => env('JWT_SECRET', 'your-secret-key-change-in-production'),
// 加密算法
'algo' => env('JWT_ALGO', 'HS256'),
// ========== Token 有效期配置 (秒) ==========
'ttl' => [
// 访问令牌有效期
'access' => env('JWT_ACCESS_TTL', 3600),
// 刷新令牌有效期,默认 7 天
'refresh' => env('JWT_REFRESH_TTL', 604800),
],
// ========== 登录模式 ==========
// single: 单端登录(同一用户只能在一个设备登录)
// client: 客户端模式(同一客户端只能在一个设备,不同客户端可同时)
// multi: 多端登录(同一用户可在多个设备同时登录)
'login_mode' => env('JWT_LOGIN_MODE', 'multi'),
// ========== 客户端类型配置 ==========
// 预定义类型: admin, api, web, mobile, mini
// 可在此处添加自定义类型
'client_types' => [
// 示例:添加自定义客户端类型
// 'pc' => 'pc',
// 'ipad' => 'ipad',
],
// ========== 存储配置 ==========
'storage' => [
// Token 存储驱动
'token' => env('JWT_STORAGE_TOKEN', 'redis'),
// 黑名单存储驱动
'blacklist' => env('JWT_STORAGE_BLACKLIST', 'redis'),
// Redis 前缀
'redis_prefix' => env('JWT_REDIS_PREFIX', 'jwt:'),
],
// ========== 安全配置 ==========
'security' => [
// 是否启用刷新令牌
'refresh_enabled' => true,
// 刷新令牌宽限期(秒),允许在令牌即将过期时提前刷新
'refresh_grace_period' => env('JWT_REFRESH_GRACE_PERIOD', 3600),
// 最大同时登录设备数(0 表示不限制)
'max_devices' => env('JWT_MAX_DEVICES', 0),
// 是否强制刷新(每次请求都返回新的 access_token)
'force_refresh' => false,
// 滑动过期:每次使用自动延长 access_token 有效期
'sliding_expiry' => false,
],
// ========== Redis 连接配置 ==========
'redis' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DATABASE', 0),
'timeout' => env('REDIS_TIMEOUT', 0),
],
// ========== 预留字段 ==========
// 可在生成 Token 时传入自定义数据
'reserved_claims' => [
// 'iss', // 签发者
// 'aud', // 受众
],
];
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace core\jwt\enum;
/**
* 客户端类型枚举
*/
enum ClientType: string
{
case ADMIN = 'admin';
case API = 'api';
case WEB = 'web';
case MOBILE = 'mobile';
case MINI = 'mini';
/**
* 检查客户端类型是否有效
*/
public static function isValid(string $type): bool
{
foreach (self::cases() as $case) {
if ($case->value === $type) {
return true;
}
}
// 合并配置文件中定义的额外类型
$config = config('jwt.app.client_types', []);
return in_array($type, $config, true);
}
/**
* 获取所有有效的客户端类型值
*
* @return string[]
*/
public static function values(): array
{
$values = array_map(fn(self $case) => $case->value, self::cases());
// 合并配置文件中定义的额外类型
$config = config('jwt.app.client_types', []);
return array_merge($values, $config);
}
/**
* 获取枚举值
*/
public function value(): string
{
return $this->value;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace core\jwt\enum;
/**
* 登录模式枚举
*/
enum LoginMode: string
{
/**
* 单端登录模式 - 同一用户只能在一个设备登录,新登录会踢掉旧登录
*/
case SINGLE = 'single';
/**
* 客户端模式 - 同一客户端只能在一个设备登录,不同客户端可同时登录
*/
case CLIENT = 'client';
/**
* 多端模式 - 同一用户可在多个设备同时登录
*/
case MULTI = 'multi';
/**
* 获取枚举值
*/
public function value(): string
{
return $this->value;
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace core\jwt\enum;
/**
* Token 状态枚举
*/
enum TokenStatus: string
{
case ACTIVE = 'active';
case REFRESHED = 'refreshed';
case REVOKED = 'revoked';
case EXPIRED = 'expired';
/**
* 获取枚举值
*/
public function value(): string
{
return $this->value;
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace core\jwt\enum;
/**
* Token 类型枚举
*/
enum TokenType: string
{
case ACCESS = 'access';
case REFRESH = 'refresh';
/**
* 获取枚举值
*/
public function value(): string
{
return $this->value;
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace core\jwt\ex;
/**
* JWT V2 模块基础异常
*/
class JwtException extends \Exception
{
public function __construct(string $message = '', int $code = -1, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace core\jwt\ex;
/**
* 登录模式异常
*/
class JwtLoginModeException extends JwtException
{
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace core\jwt\ex;
/**
* 刷新令牌已过期异常
*/
class JwtRefreshTokenExpiredException extends JwtException
{
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace core\jwt\ex;
/**
* Token 已过期异常
*/
class JwtTokenExpiredException extends JwtException
{
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace core\jwt\ex;
/**
* Token 在黑名单中异常
*/
class JwtTokenInBlacklistException extends JwtException
{
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace core\jwt\ex;
/**
* Token 无效异常
*/
class JwtTokenInvalidException extends JwtException
{
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace core\jwt\ex;
/**
* Token 已被替换异常
*/
class JwtTokenReplacedException extends JwtException
{
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace core\jwt\interfaces;
/**
* 黑名单存储接口
*
* 可实现此接口以支持不同的存储后端(Redis、MySQL、Memcached 等)
*/
interface BlacklistStorageInterface
{
/**
* 添加 Token 到黑名单
*
* @param string $jti Token 唯一标识
* @param int $ttl 有效期(秒)
* @return bool
*/
public function add(string $jti, int $ttl): bool;
/**
* 检查 Token 是否在黑名单
*
* @param string $jti
* @return bool
*/
public function has(string $jti): bool;
/**
* 移除 Token 从黑名单
*
* @param string $jti
* @return bool
*/
public function remove(string $jti): bool;
/**
* 添加用户到黑名单
*
* @param string $id 用户ID(雪花ID)
* @param int $ttl 有效期(秒)
* @return bool
*/
public function addUser(string $id, int $ttl): bool;
/**
* 检查用户是否在黑名单
*
* @param string $id 用户ID(雪花ID)
* @return bool
*/
public function hasUser(string $id): bool;
/**
* 移除用户从黑名单
*
* @param string $id 用户ID(雪花ID)
* @return bool
*/
public function removeUser(string $id): bool;
/**
* 获取黑名单剩余 TTL
*
* @param string $jti
* @return int -1 表示不存在,-2 表示已过期
*/
public function getTtl(string $jti): int;
}
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace core\jwt\interfaces;
/**
* Token 存储接口
*
* 可实现此接口以支持不同的存储后端(Redis、MySQL、Memcached 等)
*/
interface TokenStorageInterface
{
/**
* 保存 Token 信息
*
* @param string $jti Token 唯一标识
* @param array $data Token 数据
* @param int $ttl 有效期(秒)
* @return bool
*/
public function save(string $jti, array $data, int $ttl): bool;
/**
* 获取 Token 信息
*
* @param string $jti
* @return array|null
*/
public function get(string $jti): ?array;
/**
* 删除 Token 信息
*
* @param string $jti
* @return bool
*/
public function delete(string $jti): bool;
/**
* 验证 Token 是否存在
*
* @param string $jti
* @return bool
*/
public function exists(string $jti): bool;
/**
* 根据用户删除所有 Token
*
* @param string $id 用户ID(雪花ID)
* @param string|null $clientType
* @param string|null $exceptJti 保留的 JTI
* @return int 删除数量
*/
public function deleteByUser(string $id, ?string $clientType = null, ?string $exceptJti = null): int;
/**
* 删除最早的 Token
*
* @param string $id 用户ID(雪花ID)
* @return bool
*/
public function deleteOldest(string $id): bool;
/**
* 获取用户的所有 Token 数量
*
* @param string $id 用户ID(雪花ID)
* @param string|null $clientType
* @return int
*/
public function countByUser(string $id, ?string $clientType = null): int;
/**
* 获取用户的所有 Token
*
* @param string $id 用户ID(雪花ID)
* @return array
*/
public function getByUser(string $id): array;
/**
* 获取用户的所有 Token 信息(包含索引信息)
*
* @param string $id 用户ID(雪花ID)
* @param string|null $clientType 客户端类型
* @return array
*/
public function getUserTokens(string $id, ?string $clientType = null): array;
/**
* 获取所有在线 Token 信息
*
* @param string|null $clientType 客户端类型
* @return array
*/
public function getAllTokens(?string $clientType = null): array;
/**
* 更新 Token 最后活跃时间
*
* @param string $jti Token 唯一标识
* @param int|null $timestamp 活跃时间戳
* @return bool
*/
public function touch(string $jti, ?int $timestamp = null): bool;
/**
* 标记 Token 会话离线
*
* @param string $jti Token 唯一标识
* @param int|null $timestamp 离线时间戳
* @return bool
*/
public function markOffline(string $jti, ?int $timestamp = null): bool;
}
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace core\jwt\storage;
use core\jwt\interfaces\BlacklistStorageInterface;
use support\Redis;
/**
* Redis 黑名单存储
*/
class RedisBlacklistStorage implements BlacklistStorageInterface
{
/**
* @var string 前缀
*/
protected string $prefix;
/**
* @var array 配置
*/
protected array $config;
/**
* @var string 用户黑名单 Key 后缀
*/
protected const USER_BLACKLIST_SUFFIX = 'user:blacklist:';
/**
* @var string Token 黑名单 Key 后缀
*/
protected const TOKEN_BLACKLIST_SUFFIX = 'blacklist:';
public function __construct(array $config = [])
{
$this->config = $config;
$this->prefix = $config['storage']['redis_prefix'] ?? 'jwt2:';
}
/**
* 添加 Token 到黑名单
*/
public function add(string $jti, int $ttl): bool
{
$key = $this->prefix . self::TOKEN_BLACKLIST_SUFFIX . $jti;
Redis::setex($key, $ttl, '1');
return true;
}
/**
* 检查 Token 是否在黑名单
*/
public function has(string $jti): bool
{
$key = $this->prefix . self::TOKEN_BLACKLIST_SUFFIX . $jti;
return (bool) Redis::exists($key);
}
/**
* 移除 Token 从黑名单
*/
public function remove(string $jti): bool
{
$key = $this->prefix . self::TOKEN_BLACKLIST_SUFFIX . $jti;
Redis::del($key);
return true;
}
/**
* 添加用户到黑名单
*/
public function addUser(string $id, int $ttl): bool
{
$key = $this->prefix . self::USER_BLACKLIST_SUFFIX . $id;
Redis::setex($key, $ttl, '1');
return true;
}
/**
* 检查用户是否在黑名单
*/
public function hasUser(string $id): bool
{
$key = $this->prefix . self::USER_BLACKLIST_SUFFIX . $id;
return (bool) Redis::exists($key);
}
/**
* 移除用户从黑名单
*/
public function removeUser(string $id): bool
{
$key = $this->prefix . self::USER_BLACKLIST_SUFFIX . $id;
Redis::del($key);
return true;
}
/**
* 获取黑名单剩余 TTL
*/
public function getTtl(string $jti): int
{
$key = $this->prefix . self::TOKEN_BLACKLIST_SUFFIX . $jti;
return (int) Redis::ttl($key);
}
}
@@ -0,0 +1,422 @@
<?php
declare(strict_types=1);
namespace core\jwt\storage;
use core\jwt\interfaces\TokenStorageInterface;
use support\Redis;
/**
* Redis Token 存储
*/
class RedisTokenStorage implements TokenStorageInterface
{
/**
* @var string 前缀
*/
protected string $prefix;
/**
* @var array 配置
*/
protected array $config;
/**
* @var string 用户索引 Key 前缀
*/
protected const USER_INDEX_PREFIX = 'user:';
/**
* @var string 全局在线 Token 索引 Key
*/
protected const ONLINE_INDEX_KEY = 'online';
public function __construct(array $config = [])
{
$this->config = $config;
$this->prefix = $config['storage']['redis_prefix'] ?? 'jwt2:';
}
/**
* 保存 Token 信息
*/
public function save(string $jti, array $data, int $ttl): bool
{
$key = $this->prefix . $jti;
$id = $data['id'] ?? '';
$clientType = $data['client_type'] ?? '';
$now = time();
$data['last_active_at'] = $data['last_active_at'] ?? $now;
// 保存 Token 数据
Redis::setex($key, $ttl, json_encode($data));
$onlineKey = $this->prefix . self::ONLINE_INDEX_KEY;
Redis::hset($onlineKey, $jti, json_encode([
'id' => $id,
'client_type' => $clientType,
'created_at' => $data['created_at'] ?? $now,
'last_active_at' => $data['last_active_at'],
'refresh_jti' => $data['refresh_jti'] ?? '',
'status' => $data['status'] ?? 'online',
'offline_at' => $data['offline_at'] ?? null,
'snapshot' => $data,
]));
Redis::expire($onlineKey, $this->config['ttl']['refresh'] ?? 604800);
// 添加用户索引(保存 refresh_jti 以便踢人时使用)
if (!empty($id)) {
$userKey = $this->prefix . self::USER_INDEX_PREFIX . $id;
Redis::hset($userKey, $jti, json_encode([
'id' => $id,
'client_type' => $clientType,
'created_at' => $data['created_at'] ?? $now,
'last_active_at' => $data['last_active_at'],
'refresh_jti' => $data['refresh_jti'] ?? '',
'status' => $data['status'] ?? 'online',
'offline_at' => $data['offline_at'] ?? null,
]));
// 设置用户索引过期时间(比 refresh_token 长一点)
Redis::expire($userKey, $this->config['ttl']['refresh'] ?? 604800);
}
return true;
}
/**
* 获取 Token 信息
*/
public function get(string $jti): ?array
{
$key = $this->prefix . $jti;
$data = Redis::get($key);
if ($data === null || $data === false) {
return null;
}
return json_decode((string) $data, true);
}
/**
* 删除 Token 信息
*/
public function delete(string $jti): bool
{
$key = $this->prefix . $jti;
// 获取数据以便清理用户索引
$data = $this->get($jti);
Redis::del($key);
$indexRaw = Redis::hget($this->prefix . self::ONLINE_INDEX_KEY, $jti);
$indexData = $indexRaw ? (json_decode((string)$indexRaw, true) ?: []) : [];
if (($indexData['status'] ?? '') === 'offline') {
return true;
}
Redis::hdel($this->prefix . self::ONLINE_INDEX_KEY, $jti);
// 清理用户索引
if ($data && !empty($data['id'])) {
$userKey = $this->prefix . self::USER_INDEX_PREFIX . $data['id'];
Redis::hdel($userKey, $jti);
}
return true;
}
/**
* 验证 Token 是否存在
*/
public function exists(string $jti): bool
{
$key = $this->prefix . $jti;
return (bool) Redis::exists($key);
}
/**
* 根据用户删除所有 Token
*/
public function deleteByUser(string $id, ?string $clientType = null, ?string $exceptJti = null): int
{
$userKey = $this->prefix . self::USER_INDEX_PREFIX . $id;
$jtis = Redis::hgetall($userKey);
if (empty($jtis)) {
return 0;
}
$count = 0;
foreach ($jtis as $jtiKey => $info) {
// 跳过保留的 JTI
if ($exceptJti !== null && $jtiKey === $exceptJti) {
continue;
}
// 如果指定了客户端类型,只删除匹配的
if ($clientType !== null) {
$infoData = json_decode((string) $info, true);
if (($infoData['client_type'] ?? '') !== $clientType) {
continue;
}
}
$infoData = json_decode((string) $info, true) ?: [];
if (($infoData['status'] ?? '') === 'offline') {
continue;
}
// 删除 Token
Redis::del($this->prefix . $jtiKey);
Redis::hdel($userKey, $jtiKey);
Redis::hdel($this->prefix . self::ONLINE_INDEX_KEY, $jtiKey);
$count++;
}
return $count;
}
/**
* 获取用户的所有 Token 信息
*/
public function getUserTokens(string $id, ?string $clientType = null): array
{
$userKey = $this->prefix . self::USER_INDEX_PREFIX . $id;
$jtis = Redis::hgetall($userKey);
$tokens = [];
foreach ($jtis as $jtiKey => $info) {
// 如果指定了客户端类型,只返回匹配的
if ($clientType !== null) {
$infoData = json_decode((string) $info, true);
if (($infoData['client_type'] ?? '') !== $clientType) {
continue;
}
}
$tokens[$jtiKey] = json_decode((string) $info, true);
}
return $tokens;
}
/**
* 删除最早的 Token
*/
public function deleteOldest(string $id): bool
{
$userKey = $this->prefix . self::USER_INDEX_PREFIX . $id;
$jtis = Redis::hgetall($userKey);
if (empty($jtis)) {
return false;
}
// 找出最早的
$oldestJti = null;
$oldestTime = PHP_INT_MAX;
foreach ($jtis as $jtiKey => $info) {
$infoData = json_decode((string) $info, true);
$createdAt = $infoData['created_at'] ?? 0;
if ($createdAt < $oldestTime) {
$oldestTime = $createdAt;
$oldestJti = $jtiKey;
}
}
if ($oldestJti !== null) {
return $this->delete($oldestJti);
}
return false;
}
/**
* 获取用户的所有 Token 数量
*/
public function countByUser(string $id, ?string $clientType = null): int
{
$userKey = $this->prefix . self::USER_INDEX_PREFIX . $id;
$jtis = Redis::hgetall($userKey);
if (empty($jtis)) {
return 0;
}
if ($clientType === null) {
return count($jtis);
}
$count = 0;
foreach ($jtis as $jtiKey => $info) {
$infoData = json_decode((string) $info, true);
if (($infoData['client_type'] ?? '') === $clientType) {
$count++;
}
}
return $count;
}
/**
* 获取用户的所有 Token
*/
public function getByUser(string $id): array
{
$userKey = $this->prefix . self::USER_INDEX_PREFIX . $id;
$jtis = Redis::hgetall($userKey);
$tokens = [];
foreach ($jtis as $jtiKey => $info) {
$tokenData = $this->get($jtiKey);
if ($tokenData !== null) {
$tokens[] = $tokenData;
} else {
$infoData = json_decode((string) $info, true) ?: [];
if (($infoData['status'] ?? '') === 'offline') {
continue;
}
// 清理失效的索引
Redis::hdel($userKey, $jtiKey);
Redis::hdel($this->prefix . self::ONLINE_INDEX_KEY, $jtiKey);
}
}
return $tokens;
}
/**
* 获取所有在线 Token 信息
*/
public function getAllTokens(?string $clientType = null): array
{
$onlineKey = $this->prefix . self::ONLINE_INDEX_KEY;
$jtis = Redis::hgetall($onlineKey);
if (empty($jtis)) {
return [];
}
$tokens = [];
foreach ($jtis as $jtiKey => $info) {
$indexData = json_decode((string) $info, true) ?: [];
if ($clientType !== null && ($indexData['client_type'] ?? '') !== $clientType) {
continue;
}
$storedTokenData = $this->get($jtiKey);
$tokenData = $storedTokenData ?? $indexData['snapshot'] ?? null;
if ($tokenData === null) {
$tokenData = $indexData;
}
$sessionStatus = $indexData['status'] ?? 'online';
if ($storedTokenData === null) {
$sessionStatus = 'offline';
}
$tokens[$jtiKey] = array_merge($tokenData, [
'jti' => $jtiKey,
'status' => $sessionStatus,
'offline_at' => $indexData['offline_at'] ?? ($sessionStatus === 'offline' ? ($indexData['last_active_at'] ?? time()) : null),
'last_active_at' => $tokenData['last_active_at'] ?? $indexData['last_active_at'] ?? $tokenData['created_at'] ?? time(),
]);
}
return $tokens;
}
/**
* 更新 Token 最后活跃时间
*/
public function touch(string $jti, ?int $timestamp = null): bool
{
$data = $this->get($jti);
if ($data === null) {
$indexRaw = Redis::hget($this->prefix . self::ONLINE_INDEX_KEY, $jti);
$indexData = $indexRaw ? (json_decode((string)$indexRaw, true) ?: []) : [];
if (($indexData['status'] ?? '') !== 'offline') {
Redis::hdel($this->prefix . self::ONLINE_INDEX_KEY, $jti);
}
return false;
}
$timestamp = $timestamp ?? time();
$data['last_active_at'] = $timestamp;
$ttl = Redis::ttl($this->prefix . $jti);
if ($ttl > 0) {
Redis::setex($this->prefix . $jti, $ttl, json_encode($data));
} else {
Redis::set($this->prefix . $jti, json_encode($data));
}
$indexData = [
'id' => $data['id'] ?? '',
'client_type' => $data['client_type'] ?? '',
'created_at' => $data['created_at'] ?? $timestamp,
'last_active_at' => $timestamp,
'refresh_jti' => $data['refresh_jti'] ?? '',
];
Redis::hset($this->prefix . self::ONLINE_INDEX_KEY, $jti, json_encode($indexData));
if (!empty($data['id'])) {
Redis::hset($this->prefix . self::USER_INDEX_PREFIX . $data['id'], $jti, json_encode($indexData));
}
return true;
}
public function markOffline(string $jti, ?int $timestamp = null): bool
{
$timestamp = $timestamp ?? time();
$data = $this->get($jti);
$onlineKey = $this->prefix . self::ONLINE_INDEX_KEY;
$indexRaw = Redis::hget($onlineKey, $jti);
$indexData = $indexRaw ? (json_decode((string)$indexRaw, true) ?: []) : [];
if ($data === null && empty($indexData)) {
return false;
}
$snapshot = $data ?? $indexData['snapshot'] ?? $indexData;
$snapshot['status'] = 'offline';
$snapshot['offline_at'] = $timestamp;
$snapshot['last_active_at'] = $snapshot['last_active_at'] ?? $indexData['last_active_at'] ?? $timestamp;
$newIndexData = [
'id' => $snapshot['id'] ?? $indexData['id'] ?? '',
'client_type' => $snapshot['client_type'] ?? $indexData['client_type'] ?? '',
'created_at' => $snapshot['created_at'] ?? $indexData['created_at'] ?? $timestamp,
'last_active_at' => $snapshot['last_active_at'],
'refresh_jti' => $snapshot['refresh_jti'] ?? $indexData['refresh_jti'] ?? '',
'status' => 'offline',
'offline_at' => $timestamp,
'snapshot' => $snapshot,
];
Redis::hset($onlineKey, $jti, json_encode($newIndexData));
Redis::expire($onlineKey, $this->config['ttl']['refresh'] ?? 604800);
if (!empty($newIndexData['id'])) {
$userKey = $this->prefix . self::USER_INDEX_PREFIX . $newIndexData['id'];
Redis::hset($userKey, $jti, json_encode([
'id' => $newIndexData['id'],
'client_type' => $newIndexData['client_type'],
'created_at' => $newIndexData['created_at'],
'last_active_at' => $newIndexData['last_active_at'],
'refresh_jti' => $newIndexData['refresh_jti'],
'status' => 'offline',
'offline_at' => $timestamp,
]));
Redis::expire($userKey, $this->config['ttl']['refresh'] ?? 604800);
}
Redis::del($this->prefix . $jti);
return true;
}
}