乐趣区

关于php:lcobuccijwt-41-双Token的简单封装

为防止在应用 JWT 的时候,Token 过期后,会主动退出零碎回到登录页面,最好是采纳双 Token 的机制;具体过程简略形容一下:

  1. 用户登录,零碎返回两个令牌,AccessToken 和 RefreshToken,AccessToken 是资源拜访令牌,有效期较短;RefreshToken 是刷新令牌,有效期较长。
  2. 用户通过主动在 Header 传递 AccessToken。申请资源拜访,直到 AccessToken 过期。
  3. AccessToken 过期后,前端主动应用 RefreshToken 向服务器申请新的 AccessToken
  4. 客户端应用新的 AccessToken 申请资源,直到 RefreshToken 生效

Jwt 4.0 以上版本的封装网上的参考比拟少,在这里,提供一份简略的封装,至于双令牌的具体实现,前面再陆续分享。

前提条件:PHP 7.4 版本及以上,lcobucci/jwt 4.1.5

封装类文件:utils/JwtTools.php

<?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 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

<?php


namespace 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 软件,调用测试接口即可!

退出移动版