hyperf 3.x 不兼容 hyperf-ext/jwt,需手动实现 jwtguard、替换黑名单为 redis 实现、自定义中间件并统一异常处理,否则认证失败或黑名单失效。

Hyperf 默认不内置 JWT 支持,直接装 hyperf-ext/jwt 会因底层 TokenParser 和异常处理逻辑不兼容而报错——尤其是 TokenExpiredException 未被正确捕获、AuthManager 无法识别 JWT 驱动,导致认证始终失败。
为什么 hyperf-ext/jwt 不能直接 composer require 就用
该扩展基于 Hyperf 2.2 早期架构开发,与当前主流的 Hyperf 3.x(尤其是 hyperf/auth v3+)存在三处关键断层:
-
JWTGuard没有实现supportsRememberMe()方法,导致AuthManager初始化时抛BadMethodCallException -
黑名单存储默认依赖
hyperf/cache的FileDriver,但 JWT 扩展未适配 Hyperf 3 的CacheInterface新签名(如getMultiple()参数类型变更) -
JWTTokenParser返回的TokenInterface实例缺少getClaims()方法,导致中间件里$request->user()取不到用户数据
手动补全 JWTGuard 并注册为 auth 驱动
需新建 app/Auth/JWTGuard.php,继承 Hyperf\Auth\Guard\AbstractGuard,并显式实现缺失方法:
namespace App\Auth;
use Hyperf\Auth\Guard\AbstractGuard;
use Hyperf\Auth\Guard\GuardInterface;
use Hyperf\Context\ApplicationContext;
use Hyperf\Contract\ContainerInterface;
use Hyperf\Jwt\JWT;
use Hyperf\Jwt\Token;
use Psr\Http\Message\ServerRequestInterface;
class JWTGuard extends AbstractGuard implements GuardInterface
{
protected JWT $jwt;
public function __construct(ContainerInterface $container, string $providerName = null)
{
parent::__construct($container, $providerName);
$this->jwt = $container->get(JWT::class);
}
public function user(): ?object
{
$token = $this->getTokenFromRequest();
if (! $token) {
return null;
}
try {
$payload = $this->jwt->parse($token)->getPayload();
$id = $payload['uid'] ?? null; // 假设 payload 含 uid 字段
return $id ? $this->provider->retrieveById($id) : null;
} catch (\Exception $e) {
return null;
}
}
protected function getTokenFromRequest(): ?string
{
$request = ApplicationContext::getContainer()->get(ServerRequestInterface::class);
$authHeader = $request->getHeaderLine('Authorization');
if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
return $matches[1];
}
return null;
}
public function supportsRememberMe(): bool
{
return false; // JWT 本身无 remember me 概念
}
}
然后在 config/autoload/auth.php 中注册驱动:
'guards' => [
'jwt' => [
'driver' => \App\Auth\JWTGuard::class,
'provider' => 'users',
],
],
配置黑名单:用 Redis 替代默认 FileDriver
hyperf-ext/jwt 的黑名单机制依赖 BlacklistInterface,但其默认实现硬编码了 FileDriver。必须重写为 Redis 实现,并确保过期时间与 token 一致:
- 在
config/autoload/jwt.php中关闭自动黑名单(避免启动时报错):'blacklist_enabled' => false - 新建
app/Jwt/RedisBlacklist.php,实现BlacklistInterface,用$redis->setex($jti, $ttl, 1)存入 jti + 过期时间 - 在 token 签发后手动调用
$blacklist->add($token);登出时用$token->getJti()主动加入黑名单 - 注意:Redis key 命名建议加前缀如
jwt:blacklist:,避免与其他业务冲突
中间件里验证 token 并透传用户信息
不要依赖扩展自带的 JWTMiddleware(它没做异常兜底)。自己写一个轻量中间件:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$tokenStr = $this->getTokenFromRequest($request);
if (! $tokenStr) {
throw new AuthenticationException('Missing Authorization header');
}
try {
$token = $this->jwt->parse($tokenStr);
$user = $this->auth->guard('jwt')->user(); // 此时已可取到用户
$request = $request->withAttribute('user', $user);
} catch (\Firebase\JWT\ExpiredException $e) {
throw new AuthenticationException('Token expired');
} catch (\Exception $e) {
throw new AuthenticationException('Invalid token');
}
return $handler->handle($request);
}
关键点:
- 必须用
$this->auth->guard('jwt')触发自定义 Guard 的user()方法,而非直接调$this->jwt->parse() - 异常必须转为
AuthenticationException,否则会被 Hyperf 全局异常处理器吞掉,返回 500 而非 401 - 黑名单检查应放在
user()内部(比如在JWTGuard::user()里查$blacklist->has($jti)),而不是中间件里重复解析
真正麻烦的不是签发 token,而是让整个 auth 流程和 Hyperf 的生命周期对齐:Guard 必须能被容器正确实例化,异常必须落在 AuthenticationException 体系内,黑名单必须和 Redis TTL 精确同步——漏掉任意一环,都会表现为「明明 token 没过期却总 401」或「登出后 token 还能用」。











