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;
}
}