为防止在应用JWT的时候,Token过期后,会主动退出零碎回到登录页面,最好是采纳双Token的机制;具体过程简略形容一下:
- 用户登录,零碎返回两个令牌,AccessToken和RefreshToken,AccessToken是资源拜访令牌,有效期较短;RefreshToken是刷新令牌,有效期较长。
- 用户通过主动在Header传递AccessToken。申请资源拜访,直到AccessToken过期。
- AccessToken过期后,前端主动应用RefreshToken向服务器申请新的AccessToken
- 客户端应用新的AccessToken申请资源,直到RefreshToken生效
Jwt 4.0以上版本的封装网上的参考比拟少,在这里,提供一份简略的封装,至于双令牌的具体实现,前面再陆续分享。
前提条件:PHP 7.4版本及以上,lcobucci/jwt 4.1.5
封装类文件:utils/JwtTools.php
<?phpnamespace utils;use Lcobucci\Clock\SystemClock;use Lcobucci\JWT\Configuration;use Lcobucci\JWT\Signer\Key\InMemory;use Lcobucci\JWT\Signer\Hmac\Sha256;use Lcobucci\JWT\UnencryptedToken;use Lcobucci\JWT\Validation\Constraint\IssuedBy;use Lcobucci\JWT\Validation\Constraint\PermittedFor;use Lcobucci\JWT\Validation\Constraint\SignedWith;use Lcobucci\JWT\Validation\Constraint\StrictValidAt;use Lcobucci\JWT\Validation\RequiredConstraintsViolated;use think\exception\ValidateException;class JwtTools{ protected $issuedBy = 'rds.server'; protected $permittedFor = 'rds.client'; protected $issuedAt; protected $expiresAtAccess; protected $expiresAtRefresh; protected $secrect = 'aHR0cDovL3Jkcy5yYWlzZWluZm8uY24='; public function __construct() { config('system.jwt_issued_by') ? $this->issuedBy = config('system.jwt_issued_by') : null; config('system.jwt_permitted_for') ? $this->permittedFor = config('system.jwt_permitted_for') : null; config('system.jwt_secrect') ? $this->secrect = config('system.jwt_secrect') : null; $this->issuedAt = new \DateTimeImmutable(); $this->expiresAtAccess = $this->issuedAt->modify(config('system.jwt_expires_at_access') ? config('system.jwt_expires_at_access') : '+1 minute'); $this->expiresAtRefresh = $this->issuedAt->modify(config('system.jwt_expires_at_refresh') ? config('system.jwt_expires_at_refresh') : '+5 minute'); } /** * 生成Jwt配置对象 * @return Configuration */ private function createJwt(){ return Configuration::forSymmetricSigner(new Sha256(),InMemory::base64Encoded($this->secrect)); } /** * 生成Token * @param array $bind 必须存在字段 uid * @param string $type * @return string */ public function getToken(array $bind=[], $type = 'Access'){ $config = $this->createJwt(); $builder = $config->builder(); // 拜访Token能够携带用户信息,刷新Token只携带用户编号 if(is_array($bind) && !empty($bind)){ foreach ($bind as $k => $v){ $builder->withClaim($k,$v); } $builder->withClaim('scopes',$type == 'Access' ? 'Access' : 'Refresh'); } $token = $builder ->issuedBy($this->issuedBy) ->permittedFor($this->permittedFor) ->issuedAt($this->issuedAt) ->canOnlyBeUsedAfter($this->issuedAt->modify('+1 second')) ->expiresAt($type == 'Access' ? $this->expiresAtAccess : $this->expiresAtRefresh) ->getToken($config->signer(),$config->signingKey()); return $token->toString(); } /** * 校验Token * @param $token * @return bool */ public function verify($token){ $config = $this->createJwt(); try { $token = $config->parser()->parse($token); assert($token instanceof UnencryptedToken); } catch (\Exception $e){ \think\facade\Log::error('令牌解析失败[1]:'.$e->getMessage()); return ['status'=>1,'msg'=>'令牌解析谬误']; } // 验证签发端是否匹配 $validate_issued = new IssuedBy($this->issuedBy); $config->setValidationConstraints($validate_issued); $constraints = $config->validationConstraints(); try { $config->validator()->assert($token,...$constraints); } catch (RequiredConstraintsViolated $e){ \think\facade\Log::error('令牌验证失败[2]:' . $e->getMessage()); return ['status'=>2,'msg'=>'签发谬误']; } //验证客户端是否匹配 $validate_permitted_for = new PermittedFor($this->permittedFor); $config->setValidationConstraints($validate_permitted_for); $constraints = $config->validationConstraints(); try { $config->validator()->assert($token,...$constraints); } catch (RequiredConstraintsViolated $e){ \think\facade\Log::error('令牌验证失败[3]:' . $e->getMessage()); return ['status'=>3,'msg'=>'客户端谬误']; } // 验证是否过期 $timezone = new \DateTimeZone('Asia/Shanghai'); $time = new SystemClock($timezone); $validate_exp = new StrictValidAt($time); $config->setValidationConstraints($validate_exp); $constraints = $config->validationConstraints(); try { $config->validator()->assert($token,...$constraints); } catch (RequiredConstraintsViolated $e){ \think\facade\Log::error('令牌验证失败[4]:' . $e->getMessage()); return ['status'=>4,'msg'=>'已过期']; } // 验证令牌是否已应用预期的签名者和密钥签名 $validate_signed = new SignedWith(new Sha256(),InMemory::base64Encoded($this->secrect)); $config->setValidationConstraints($validate_signed); $constraints = $config->validationConstraints(); try { $config->validator()->assert($token,...$constraints); } catch (RequiredConstraintsViolated $e){ \think\facade\Log::error('令牌验证失败[5]:' . $e->getMessage()); return ['status'=>5,'msg'=>'签名谬误']; } return ['status'=>0,'msg'=>'验证通过']; } /** * 获取token的载体内容 * @param $token * @return mixed */ public function getTokenContent($token){ $config = $this->createJwt(); try { $decode_token = $config->parser()->parse($token); $claims = json_decode(base64_decode($decode_token->claims()->toString()),true); } catch (\Exception $e){ throw new ValidateException($e->getMessage()); } return $claims; }}
配套配置文件:config/system.php
<?php// +----------------------------------------------------------------------// | 零碎设置// +----------------------------------------------------------------------return [ // 明码加密 'password_secrect' => 'Rapid_Development_System', // 是否开启验证码 'verify_status' => false, // JWT配置 'jwt_issued_by' => 'rds.server', 'jwt_permitted_for' => 'rds.client', 'jwt_secrect' => 'aHR0cDovL3Jkcy5yYWlzZWluZm8uY24=', 'jwt_expires_at_access' => '+5 minute', 'jwt_expires_at_refresh' => '+30 minute', ];
测试类文件:app/admin/controller/JwtTest.php
<?phpnamespace app\admin\controller;use app\BaseController;use utils\JwtTools;class JwtTest extends BaseController{ public function index() { return '您好!这是一个[admin]示例利用'; } /** * 创立Token * @return \think\response\Json */ public function getToken(){ $type = $this->request->param('type','Access'); $jwtTools = new JwtTools(); $token = $jwtTools->getToken(['uid'=>1],$type); return json(['status'=>200,'data'=>$token]); } /** * 提取Token内容 * @return \think\response\Json */ public function getContent(){ $token = $this->request->header('AccessToken'); if($token){ $jwtTools = new JwtTools(); $content = $jwtTools->getTokenContent($token); } else { $content = '无无效令牌'; } return json(['status'=>200,'data'=>$content]); } /** * 验证令牌 * @return \think\response\Json */ public function verifyToken(){ $token = $this->request->header('AccessToken'); if($token){ $jwtTools = new JwtTools(); $content = $jwtTools->verify($token); } else { $content = '无无效令牌'; } return json(['status'=>200,'data'=>$content['msg']]); } /** * 登录后生成拜访令牌和刷新令牌 * @return \think\response\Json */ public function getTokens(){ $jwtTools = new JwtTools(); $payload = [ 'uid' => [ 'user_id' => 100, 'username' => 'Tome', 'sex' => 2, ] ]; $accessToken = $jwtTools->getToken($payload,'Access'); $refreshToken = $jwtTools->getToken(['uid'=>100],'Refresh'); $tokens = [ 'Access' => $accessToken, 'Refresh'=> $refreshToken ]; return json(['status'=>200,'data'=>$tokens]); } /** * 通过刷新令牌,申请新的拜访令牌 * @return \think\response\Json */ public function refreshToken(){ $token = $this->request->header('RefreshToken'); $jwtTools = new JwtTools(); if($jwtTools->verify($token)){ $content = $jwtTools->getTokenContent($token); $accessToken = $jwtTools->getToken(['uid'=>$content['uid']],'Access'); $tokens = [ 'Access' => $accessToken, 'Refresh'=> $token ]; return json(['status'=>200,'data'=>$tokens]); } else { return json(['status'=>411,'data'=>'刷新令牌有效']); } }}
通过POSTMAN软件,调用测试接口即可!