php

JWT lcobucci/jwt

2023-09-17

JWT适合一次性的命令认证,颁发一个有效期极短的JWT,即使暴露了危险也很小,由于每次操作都会生成新的JWT,实现无状态。

<?php

namespace app\common\library;

use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Parser;
use think\Exception;


class JWT
{
    /**
     * 现在的4.x是发行版 测试版
     * 使用3.3.1 稳定版本
     * composer require lcobucci/jwt 3.3.1
     */
    /**
     * 使用的加密类名称
     *  alg:
     * 使用公钥私钥 加密对 请使用下面的类
     * ['Lcobucci\JWT\Signer\Rsa\Sha256','Lcobucci\JWT\Signer\Rsa\Sha384','Lcobucci\JWT\Signer\Rsa\Sha512']
     *
     * 使用字符串密钥类【单向不可逆加密算法】 请使用下面的类
     * ['Lcobucci\JWT\Signer\Hmac\Sha256','Lcobucci\JWT\Signer\Hmac\Sha384','Lcobucci\JWT\Signer\Hmac\Sha512']
     *
     * Ecdsa类 --- 在github上找了一个 BitcoinPHP/BitcoinECDSA.php[这个已经很完整了,可以单独使用]  所以不加进来
     */
    private $alg = 'Lcobucci\JWT\Signer\Hmac\Sha256';

    /**
     * 用户 aud
     */
    private $audience;

    /**
     * 身份id  jti
     */
    private $id;

    /**
     * 发布时间 iat
     */
    private $issuedAt;

    /**
     * 发行人 iss
     */
    private $issuer;

    /**
     * 主题 sub
     */
    private $subject;

    /**
     * 到期时间 exp
     */
    private $expiration;

    /**
     * 在此之前不可用  nbf
     */
    private $notBefore;

    /**
     * 其他私有参数设置 比如uid
     */
    private $claims = [];

    /**
     * 加密私钥 用于加密 使用[file://]+[文件路径] 比如根目录下的prikey.txt文件 eg:file://prikey.txt
     */
    private $privateKey = null;

    /**
     * 解密公钥 如果 不使用 非对称加密类进行签名 那么公钥值等于私钥值
     * 使用[file://]+[文件路径] 比如根目录下的pubkey.txt文件 eg:file://pubkey.txt
     */
    private $publicKey = null;


    public function __construct($config = [])
    {
        if (!is_array($config)) {
            throw new Exception('构造参数必须是数组');
        }
        $time = time();
        $this->alg = isset($config['alg']) && !empty($config['alg']) ? $config['alg'] : $this->alg;
        $this->audience = isset($config['aud']) && !empty($config['aud']) ? $config['aud'] : get_ip();
        $this->id = isset($config['jti']) && !empty($config['jti']) ? $config['jti'] : $this->getNoncestr(20);
        $this->issuedAt = isset($config['iat']) && !empty($config['iat']) ? $config['iat'] : $time;
        $this->issuer = isset($config['iss']) && !empty($config['iss']) ? $config['iss'] : $_SERVER['SERVER_NAME'];
        $this->subject = isset($config['sub']) && !empty($config['sub']) ? $config['sub'] : '无主题';
        $this->expiration = isset($config['exp']) && !empty($config['exp']) ? $time + $config['exp'] : $time + 3600;
        $this->notBefore = isset($config['nbf']) && !empty($config['nbf']) ? $config['nbf'] : $time;
        $this->privateKey = isset($config['privateKey']) && !empty($config['privateKey']) ? $config['privateKey'] : null;
        $this->publicKey = isset($config['publicKey']) && !empty($config['publicKey']) ? $config['publicKey'] : null;
        $this->claims = isset($config['claims']) && is_array($config['claims']) ? $config['claims'] : $this->claims;
    }

    /**
     * 使用私钥加密token
     */
    public function getToken()
    {
        $token = (new Builder())
            ->issuedBy($this->issuer)
            ->permittedFor($this->audience)
            ->identifiedBy($this->id, true)
            ->issuedAt($this->issuedAt)
            ->relatedTo($this->subject)
            ->canOnlyBeUsedAfter($this->notBefore)
            ->expiresAt($this->expiration);

        if (count($this->claims) > 0) {

            for ($i = 0; $i < count($this->claims); ++$i) {
                $token->withClaim($this->claims[$i][0], $this->claims[$i][1]);
            }
        }

        if (is_null($this->privateKey)) {
            return $token->getToken();
        }

        $signer = new $this->alg;
        $privateKey = new Key($this->privateKey);

        return $token->getToken($signer,  $privateKey);
    }

    /**
     * 验证 token 的有效性
     */
    public function verify($token)
    {

        $tokenArr = explode('.', $token);

        if (count($tokenArr) != 3) {
            return ['result' => false, 'errorMsg' => '非法token'];
        }

        if (is_null($this->publicKey)) {
            return ['result' => false, 'errorMsg' => '解密密钥不能为空'];
        }

        $signer = new $this->alg;

        $signer_key = $this->publicKey;

        $token = (new Parser())->parse((string) $token);

        $data = new ValidationData();

        // $data->setIssuer($token->getClaim('iss'));
        // $data->setAudience($token->getClaim('aud'));
        // $data->setId($token->getHeader('jti'));
        // $data->setSubject($token->getClaim('sub'));

        $data->setIssuer($this->issuer);
        $data->setAudience($this->audience);
        $data->setId($this->id);
        $data->setSubject($this->subject);

        //如果没有使用加密,那么就只验证数据
        if ($token->getHeader('alg') !== 'none') {
            //验证密钥是否匹配【公钥私钥】
            if (!$token->verify($signer, $signer_key)) {
                return ['result' => false, 'errorMsg' => '密钥不对'];
            }
        }

        //验证token是否有效 数据比对不成功
        if (!$token->validate($data)) {
            return ['result' => false, 'errorMsg' => '签名错误'];
        }

        return ['result' => true, 'msg' => '验证通过'];
    }

    /**
     * 生成非对称密钥对
     * 目的:方便自己生成密钥对,可以用于测试
     */
    public function createRsaKey($alg = "sha256", $byte = 1024, $type = OPENSSL_KEYTYPE_RSA)
    {
        $config = array(
            "digest_alg" => $alg,
            "private_key_bits" => $byte,   //512 1024 2048 4096
            "private_key_type" => $type,
        );

        // 创建公钥和私钥 密钥对
        $res = openssl_pkey_new($config);

        //从新建的密钥对立面获取私钥
        openssl_pkey_export($res, $privKey);

        //从新建的密钥对立面获取公钥
        $pubKey = openssl_pkey_get_details($res);
        $pubKey = $pubKey["key"];

        return ['result' => true, 'msg' => '非对称密钥对生成成功', 'publicKey' => $pubKey, 'privateKey' => $privKey];
    }

    /**
     * 生成随机的字符作为本次请求的jti 识别id【标识】
     * @param $length int
     * 生成随机字符串
     * 大于10位,将(当前时间戳+7200  ---- 作为有效时间)隔个字符插入
     */
    private function getNoncestr($length = 20)
    {
        if ($length > 10) {
            $strs = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm";
            $str = substr(str_shuffle($strs), mt_rand(0, strlen($strs) - $length - 1 - 10), $length - 10);
            $strArr = str_split($str, 1);
            $timeArr = str_split(time() + 3600, 1);
            $string = "";
            if (count($strArr) < count($timeArr)) {

                for ($i = 0; $i < count($timeArr); ++$i) {

                    if (isset($strArr[$i])) {
                        $string .= $strArr[$i] . $timeArr[$i];
                    } else {
                        $string .= $timeArr[$i];
                    }
                }
            } else {

                for ($i = 0; $i < count($strArr); ++$i) {

                    if (isset($timeArr[$i])) {
                        $string .= $strArr[$i] . $timeArr[$i];
                    } else {
                        $string .= $strArr[$i];
                    }
                }
            }
        } else {
            $strs = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm";
            $string = substr(str_shuffle($strs), mt_rand(0, strlen($strs) - $length - 1), $length);
        }

        return $string;
    }

    /**
     * 设置属性
     */
    public function __set($name, $val)
    {
        return $this->$name = $val;
    }

    /**
     * 获取属性
     */
    public function __get($name)
    {
        return $this->$name;
    }
}