jwt注销需按守卫隔离黑名单,否则多守卫下会误判失效;修复方法是自定义黑名单类,在redis键或数据库表中加入guard标识,并在操作前显式指定守卫。

JWT 令牌本身是无状态的,Laravel 默认不维护“已注销”列表,所以所谓“JWT 注销”本质是让旧 token 失效 —— 常见做法是将其加入黑名单(Blacklist)。当多守卫(multi-guard)环境下(如 api 和 admin 共用同一套 JWT 配置但需独立管理 token),若未正确隔离黑名单作用域,就会出现“注销报错”或“跨守卫失效误判”,比如 admin 登出后 api token 也被误判无效。
问题根源:JWT 黑名单未按 guard 分区
Laravel JWT(如 tymon/jwt-auth 或 laravel/jwt-auth 的衍生包)默认黑名单存储(Redis/DB)不区分 guard。所有守卫共用同一张黑名单表或同一个 Redis key 前缀,导致:
- admin 守卫调用
invalidate()时,把 token 加入黑名单,但没标记所属 guard; - 后续 api 守卫验证同一 token,查黑名单时命中,直接拒绝,报
TokenInvalidException或TokenBlacklistedException; - 底层
Blacklist::has()和Blacklist::add()方法未注入当前 guard 上下文。
修复核心:为黑名单操作注入 guard 标识
以主流 jwt 包 tymon/jwt-auth(v1.0+)为例,在自定义黑名单驱动中加入 guard 前缀。假设你使用 Redis 存储黑名单:
在 config/jwt.php 中确认 blacklist 配置指向自定义驱动:
'blacklist' => [
'driver' => 'redis',
],
然后创建自定义黑名单类(如 app/Services/JWT/CustomBlacklist.php):
namespace App\Services\JWT;
use Tymon\JWTAuth\Blacklist as BaseBlacklist;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Support\Facades\Auth;
class CustomBlacklist extends BaseBlacklist
{
protected $redis;
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
protected function getStorageKey(): string
{
$guard = Auth::guard()->getName() ?: 'api';
return 'jwt:blacklist:'. $guard;
}
public function has($jti): bool
{
return (bool) $this->redis->connection()->zScore($this->getStorageKey(), $jti);
}
public function add($jti, $ttl = null): bool
{
$ttl = $ttl ?: config('jwt.blacklist_ttl', 60);
$this->redis->connection()->zAdd($this->getStorageKey(), time() + $ttl, $jti);
return true;
}
public function remove($jti): bool
{
return (bool) $this->redis->connection()->zRem($this->getStorageKey(), $jti);
}
}
再在 config/jwt.php 中绑定该类:
'providers' => [
'blacklist' => [
'redis' => \App\Services\JWT\CustomBlacklist::class,
],
],
守卫切换时确保 Auth::guard() 正确生效
多守卫场景下,Auth::guard('admin') 必须在 token 解析前就明确指定,否则 Auth::guard()->getName() 可能返回默认 guard(如 api),导致黑名单 key 错乱。建议在中间件或 logout 接口开头显式声明:
// admin 注销接口
public function logout(Request $request)
{
// 显式指定守卫,避免依赖全局默认
if (!Auth::guard('admin')->check()) {
throw new UnauthorizedException();
}
$token = $request->user('admin')->token();
JWTAuth::setToken($token)->invalidate(); // 此时 CustomBlacklist::getStorageKey() 返回 'jwt:blacklist:admin'
return response()->json(['message' => 'Admin logged out']);
}
数据库黑名单方案(可选)
若用 DB 黑名单,需在 blacklist 表中新增 guard 字段,并在 add()/has() 中加入 where 条件:
public function add($jti, $ttl = null): bool
{
$guard = Auth::guard()->getName() ?: 'api';
$expiresAt = now()->addMinutes($ttl ?: config('jwt.blacklist_ttl', 60));
return DB::table('blacklist')->insert([
'jti' => $jti,
'guard' => $guard,
'expires_at' => $expiresAt,
'created_at' => now(),
]);
}
public function has($jti): bool
{
$guard = Auth::guard()->getName() ?: 'api';
return DB::table('blacklist')
->where('jti', $jti)
->where('guard', $guard)
->where('expires_at', '>', now())
->exists();
}
不复杂但容易忽略 —— 关键是让黑名单逻辑感知当前守卫上下文,而不是靠“全局黑名单”一刀切。











