admin 发表于 2023-7-5 23:10:11

lcobucci/jwt 刷新验证 双Token封装

为避免在使用JWT的时候,Token过期后,会自动退出系统回到登录页面,最好是采用双Token的机制;具体过程简单描述一下:
[*]用户登录,系统返回两个令牌,AccessToken和RefreshToken,AccessToken是资源访问令牌,有效期较短;RefreshToken是刷新令牌,有效期较长。
[*]用户通过自动在Header传递AccessToken。申请资源访问,直到AccessToken过期。
[*]AccessToken过期后,前端自动使用RefreshToken向服务器申请新的AccessToken
[*]客户端使用新的AccessToken请求资源,直到RefreshToken失效

<?php


namespace 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 Jwt
{
    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('令牌解析失败:'.$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('令牌验证失败:' . $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('令牌验证失败:' . $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('令牌验证失败:' . $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('令牌验证失败:' . $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/jwt.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',
]; 测试类文件:jwtTest.php

<?php


namespace app\controller;

use app\BaseController;
use utils\Jwt;

class JwtTest extends BaseController
{
    public function index()
    {
      return '';
    }

    /**
   * 创建Token
   * @return \think\response\Json
   */
    public function getToken(){
      $type = $this->request->param('type','Access');
      $jwt = new Jwt();
      $token = $jwt->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){
            $jwt = new Jwt();
            $content = $jwt->getTokenContent($token);
      } else {
            $content = '无有效令牌';
      }
      return json(['status'=>200,'data'=>$content]);
    }

    /**
   * 验证令牌
   * @return \think\response\Json
   */
    public function verifyToken(){
      $token = $this->request->header('AccessToken');
      if($token){
            $jwt = new Jwt();
            $content = $jwt->verify($token);
      } else {
            $content = '无有效令牌';
      }
      return json(['status'=>200,'data'=>$content['msg']]);
    }

    /**
   * 登录后生成访问令牌和刷新令牌
   * @return \think\response\Json
   */
    public function getTokens(){
      $jwt = new Jwt();
      $payload = [
            'uid' => [
                'user_id'   => 100,
                'username'=> 'Tome',
                'sex'       => 2,
            ]
      ];
      $accessToken = $jwt->getToken($payload,'Access');
      $refreshToken = $jwt->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');
      $jwt = new Jwt();
      if($jwtTools->verify($token)){
            $content = $jwt->getTokenContent($token);
            $accessToken = $jwt->getToken(['uid'=>$content['uid']],'Access');
            $tokens = [
                'Access' => $accessToken,
                'Refresh'=> $token
            ];
            return json(['status'=>200,'data'=>$tokens]);
      } else {
            return json(['status'=>411,'data'=>'刷新令牌无效']);
      }
    }
}


页: [1]
查看完整版本: lcobucci/jwt 刷新验证 双Token封装