在前面的文章中,我们引入了JWT的验证方式,但是在前面并没有做过多的处理,只是用JWT生成了一个token,那么今天来就解决后续问题
为了保证用户信息的安全, 我们的生成的JWT不可能一直有效,我们在配置文件里边配置的有有效期,这里的单位是分钟
但是当我们用户一直在60分钟之内在操作,但是JWT过期后就需要用户重新登录,这样是不合理的,那么就需要对JWT生成的token进行刷新,从而可以达到用户一直有效请求
那么接下来我们就模拟一下用户的操作,来做个简单的实例
我们先自定义一个路由文件,用来存放我们自己路由
然后定义几个路由用来测试
修改一下JWT失效的时间
在jwt-auth的组件中过期校验是由Expiration中的
validatePayload来校验jwt的过期时间
然后这里进行修改一下,因为过期时间是1分钟不好测试,我们来把这个过期时间进行调整一下,调整为15秒就过期
然后我们进行一下测试登录
等15秒后咱们来获取一下用户信息,这里现在会报出token已经过期
其实这里存在的问题就已经很明显了,如果用户这个时候在正常的操作,但是我们的token这个时候给过期了,那么对用户的体验来说肯定是不好的,那么下来我们就来解决这个问题
这个时候我们可以在异常这里做一个这样的操作,那就是让他在时间过期后给她重置登录时时间,如果到了这个时间我们需要重置一下请求头的token数据,但这样是手动操作的
然后在刷新页面,就依然可以获取到用户信息,但是这样肯定是不行的,这样做只是为了显示这个效果而已,实际项目这样肯定不可以,token到期后,就直接跳到登录,这样肯定是不可以的
所以我们的token在无刷新界面的情况下就很有必要了
那么代码怎么写?
代码写在哪里呢?
如果说对于token的检测,最好是使用与中间件,因为它可以很方便的帮助由我做到对于api请求的token检测;也就是说代码会写在于中间件中
所以创建一个刷新token的中间件
那么中间件会有什么问题?
1.
它不确定是否用户有登入
2.
它不确定之前登入的用户是否退出了登入
3.
如果当前的token过期了那不就请求就失败了?怎么解决呢?
解决问题以及流程;
1.
可以通过判断token是否存在,如果存在就有登入没有就没有登入
2.
判断用户的登入状态是否为退出;
3.
判断是否token过期
4.
跟新请求头部
流程:
创建自定义JWT自动刷新的中间件
这里是中间件里边的内容,参考地址:https://learnku.com/articles/7264/using-jwt-auth-to-implement-api-user-authentication-and-painless-refresh-access-token
<?php
namespace App\Http\Middleware;
use Auth;
use Closure;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class RefreshJwtToken extends BaseMiddleware
{
public function handle($request, Closure $next)
{
// 检查此次请求中是否带有 token,如果没有则抛出异常。
$this->checkForToken($request);
// 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException 异常
try {
// 检测用户的登录状态,如果正常则通过
if ($this->auth->parseToken()->authenticate()) {
return $next($request);
}
throw new UnauthorizedHttpException('jwt-auth', '未登录');
} catch (TokenExpiredException $exception) {
// 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中
try {
// 刷新用户的 token
$token = $this->auth->refresh();
echo '<br>---statr-----<br>';
echo $token;
echo '<br>----end----<br>';
// 使用一次性登录以保证此次请求的成功
$sub = $this->auth->manager()->getPayloadFactory()->buildClaimsCollection()
->toPlainArray()['sub'];
auth('api')->onceUsingId($sub);
} catch (JWTException $exception) {
// 如果捕获到此异常,即代表 refresh 也过期了,
// 用户无法刷新令牌,需要重新登录。
throw new UnauthorizedHttpException('jwt-auth',
$exception->getMessage());
}
}
// 在响应头中返回新的 token
return $this->setAuthenticationHeader($next($request), $token);
}
}
其实实际上需要的方法就是
$this->auth->parseToken()->authenticate() | 校验用户的登入状态 |
---|---|
$this->checkForToken($request) | 校验是否存在token |
setAuthenticationHeader() | 向响应输出的时候设置刷新的token |
$this->auth->refresh() | 刷新token |
模仿BaseMiddleware-> authenticate()的写法,不过注意那个方法中所抛出的异常并不是实际所需要的异常JWTException 包含了所有异常对于过期时间的异常也在这里,但是对于目前的情况最好是可以抛出TokenExpiredException 过期的异常;
其实最难的还是后面那个点,这是难点所在。这个点的解释最有从源码的角度解释会好;
$this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()[‘sub’];
获取jwt-token中的用户id
auth(‘api’)->onceUsingId();将改定的用户设置为登入;其实这一步就是把用户设置成为登录状态
然后进行测试
首先需要把上边创建的中间件注册到我们的路由中间件里边去
然后修改路由
把Expiration.php恢复,当令牌过期,让其抛出TokenExpiredException异常,让自定义的中间件捕获后在来生成令牌
然后我们在来测试
间隔一分钟去访问user那个路由,我们的token就已经刷新了
这边需要注意一点就是token刷新之后就会进入黑名单,之后就不能在进行刷新
所以项目在特殊情况下需要设置这几个参数
这里需要注意一个问题,中间件谁在前就会执行谁
在文章一开始就说了需要判断用户是否登录,由于jwt里边不存在check的检验方法,所以我们自己定义一个webCheck方法
因为在项目中曾经修改过配置文件,把api的认证改为了jwt的认证守卫,
这个守卫就位于\vendor\tymon\src
在之前使用auth(‘api’)->
attempt 的时候实际就是执行的下面的方法。
直接进入login的方法
实际上登入的方法参数中并没有太多的操作,就是校验用户的信息,然后再设计登入之后的user
也就是在这个过程中会直接返回出user用户的信息;
而在正常情况下使用auth的时候是可以,因为是使用的web,也就是auth.php中的配置
也就是使用的是SessionGurad , 直接进入login方法
在通过getAuthIdentifier()方法完成,完成之后然后再设置session缓存;
也就是说用户的信息会缓存在session中
那么来看一下校验;
JWTGurad与SessionGuard的校验,通过查找源码会发现并不存在check方法,这个方法位于Illuminate\Auth\GuardHelpers中;
可以看到实际就是使用对应guard(web,api)中的user方法,也就是通过user方法获取用户,如果说用户不存在就会返回false;
简单解释:
jwt的校验过程:最为重要的点就在于,通过request获取jwt然后再去解析获取用的信息;
session的校验就是从session中获取用户的id然后再去数据库中查询用户
所以这就是问题所以在;那么针对于这个问题的话,既希望能够保持jwt对于api认证,又希望在web端校验(会员登入)
最好的解决办法就是在JWTGurad中添加一个webCheck的校验方法;但是实际上这样也不并不是很好因为版本会更新所以最好的办法就是扩展自定义guard
在项目中通过Auth:: guard (‘守卫名称’) 就可以解析出所需要的守卫;
然后再通过resolve方法去解析出这个守卫;
发现这个类中有一个数组customCreators,这说明,我们是可以自定义driver的。也就是说,虽然config/auth.php的guards中说driver只支持session和token,但实际上是可以自己扩展的。
可以测试一下
在这个地方通过dd打印扩展的Guard
为了方便测试,接下来所有的测试路由均放置于routes/test.php中,并且以test为前缀;并且中间件为web 组
然后访问
那怎么扩展呢,顺藤摸瓜,发现class AuthManager中有一个方法extend:
然后看一下是怎么加载JWTGuard的
打开jwt的
这个extendAuthGuard就是在父级AbstractServiceProvider中的方法
可以从方法中就可以看到这就是加载JWTGuard的方法所在;也就是对于auth的guard扩展
其实也就是说JWTGuard 的扩展载入是通过与服务提供LaravelServiceProvider中的boot载入的
接下来自定义一个auth的guard,并且继承与JWTGuard
然后建议这个时候就对于这个Guard进行扩展加入做测试;模仿JWTGuard的载入方式而操作的,
然后修改config/auth.php
在访问
下来完善自定义的app\Auth\JWTSessionGuard.php的代码
1. 重写JWTGuard控制器
2. 然后加入session的载入 注意RedisFactory 是use Illuminate\Contracts\Session\Session;
3. 下一步在登入成功之后调用updateSession 缓存用户的id
<?php
namespace App\Auth;
use Tymon\JWTAuth\JWT;
use Tymon\JWTAuth\JWTGuard;
use Illuminate\Http\Request;
use Illuminate\Contracts\Session\Session;
use Illuminate\Contracts\Auth\UserProvider;
use Tymon\JWTAuth\Contracts\JWTSubject;
class JWTSessionGuard extends JWTGuard
{
protected $session;
public function __construct(JWT $jwt, UserProvider $provider, Request $request , Session $session){
$this->session = $session ;
parent::__construct($jwt, $provider, $request);
}
public function login(JWTSubject $user)
{
$token = $this->jwt->fromUser($user);
$this->setToken($token)->setUser($user);
$this->updateSession($user->id);
return $token;
}
protected function updateSession($id)
{
$this->session->put($this->getName(), $id);
$this->session->migrate(true);
}
public function getName()
{
return 'login_jwt_' . sha1(static::class);
}
}
最后修改AuthServiceProvider中的boot方法
然后在测试
然后在测试一下,直接验证成功
添加中间件
然后还需要引入俩个中间件
然后在回到咱们用户登录的控制器
修改路由
然后测试,目前就把token本地存储,还有token的刷新就全部弄完了