初始化本地仓库
This commit is contained in:
@@ -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