Laravel JWT 令牌在身份验证 JWT 方法中刷新后无效
Laravel JWT tokens are Invalid after refresh them in a authentication JWT approach
编辑:
阅读有关错误的讨论:https://github.com/tymondesigns/jwt-auth/issues/83
我的原始问题:
我正在实施 jwt-auth 我的受保护资源,需要经过身份验证的用户使用以下代码:
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function() {
// Protected routes
});
当用户 'sign in' 在 API 上创建授权令牌,并在响应授权 header 时发送给调用该资源的客户端应用程序。因此,客户端应用程序在任何响应的 header 上拦截授权令牌时,使用此令牌值设置 variable/session/whatever,以便在下一个请求时再次发送到 API。
在 'login' 之后对受保护资源的第一次请求工作正常,但是下一个客户端应用程序请求 API 使用刷新的令牌,给出以下错误(API mount all json 格式的回复):
{
"error": "token_invalid"
}
刷新的令牌会发生什么?我的刷新令牌实现(设置为后中间件)是错误的?或者不需要手动刷新客户端应用程序请求附带的所有授权令牌?
更新:
我按照提议 here 更新了 jwt-auth RefreshToken 中间件,但是 token_invalid
仍然存在。
错误:
我想我发现发生了什么。注意在refresh方法中,old token加入黑名单缓存case enabled:
// Tymon\JWTAuth\JWTManager
public function refresh(Token $token)
{
$payload = $this->decode($token);
if ($this->blacklistEnabled) {
// invalidate old token
$this->blacklist->add($payload);
}
// return the new token
return $this->encode(
$this->payloadFactory->setRefreshFlow()->make([
'sub' => $payload['sub'],
'iat' => $payload['iat']
])
);
}
请注意,在添加到黑名单方法中,关键是来自旧令牌负载的 jti 参数:
// Tymon\JWTAuth\Blacklist
public function add(Payload $payload)
{
$exp = Utils::timestamp($payload['exp']);
// there is no need to add the token to the blacklist
// if the token has already expired
if ($exp->isPast()) {
return false;
}
// add a minute to abate potential overlap
$minutes = $exp->diffInMinutes(Utils::now()->subMinute());
$this->storage->add($payload['jti'], [], $minutes);
return true;
}
因此,调用has on blacklist方法时,旧token jti param与新token相同,所以新token在黑名单中:
// Tymon\JWTAuth\Blacklist
public function has(Payload $payload)
{
return $this->storage->has($payload['jti']);
}
如果您不需要黑名单功能,只需在 jwt.php 配置文件中将其设置为 false。但我不能说它是否暴露于某些安全漏洞。
阅读有关错误的讨论:https://github.com/tymondesigns/jwt-auth/issues/83
当我遇到这个问题时,我发现让我的项目正常工作的解决方案是在每个新请求上使用旧令牌中的数据生成一个新令牌。
我的解决方案对我有用,但不好、丑陋,如果您有很多异步请求并且您的 API(或业务核心)服务器速度慢,可能会产生更多问题。
目前还可以,但我会进一步调查这个问题,因为在 0.5.3 版本之后问题仍然存在。
例如:
请求 1(获取/登录):
Some guest data on token
请求 2(POST/登录响应):
User data merged with guest data on old token generating a new token
程序代码示例(你可以做得更好=)),你可以运行这个在routes.php out of routes,我说那是丑陋的哈哈:
// ----------------------------------------------------------------
// AUTH TOKEN WORK
// ----------------------------------------------------------------
$authToken = null;
$getAuthToken = function() use ($authToken, $Response) {
if($authToken === null) {
$authToken = JWTAuth::parseToken();
}
return $authToken;
};
$getLoggedUser = function() use ($getAuthToken) {
return $getAuthToken()->authenticate();
};
$getAuthPayload = function() use ($getAuthToken) {
try {
return $getAuthToken()->getPayload();
} catch (Exception $e) {
return [];
}
};
$mountAuthPayload = function($customPayload) use ($getLoggedUser, $getAuthPayload) {
$currentPayload = [];
try {
$currentAuthPayload = $getAuthPayload();
if(count($currentAuthPayload)) {
$currentPayload = $currentAuthPayload->toArray();
}
try {
if($user = $getLoggedUser()) {
$currentPayload['user'] = $user;
}
$currentPayload['isGuest'] = false;
} catch (Exception $e) {
// is guest
}
} catch(Exception $e) {
// Impossible to parse token
}
foreach ($customPayload as $key => $value) {
$currentPayload[$key] = $value;
}
return $currentPayload;
};
// ----------------------------------------------------------------
// AUTH TOKEN PAYLOAD
// ----------------------------------------------------------------
try {
$getLoggedUser();
$payload = ['isGuest' => false];
} catch (Exception $e) {
$payload = ['isGuest' => true];
}
try {
$payload = $mountAuthPayload($payload);
} catch (Exception $e) {
// Make nothing cause token is invalid, expired, etc., or not exists.
// Like a guest session. Create a token without user data.
}
一些路线(保存用户移动设备的简单示例):
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function () use ($getLoggedUser, $mountAuthPayload) {
Route::post('/session/device', function () use ($Response, $getLoggedUser, $mountAuthPayload) {
$Response = new \Illuminate\Http\Response();
$user = $getLoggedUser();
// code to save on database the user device from current "session"...
$payload = app('tymon.jwt.payload.factory')->make($mountAuthPayload(['device' => $user->device->last()->toArray()]));
$token = JWTAuth::encode($payload);
$Response->header('Authorization', 'Bearer ' . $token);
$responseContent = ['setted' => 'true'];
$Response->setContent($responseContent);
return $Response;
});
});
编辑:
阅读有关错误的讨论:https://github.com/tymondesigns/jwt-auth/issues/83
我的原始问题:
我正在实施 jwt-auth 我的受保护资源,需要经过身份验证的用户使用以下代码:
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function() {
// Protected routes
});
当用户 'sign in' 在 API 上创建授权令牌,并在响应授权 header 时发送给调用该资源的客户端应用程序。因此,客户端应用程序在任何响应的 header 上拦截授权令牌时,使用此令牌值设置 variable/session/whatever,以便在下一个请求时再次发送到 API。
在 'login' 之后对受保护资源的第一次请求工作正常,但是下一个客户端应用程序请求 API 使用刷新的令牌,给出以下错误(API mount all json 格式的回复):
{
"error": "token_invalid"
}
刷新的令牌会发生什么?我的刷新令牌实现(设置为后中间件)是错误的?或者不需要手动刷新客户端应用程序请求附带的所有授权令牌?
更新:
我按照提议 here 更新了 jwt-auth RefreshToken 中间件,但是 token_invalid
仍然存在。
错误:
我想我发现发生了什么。注意在refresh方法中,old token加入黑名单缓存case enabled:
// Tymon\JWTAuth\JWTManager
public function refresh(Token $token)
{
$payload = $this->decode($token);
if ($this->blacklistEnabled) {
// invalidate old token
$this->blacklist->add($payload);
}
// return the new token
return $this->encode(
$this->payloadFactory->setRefreshFlow()->make([
'sub' => $payload['sub'],
'iat' => $payload['iat']
])
);
}
请注意,在添加到黑名单方法中,关键是来自旧令牌负载的 jti 参数:
// Tymon\JWTAuth\Blacklist
public function add(Payload $payload)
{
$exp = Utils::timestamp($payload['exp']);
// there is no need to add the token to the blacklist
// if the token has already expired
if ($exp->isPast()) {
return false;
}
// add a minute to abate potential overlap
$minutes = $exp->diffInMinutes(Utils::now()->subMinute());
$this->storage->add($payload['jti'], [], $minutes);
return true;
}
因此,调用has on blacklist方法时,旧token jti param与新token相同,所以新token在黑名单中:
// Tymon\JWTAuth\Blacklist
public function has(Payload $payload)
{
return $this->storage->has($payload['jti']);
}
如果您不需要黑名单功能,只需在 jwt.php 配置文件中将其设置为 false。但我不能说它是否暴露于某些安全漏洞。
阅读有关错误的讨论:https://github.com/tymondesigns/jwt-auth/issues/83
当我遇到这个问题时,我发现让我的项目正常工作的解决方案是在每个新请求上使用旧令牌中的数据生成一个新令牌。
我的解决方案对我有用,但不好、丑陋,如果您有很多异步请求并且您的 API(或业务核心)服务器速度慢,可能会产生更多问题。
目前还可以,但我会进一步调查这个问题,因为在 0.5.3 版本之后问题仍然存在。
例如:
请求 1(获取/登录):
Some guest data on token
请求 2(POST/登录响应):
User data merged with guest data on old token generating a new token
程序代码示例(你可以做得更好=)),你可以运行这个在routes.php out of routes,我说那是丑陋的哈哈:
// ----------------------------------------------------------------
// AUTH TOKEN WORK
// ----------------------------------------------------------------
$authToken = null;
$getAuthToken = function() use ($authToken, $Response) {
if($authToken === null) {
$authToken = JWTAuth::parseToken();
}
return $authToken;
};
$getLoggedUser = function() use ($getAuthToken) {
return $getAuthToken()->authenticate();
};
$getAuthPayload = function() use ($getAuthToken) {
try {
return $getAuthToken()->getPayload();
} catch (Exception $e) {
return [];
}
};
$mountAuthPayload = function($customPayload) use ($getLoggedUser, $getAuthPayload) {
$currentPayload = [];
try {
$currentAuthPayload = $getAuthPayload();
if(count($currentAuthPayload)) {
$currentPayload = $currentAuthPayload->toArray();
}
try {
if($user = $getLoggedUser()) {
$currentPayload['user'] = $user;
}
$currentPayload['isGuest'] = false;
} catch (Exception $e) {
// is guest
}
} catch(Exception $e) {
// Impossible to parse token
}
foreach ($customPayload as $key => $value) {
$currentPayload[$key] = $value;
}
return $currentPayload;
};
// ----------------------------------------------------------------
// AUTH TOKEN PAYLOAD
// ----------------------------------------------------------------
try {
$getLoggedUser();
$payload = ['isGuest' => false];
} catch (Exception $e) {
$payload = ['isGuest' => true];
}
try {
$payload = $mountAuthPayload($payload);
} catch (Exception $e) {
// Make nothing cause token is invalid, expired, etc., or not exists.
// Like a guest session. Create a token without user data.
}
一些路线(保存用户移动设备的简单示例):
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function () use ($getLoggedUser, $mountAuthPayload) {
Route::post('/session/device', function () use ($Response, $getLoggedUser, $mountAuthPayload) {
$Response = new \Illuminate\Http\Response();
$user = $getLoggedUser();
// code to save on database the user device from current "session"...
$payload = app('tymon.jwt.payload.factory')->make($mountAuthPayload(['device' => $user->device->last()->toArray()]));
$token = JWTAuth::encode($payload);
$Response->header('Authorization', 'Bearer ' . $token);
$responseContent = ['setted' => 'true'];
$Response->setContent($responseContent);
return $Response;
});
});