初始化本地仓库
This commit is contained in:
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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', // 受众
|
||||
],
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user