用refreshToken折腾前端,后端token到期自动续期不行吗?

865 阅读4分钟

看了好多无感刷新Token的方法, 处理的都不尽如人意, 全都要折腾前端, 自己处理完不行吗

前端跟往常一样只需要存一个token

后端所有Jwt生成的真实token都存到Redis,随机字符串做key返回给前端存起来做token(以下称作虚拟token)

前端虚拟token不过期真实token过期:直接调用JWT重新签发新的真实Token存起来,前端虚拟token不变

有效期内访问可以延长有效期,有效期外重新登陆

别有事没事就跑去折腾前端

文件JWTPro.PHP

<?php

use think\cache\driver\Redis;

/**
 * @author 木头人下士
 * @since 2021-09-03
 * 自动延期Token
 * 用户登录互斥
 * Class JwtPro
 */
class JwtPro {
    protected $multi_login = true;//是否开启多地登录
    protected $prefix = 'JWToken_';//key前缀 服务器只存在单系统可以不改,多系统必须修改,否则可能会有意料之外的后果!!!!!!
    protected $pc_expired_time = 60 * 60 * 24;//电脑端过期时长
    protected $mobile_expired_time = 60 * 60 * 24 * 7;//手机端过期时长

    /**
     * 获取手机端token
     * @param $uid
     * @return false|string
     * @since 2021-09-03
     * @author 木头人下士
     */
    public function getMobileToken($uid){
        return $this->getToken($uid,$this->mobile_expired_time);
    }

    /**
     * 获取电脑端token
     * @param $uid
     * @return false|string
     * @since 2021-09-03
     * @author 木头人下士
     */
    public function getPcToken($uid){
        return $this->getToken($uid,$this->pc_expired_time);
    }

    /**
     * 验证手机端token
     * @since 2021-09-03
     * @author 木头人下士
     */
    public function checkMobileToken($token){
        return $this->checkToken($token,$this->mobile_expired_time);
    }

    /**
     * 验证pc端token
     * @since 2021-09-03
     * @author 木头人下士
     */
    public function checkPcToken($token){
        return $this->checkToken($token,$this->pc_expired_time);
    }

    /**
     * 获取JWToken
     * @author 木头人下士
     * @since 2021-09-03
     * @param $uid string|integer 需要加密的字段
     * @param $time integer 过期时间 默认两小时
     */
    public function getToken($uid,$time = 60 * 60 * 2,$random = ''){
        try {
            //获取redis实例
            $redis = $this->checkRedis();
            //拼接key
            $key = $this->prefix.$uid;
            //设置token
            $token = $this->setToken($key,$time);
            //如果token不存在
            $data['token'] = $token;
            //随机串
            $data['random'] = $random == '' ? get_random_str(7,5) : $random;
            $redis->set($key,$data);
            //设置过期时间秒数  到期自动删除 如果不是单点登录 则按照最后一个登录的token设置
            $redis->expire($key,$time);
            return $this->encode($uid,$time,$data['random']);
        }catch (Exception $e){
            echo $e->getMessage();
            exit;
        }
    }

    /**
     * 验证token
     * @author 木头人下士
     * @since 2021-09-03
     * @param $token string
     */
    public function checkToken($token,$time){
        try {
            //Redis实例
            $redis = $this->checkRedis();
            //解密token
            $decode_str = $this->decode($token);
            //分割解密串获取uid跟过期时间
            $tokenArr = explode(',',$decode_str);
            if($tokenArr[0] == '@shb@'){
                $uid = $tokenArr[1];
                $exp_time = $tokenArr[2];
                //判断随机串是否正确
                $random = $tokenArr[3] ?? '';
                //拼接key
                $key = $this->prefix.$uid;
                if($redis->EXISTS($key)){
                    //获取token
                    $data = $redis->get($key);
                    //验证
                    $check = $this->checkJWToken($data['token']);
                    if($check){
                        if(!$this->multi_login && isset($data['random']) && $data['random'] != $random){
                            return message('您的账号已在别处登录',false,null,401);
                        }
                        //先续期
                        $this->getToken($uid,$time,$random);
                        //验证成功
                        $checkArr = explode($this->prefix,$check);
                        return $checkArr[1];
                    }
                    //token验证失败
                    return false;
                }
                //不存在也是过期
                return false;
            }
            //token格式不正确
            return false;
        }catch (Exception $e){
            echo $e->getMessage();
            exit;
        }
    }

    /**
     * @author 木头人下士
     * @since 2021-09-03
     * 设置真实token
     */
    protected function setToken($key,$time){
        //JWT实例
        $jwt = new Jwt();
        //获取token 这边多设置一个小时,防止虚拟token验证成功,但真实token失效的问题  机智~~~
        return $jwt->getToken($key,$time + 60 * 60);
    }

    /**
     * 使用JWT验证JWToken
     * @param $token
     * @return bool|mixed
     */
    protected function checkJWToken($token){
        $jwt = new Jwt();
        //验证
        return $jwt->verifyToken($token);
    }

    /**
     * 验证Redis是否可用
     * @return Redis
     */
    protected function checkRedis(){
        //redis实例
        $redis = new Redis();
        //测试redis连接
        $redis->connect(env('cache.HOST'), env('cache.PORT'));
        $conn = $redis->ping();
        if (!$conn){
            echo '请开启Redis拓展';
            exit;
        }
        return $redis;
    }
    /**
     * 加密
     */
    protected function encode($data,$time,$random,$key = "#s21M"){
        $exp_time = time() + $time;
        $value = "@shb@,{$data},{$exp_time},{$random}";
        return openssl_encrypt($value, 'des-ecb', $key);
    }

    /**
     * 解密
     */
    protected function decode($data,$key = "#s21M"){
        return openssl_decrypt($data, 'des-ecb', $key);
    }
}

文件JWT.PHP

<?php
/**
 * JWT鉴权
 * @since 2020/11/14
 * Class Jwt
 * @package App\Helpers
 */
class Jwt
{
    // 头部
    private static $header = [
        'alg' => 'HS256', //生成signature的算法
        'typ' => 'JWT'    //JWT 令牌统一写为JWT
    ];

    // 使用HMAC生成信息摘要时所使用的密钥
    private static $key = 'dasdsadsafeb7f0a5ede98cdca2';

    /**
     * alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
     * @return string
     * @since 2020/11/14
     */
    public function getHeader()
    {
        return self::base64UrlEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
    }

    /**
     * Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据
     * JWT 规定了7个官方字段,供选用,这里可以存放私有信息,比如uid
     * @param $uid
     * @return string
     * @since 2020/11/14
     */
    public function getPayload($uid,$time)
    {
        $payload = [
            'iss' => 'jwt_yh', //签发人
            'exp' => time() + $time, //过期时间
            'sub' => 'YH', //主题
            'aud' => 'every', //受众
            'nbf' => time(), //生效时间,该时间之前不接收处理该Token
            'iat' => time(), //签发时间
            'jti' => 10001, //编号(JWT ID用于标识该JWT)
            'uid' => $uid, //私有信息,uid
        ];
        return self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
    }

    /**
     * 获取Token
     * @param array $payload jwt载荷
     * 格式如下非必须:
     * [
     *  'iss'=>'jwt_admin',  //该JWT的签发者
     *  'iat'=>time(),  //签发时间
     *  'exp'=>time()+7200,  //过期时间
     *  'nbf'=>time()+60,  //该时间之前不接收处理该Token
     *  'sub'=>'www.admin.com',  //面向的用户
     *  'jti'=>md5(uniqid('JWT').time())  //该Token唯一标识
     * ]
     * @return bool|string 返回结果
     * @since 2020/11/14
     */
    public function getToken($uid,$time = 60 * 60 * 24 * 7)
    {
        // 获取JWT头
        $header = self::getHeader();
        // 获取JWT有效载荷
        $payload = self::getPayload($uid,$time);
        // JWT头拼接JWT有效载荷
        $raw = $header . '.' . $payload;
        // Token字符串
        $token = $raw . '.' . self::signature($raw, self::$key, self::$header['alg']);
        // 返回Token
        return $token;
    }

    /**
     * 验证token是否有效,默认验证exp,nbf,iat时间
     * @param string $token token字符串
     * @return bool|mixed 返回结果
     * @since 2020/11/14
     */
    public function verifyToken(string $token)
    {
        if (!$token) {
            return false;
        }
        $tokens = explode('.', $token);
        if (count($tokens) != 3) {
            return false;
        }

        list($base64header, $base64payload, $sign) = $tokens;

        // 获取jwt算法
        $base64decodeheader = json_decode(self::base64UrlDecode($base64header), JSON_OBJECT_AS_ARRAY);
        if (empty($base64decodeheader['alg'])) {
            return false;
        }

        //签名验证
        if (self::signature($base64header . '.' . $base64payload, self::$key, $base64decodeheader['alg']) !== $sign) {
            return false;
        }

        $payload = json_decode(self::base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);

        //签发时间大于当前服务器时间验证失败
        if (isset($payload['iat']) && $payload['iat'] > time()) {
            return false;
        }

        //过期时间小宇当前服务器时间验证失败
        if (isset($payload['exp']) && $payload['exp'] < time()) {
            return false;
        }

        //该nbf时间之前不接收处理该Token
        if (isset($payload['nbf']) && $payload['nbf'] > time()) {
            return false;
        }

        return $payload['uid'];
    }

    /**
     * base64UrlEncode编码实现
     * @param string $str 需要编码的字符串
     * @return mixed 返回结果
     * @since 2020/11/14
     */
    private static function base64UrlEncode(string $str)
    {
        return str_replace('=', '', strtr(base64_encode($str), '+/', '-_'));
    }

    /**
     * base64UrlDecode解码实现
     * @param string $str 需要解码的字符串
     * @return bool|string 返回结果
     * @since 2020/11/14
     */
    private static function base64UrlDecode(string $str)
    {
        $remainder = strlen($str) % 4;
        if ($remainder) {
            $addlen = 4 - $remainder;
            $str .= str_repeat('=', $addlen);
        }
        return base64_decode(strtr($str, '-_', '+/'));
    }

    /**
     * HMACSHA256签名实现
     * @param string $str 待签名字符串
     * @param string $key 加密密钥
     * @param string $alg 算法方式
     * @return mixed 返回结果
     * @since 2020/11/14
     */
    private static function signature(string $str, string $key, string $alg)
    {
        $alg_config = array(
            'HS256' => 'sha256'
        );
        return self::base64UrlEncode(hash_hmac($alg_config[$alg], $str, $key, true));
    }
}