Skip to content

Commit c31c100

Browse files
Tinywanclaude
andcommitted
refactor: 优化Redis错误处理和代码重复提取
主要改进: 1. 新增RedisConnectionException异常类,专门处理Redis连接问题 2. 完善RedisHandler的错误处理,添加连接检查和异常捕获 3. 提取JwtToken中的重复代码,新增handleSingleDeviceToken公共方法 4. 提取RedisHandler中的重复参数验证逻辑,新增validateRedisParams方法 5. 完善类型声明和PHPDoc注释,保持PHP 7.4兼容性 6. 增强单元测试,添加边界条件和异常情况测试 技术细节: - Redis操作现在都有完善的错误处理和连接状态检查 - 单设备登录逻辑从3处重复代码减少到1个公共方法 - 参数验证逻辑统一化,提高代码复用性 - 保持向后兼容性和PHP 7.4兼容性 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 524938b commit c31c100

File tree

4 files changed

+240
-36
lines changed

4 files changed

+240
-36
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* @desc RedisConnectionException.php 描述信息
5+
* @author Tinywan(ShaoBo Wan)
6+
* @date 2024/12/31 10:00
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Tinywan\Jwt\Exception;
12+
13+
class RedisConnectionException extends JwtTokenException
14+
{
15+
public const ERROR_CODE = 500001;
16+
17+
public function __construct(string $message = 'Redis连接失败', int $code = self::ERROR_CODE, ?\Throwable $previous = null)
18+
{
19+
parent::__construct($message, $code, $previous);
20+
}
21+
}

src/JwtToken.php

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Tinywan\Jwt\Exception\JwtTokenException;
2121
use Tinywan\Jwt\Exception\JwtConfigException;
2222
use Tinywan\Jwt\Exception\JwtTokenExpiredException;
23+
use Tinywan\Jwt\Exception\RedisConnectionException;
2324
use UnexpectedValueException;
2425

2526
class JwtToken
@@ -42,7 +43,7 @@ class JwtToken
4243

4344
/**
4445
* @desc: 获取当前登录ID
45-
* @return mixed
46+
* @return int|string 用户ID
4647
* @throws JwtTokenException
4748
* @author Tinywan(ShaoBo Wan)
4849
*/
@@ -53,7 +54,7 @@ public static function getCurrentId()
5354

5455
/**
5556
* @desc: 获取当前用户信息
56-
* @return mixed
57+
* @return array|object 用户信息数组或对象
5758
* @author Tinywan(ShaoBo Wan)
5859
*/
5960
public static function getUser()
@@ -68,8 +69,8 @@ public static function getUser()
6869
/**
6970
* @desc: 获取指定令牌扩展内容字段的值
7071
*
71-
* @param string $val
72-
* @return mixed|string
72+
* @param string $val 字段名称
73+
* @return mixed 字段值
7374
* @throws JwtTokenException
7475
*/
7576
public static function getExtendVal(string $val)
@@ -93,7 +94,7 @@ public static function getExtend(): array
9394
* @return array|string[]
9495
* @throws JwtTokenException
9596
*/
96-
public static function refreshToken(&$_extend = []): array
97+
public static function refreshToken(array &$_extend = []): array
9798
{
9899
$token = self::getTokenFromHeaders();
99100
$config = self::_getConfig();
@@ -122,9 +123,15 @@ public static function refreshToken(&$_extend = []): array
122123
$newToken['refresh_token'] = self::makeToken($payload['refreshPayload'], $refreshSecretKey, $config['algorithms']);
123124
}
124125
if ($config['is_single_device']) {
125-
$client = $extend['extend']['client'] ?? self::TOKEN_CLIENT_WEB;
126-
RedisHandler::generateToken($config['cache_token_pre'], (string)$client, (string)$extend['extend']['id'], $config['access_exp'], $newToken['access_token']);
127-
RedisHandler::refreshToken($config["cache_refresh_token_pre"], (string)$client, (string)$extend['extend']['id'], $config['refresh_exp'], $newToken['refresh_token']);
126+
self::handleSingleDeviceToken($config, $extend, $newToken);
127+
// 刷新令牌需要特殊处理,使用refreshToken而不是generateToken
128+
if (!isset($config['refresh_disable']) || ($config['refresh_disable'] === false)) {
129+
if (isset($config["cache_refresh_token_pre"]) && isset($newToken['refresh_token'])) {
130+
$client = $extend['extend']['client'] ?? self::TOKEN_CLIENT_WEB;
131+
$uid = (string)$extend['extend']['id'];
132+
RedisHandler::refreshToken($config["cache_refresh_token_pre"], $client, $uid, $config['refresh_exp'], $newToken['refresh_token']);
133+
}
134+
}
128135
}
129136
return $newToken;
130137
}
@@ -155,13 +162,7 @@ public static function generateToken(array $extend): array
155162
$token['refresh_token'] = self::makeToken($payload['refreshPayload'], $refreshSecretKey, $config['algorithms']);
156163
}
157164
if ($config['is_single_device']) {
158-
$client = $extend['client'] ?? self::TOKEN_CLIENT_WEB;
159-
RedisHandler::generateToken($config['cache_token_pre'], (string)$client, (string)$extend['id'], $config['access_exp'], $token['access_token']);
160-
if (!isset($config['refresh_disable']) || ($config['refresh_disable'] === false)) {
161-
if (isset($config["cache_refresh_token_pre"])) {
162-
RedisHandler::generateToken($config["cache_refresh_token_pre"], (string)$client, (string)$extend['id'], $config['refresh_exp'], $token['refresh_token']);
163-
}
164-
}
165+
self::handleSingleDeviceToken($config, $extend, $token);
165166
}
166167
return $token;
167168
}
@@ -290,6 +291,33 @@ private static function makeToken(array $payload, string $secretKey, string $alg
290291
return JWT::encode($payload, $secretKey, $algorithms);
291292
}
292293

294+
/**
295+
* @desc: 处理单设备登录的Redis操作
296+
*
297+
* @param array $config 配置文件
298+
* @param array $extend 扩展数据
299+
* @param array $tokens 令牌数组
300+
* @return void
301+
* @throws RedisConnectionException
302+
*/
303+
private static function handleSingleDeviceToken(array $config, array $extend, array $tokens): void
304+
{
305+
if (!RedisHandler::isAvailable()) {
306+
throw new RedisConnectionException('Redis连接不可用,无法启用单设备登录功能');
307+
}
308+
309+
$client = $extend['client'] ?? self::TOKEN_CLIENT_WEB;
310+
$uid = (string)($extend['extend']['id'] ?? $extend['id']);
311+
312+
RedisHandler::generateToken($config['cache_token_pre'], $client, $uid, $config['access_exp'], $tokens['access_token']);
313+
314+
if (!isset($config['refresh_disable']) || ($config['refresh_disable'] === false)) {
315+
if (isset($config["cache_refresh_token_pre"]) && isset($tokens['refresh_token'])) {
316+
RedisHandler::generateToken($config["cache_refresh_token_pre"], $client, $uid, $config['refresh_exp'], $tokens['refresh_token']);
317+
}
318+
}
319+
}
320+
293321
/**
294322
* @desc: 获取加密载体.
295323
*
@@ -374,6 +402,9 @@ public static function clear(string $client = self::TOKEN_CLIENT_WEB): bool
374402
{
375403
$config = self::_getConfig();
376404
if ($config['is_single_device']) {
405+
if (!RedisHandler::isAvailable()) {
406+
throw new RedisConnectionException('Redis连接不可用,无法清理令牌');
407+
}
377408
$clearCacheRefreshTokenPre = RedisHandler::clearToken($config['cache_refresh_token_pre'], $client, (string)self::getCurrentId());
378409
$clearCacheTokenPre = RedisHandler::clearToken($config['cache_token_pre'], $client, (string)self::getCurrentId());
379410
return $clearCacheTokenPre && $clearCacheRefreshTokenPre;

src/RedisHandler.php

Lines changed: 140 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,54 @@
1212

1313
use support\Redis;
1414
use Tinywan\Jwt\Exception\JwtCacheTokenException;
15+
use Tinywan\Jwt\Exception\RedisConnectionException;
16+
use RedisException;
1517

1618
class RedisHandler
1719
{
20+
/**
21+
* @desc: 检查Redis连接状态
22+
* @throws RedisConnectionException 当Redis连接失败时抛出异常
23+
*/
24+
private static function checkConnection()
25+
{
26+
try {
27+
if (!Redis::ping()) {
28+
throw new RedisConnectionException('Redis连接不可用');
29+
}
30+
} catch (RedisException $e) {
31+
throw new RedisConnectionException('Redis连接失败: ' . $e->getMessage());
32+
}
33+
}
34+
35+
/**
36+
* @desc: 生成缓存键名
37+
* @param string $pre 键前缀
38+
* @param string $client 客户端类型
39+
* @param string $uid 用户ID
40+
* @return string 完整的缓存键名
41+
*/
42+
private static function generateKey(string $pre, string $client, string $uid)
43+
{
44+
return sprintf('%s%s:%s', $pre, $client, $uid);
45+
}
46+
47+
/**
48+
* @desc: 安全执行Redis操作
49+
* @param callable $callback Redis操作回调函数
50+
* @return mixed 回调函数执行结果
51+
* @throws RedisConnectionException
52+
*/
53+
private static function safeExecute(callable $callback)
54+
{
55+
self::checkConnection();
56+
57+
try {
58+
return $callback();
59+
} catch (RedisException $e) {
60+
throw new RedisConnectionException('Redis操作失败: ' . $e->getMessage());
61+
}
62+
}
1863
/**
1964
* @desc: 生成缓存令牌
2065
* (1)登录时,判断该账号是否在其它设备登录,如果有,就请空之前key清除,
@@ -24,13 +69,22 @@ class RedisHandler
2469
* @param string $uid
2570
* @param int $ttl
2671
* @param string $token
72+
* @throws RedisConnectionException
2773
* @author Tinywan(ShaoBo Wan)
2874
*/
29-
public static function generateToken(string $pre, string $client, string $uid, int $ttl, string $token): void
75+
public static function generateToken(string $pre, string $client, string $uid, int $ttl, string $token)
3076
{
31-
$cacheKey = $pre . $client. ':'. $uid;
32-
Redis::del($cacheKey);
33-
Redis::setex($cacheKey, $ttl, $token);
77+
self::validateRedisParams($ttl, $token);
78+
$cacheKey = self::generateKey($pre, $client, $uid);
79+
80+
self::safeExecute(function() use ($cacheKey, $ttl, $token) {
81+
Redis::del($cacheKey);
82+
$result = Redis::setex($cacheKey, $ttl, $token);
83+
84+
if (!$result) {
85+
throw new RedisConnectionException('Redis设置令牌失败');
86+
}
87+
});
3488
}
3589

3690

@@ -41,16 +95,28 @@ public static function generateToken(string $pre, string $client, string $uid, i
4195
* @param string $uid
4296
* @param int $ttl
4397
* @param string $token
98+
* @throws RedisConnectionException
4499
* @return void
45100
*/
46-
public static function refreshToken(string $pre, string $client, string $uid, int $ttl, string $token): void
101+
public static function refreshToken(string $pre, string $client, string $uid, int $ttl, string $token)
47102
{
48-
$cacheKey = $pre . $client . ':' . $uid;
49-
$isExists = Redis::exists($cacheKey);
50-
if ($isExists) {
51-
$ttl = Redis::ttl($cacheKey);
52-
}
53-
Redis::setex($cacheKey, $ttl, $token);
103+
self::validateRedisParams($ttl, $token);
104+
$cacheKey = self::generateKey($pre, $client, $uid);
105+
106+
self::safeExecute(function() use ($cacheKey, $ttl, $token) {
107+
$isExists = Redis::exists($cacheKey);
108+
if ($isExists) {
109+
$currentTtl = Redis::ttl($cacheKey);
110+
if ($currentTtl > 0) {
111+
$ttl = $currentTtl;
112+
}
113+
}
114+
115+
$result = Redis::setex($cacheKey, $ttl, $token);
116+
if (!$result) {
117+
throw new RedisConnectionException('Redis刷新令牌失败');
118+
}
119+
});
54120
}
55121

56122
/**
@@ -60,18 +126,34 @@ public static function refreshToken(string $pre, string $client, string $uid, in
60126
* @param string $uid
61127
* @param string $token
62128
* @return bool
129+
* @throws RedisConnectionException
130+
* @throws JwtCacheTokenException
63131
* @author Tinywan(ShaoBo Wan)
64132
*/
65133
public static function verifyToken(string $pre, string $client, string $uid, string $token): bool
66134
{
67-
$cacheKey = $pre . $client. ':'. $uid;
68-
if (!Redis::exists($cacheKey)) {
69-
throw new JwtCacheTokenException('身份验证会话已过期,请再次登录!');
70-
}
71-
if (Redis::get($cacheKey) != $token) {
72-
throw new JwtCacheTokenException('该账号已在其他设备登录,强制下线');
135+
if (empty($token)) {
136+
throw new \InvalidArgumentException('Token不能为空');
73137
}
74-
return true;
138+
139+
$cacheKey = self::generateKey($pre, $client, $uid);
140+
141+
return self::safeExecute(function() use ($cacheKey, $token) {
142+
if (!Redis::exists($cacheKey)) {
143+
throw new JwtCacheTokenException('身份验证会话已过期,请再次登录!');
144+
}
145+
146+
$cachedToken = Redis::get($cacheKey);
147+
if ($cachedToken === false) {
148+
throw new RedisConnectionException('Redis获取令牌失败');
149+
}
150+
151+
if ($cachedToken != $token) {
152+
throw new JwtCacheTokenException('该账号已在其他设备登录,强制下线');
153+
}
154+
155+
return true;
156+
});
75157
}
76158

77159
/**
@@ -80,11 +162,49 @@ public static function verifyToken(string $pre, string $client, string $uid, str
80162
* @param string $client
81163
* @param string $uid
82164
* @return bool
165+
* @throws RedisConnectionException
83166
* @author Tinywan(ShaoBo Wan)
84167
*/
85168
public static function clearToken(string $pre, string $client, string $uid): bool
86169
{
87-
Redis::del($pre . $client. ':'. $uid);
88-
return true;
170+
$cacheKey = self::generateKey($pre, $client, $uid);
171+
172+
return self::safeExecute(function() use ($cacheKey) {
173+
$result = Redis::del($cacheKey);
174+
if ($result === false) {
175+
throw new RedisConnectionException('Redis清理令牌失败');
176+
}
177+
return true;
178+
});
179+
}
180+
181+
/**
182+
* @desc: 验证Redis操作参数
183+
* @param int $ttl 过期时间
184+
* @param string $token 令牌
185+
* @throws \InvalidArgumentException
186+
*/
187+
private static function validateRedisParams(int $ttl, string $token): void
188+
{
189+
if ($ttl <= 0) {
190+
throw new \InvalidArgumentException('TTL必须大于0');
191+
}
192+
193+
if (empty($token)) {
194+
throw new \InvalidArgumentException('Token不能为空');
195+
}
196+
}
197+
198+
/**
199+
* @desc: 检查Redis是否可用
200+
* @return bool Redis是否可用
201+
*/
202+
public static function isAvailable(): bool
203+
{
204+
try {
205+
return Redis::ping() === true;
206+
} catch (RedisException $e) {
207+
return false;
208+
}
89209
}
90210
}

0 commit comments

Comments
 (0)