This commit is contained in:
2021-08-06 16:54:17 +08:00
parent 1ac6c00a79
commit c1bc93fb4c
4 changed files with 238 additions and 416 deletions
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Snowflake\Jwt;
use Closure;
use Exception;
use HttpServer\Http\Request;
use HttpServer\IInterface\Middleware;
use Server\RequestInterface;
use Snowflake\Snowflake;
/**
* Class CoreMiddleware
* @package Snowflake\Snowflake\Route
* 跨域中间件
*/
class JWTAuthMiddleware implements Middleware
{
/** @var int */
public int $zOrder = 0;
/**
* @param Request $request
* @param Closure $next
* @return mixed
* @throws Exception
*/
public function onHandler(RequestInterface $request, Closure $next): mixed
{
$authorization = $request->header('Authorization');
if (empty($authorization)) {
throw new JWTAuthTokenException('JWT voucher cannot be empty.');
}
if (!str_starts_with($authorization, 'Bearer ')) {
throw new JWTAuthTokenException('JWT Voucher Format Error.');
}
$authorization = str_replace('Bearer ', '', $authorization);
$jwt = Snowflake::app()->getJwt();
if (!$jwt->validator($authorization)) {
throw new JWTAuthTokenException('JWT Validator fail.');
}
return $next($request);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Snowflake\Jwt;
use JetBrains\PhpStorm\Pure;
use Throwable;
/**
*
*/
class JWTAuthTokenException extends \Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
#[Pure] public function __construct($message = "", $code = 4001, Throwable $previous = null)
{
parent::__construct($message, 4001, $previous);
}
}
+59 -416
View File
@@ -3,16 +3,13 @@ declare(strict_types=1);
namespace Snowflake\Jwt;
use Annotation\Inject;
use Exception;
use HttpServer\Http\HttpHeaders;
use Server\Constrict\Request;
use Snowflake\Abstracts\Component;
use Snowflake\Abstracts\Config;
use Snowflake\Cache\Redis;
use Snowflake\Core\Json;
use Snowflake\Core\Str;
use Snowflake\Exception\AuthException;
use Snowflake\Exception\ConfigException;
use Snowflake\Snowflake;
/**
@@ -22,56 +19,12 @@ use Snowflake\Snowflake;
class Jwt extends Component
{
/** @var int $user */
private int $user;
use JwtHelper;
private array $data;
private array $source = ['browser', 'android', 'iphone', 'pc', 'mingame'];
private array $config = ['token' => ''];
private ?int $timeout = 7200;
private string $key = 'www.xshucai.com';
private ?string $public = '-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6BuML3gtLGde7QKNuNST
UCB9gdHC7XIpOc7Wx2I64Esj3UxWHTgp3URj0ge8zpy7A3FfBdppR7d1nwoD6Xad
jqfjEWpTy4WwGYsOfH0tFl3wAmse0lebF4NFsS9pzrikQT6c9qsVm88pCjvg4i5t
WhTMEnpTFDYoDR0KXlLXltQMudBBUHFaVwP0wKJ/cGX7R1Mrv35K4MXwQFOuGZkP
hsp2rO9x5LjtSKIXbexy7WhUu6QMjD/XzgsXr9UF+ExYmBGXRVWgNFLMkiaCZ2Uz
WlQhpQrA5/wKd76dCzjvqw9M32OiZl2lCKT73cV8GUvt7BNsM1SiPhqfY7nhO6y3
cwIDAQAB
-----END PUBLIC KEY-----';
private ?string $private = '-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA6BuML3gtLGde7QKNuNSTUCB9gdHC7XIpOc7Wx2I64Esj3UxW
HTgp3URj0ge8zpy7A3FfBdppR7d1nwoD6XadjqfjEWpTy4WwGYsOfH0tFl3wAmse
0lebF4NFsS9pzrikQT6c9qsVm88pCjvg4i5tWhTMEnpTFDYoDR0KXlLXltQMudBB
UHFaVwP0wKJ/cGX7R1Mrv35K4MXwQFOuGZkPhsp2rO9x5LjtSKIXbexy7WhUu6QM
jD/XzgsXr9UF+ExYmBGXRVWgNFLMkiaCZ2UzWlQhpQrA5/wKd76dCzjvqw9M32Oi
Zl2lCKT73cV8GUvt7BNsM1SiPhqfY7nhO6y3cwIDAQABAoIBADPihJHP8XktmmCs
43Vfv5Z3zNaKR2LA1Eph3E0xviuJYHkFqXJarbESqqW2qRQeoQeB/lXWnxYzAo4M
tRcpNss+6FlqRVUHi3gKR7C4Yq3PTemcfIVUpAy7gYa8LJDTYZRcJMZXNDtiMbBh
9kFZU4SBhaTTx2KLQKS9yyWOqzbBvyLXN+1+Wy477M9+MXXTKw79dO+pML6cR0yl
pNfVR5FX5L/GB5vOtQB/Aqg/CKT8NC5MzWPnKY+TPCCHZyoZuB9dLDuWOlqsN4QX
Y4B8fFca5yRwzHra5aGoqdaT/zGctt+I6V/f/KNQCo36f9LPxeXg1+FHvvtTj5WZ
N8CGPzECgYEA9R7lRMXzrHE4rK0DhxQXIFbIKKtxrimqZQdbwOUeYYD2R6CDSItK
z88RSYElmd6wiS7fYIaheXNqJ8Yu6SQFBF/yshBwjQVl9NJG94LJlgx1XnVZEju6
OZjMUOhHXBymtXnLo16pDRl8odc4MFLRH25/vLtwChUr+Qoyt54GzFUCgYEA8mjL
jdh94JAmcdnDXsKgjNOGyNWGDVvWoFmy8lEQsMXY1JJnEd3YfDM2prmv3vaoiXzi
YkSETl6ZUtJqh78MnHCBY1vI6EAcKQAF/kvP2TataRCXNcGNQwn2mtq+B+heTta6
Di8jjAdmdUAYHbmOQryBudiRYG7JEF038elzvKcCgYEAq81ByFguGBkrLev94vkz
1Fi+5bJ0dSuC4Fit+J8eEhz/gOiB26C1iL2LUkeQgS5R8XTG37K9DpDUQJhpXMMA
OTa+tgtLt6um8FdJokUq4V5ODSyWh28RcTklSzfifC8gsWVyU0kPl7zbW9uq6EPD
ixI5uaBuQMLiFSUOsx+xiBkCgYEAtqXHWeVZUy7KCNavomK7XeCzmfdovgAIw2FS
t8nk7YzlR6XYC1pAl7Ru5Ujb/v+TFaUHXkuJ9RLKK+Fna0jEU8thcl/iDTzg+vON
kIHG5j+Qga2CgXqI2Y5URXGz5XlsNbMNFUrnWcbpqEbW5O6/BgHLLSDEyQgwbygN
0zS3g9kCgYEAhssb7kOljdIul4lY5MXc67Zf1dp6S2bucLOxsG6cRW07b3pBz7QF
5aPE7ZwnkzTnA4HuGGauKj+qKGAR7ve55XClAq/XipiVFrjwV/t3LC6j5DoqTJYR
mlAZUEjsoaT9vjvjGTxl3uCm0TX5KTgtSJIt2kA1tYVjQef+/iZTHxY=
-----END RSA PRIVATE KEY-----';
/** @var Request */
#[Inject(Request::class)]
private Request $request;
/**
@@ -88,426 +41,116 @@ mlAZUEjsoaT9vjvjGTxl3uCm0TX5KTgtSJIt2kA1tYVjQef+/iZTHxY=
$this->timeout = Config::get('ssl.timeout', 7200);
}
/**
* @param string $publicKey
*/
public function setPublic(string $publicKey)
{
$this->public = $publicKey;
}
/**
* @param int $timeout
*/
public function setTimeout(int $timeout)
{
$this->timeout = $timeout;
}
/**
* @param string $timeout
*/
public function setKey(string $timeout)
{
$this->key = $timeout;
}
/**
* @param string $privateKey
*/
public function setPrivate(string $privateKey)
{
$this->private = $privateKey;
}
/**
* @param int $unionId
* @param array $headers
*
* @return array
* @return string
* @throws Exception
*/
public function create(int $unionId, array $headers = []): array
public function create(int $unionId): string
{
$this->user = $unionId;
$this->config['time'] = time();
if (empty($headers)) {
$headers = request()->getHeaders();
} else if ($headers instanceof HttpHeaders) {
$headers = $headers->getHeaders();
}
$this->data = $headers;
$this->data = $this->request->getHeaders();
if (!isset($this->data['source'])) {
$this->data['source'] = 'browser';
}
return $this->createEncrypt($unionId);
}
/**
* @return string
*/
private function jwtHeader(): string
{
return base64_encode(json_encode(['type' => 'openssl', 'encrypt' => $this->encrypt]));
}
/**
* @param $unionId
* @return array
* @return string
* @throws Exception
* 对相关信息进行加密
*/
private function createEncrypt($unionId): array
private function jwtBody($unionId): string
{
$caches = $this->clear($unionId);
$param = $this->assembly(array_merge($this->config, [
'user' => $unionId,
'token' => $this->token($unionId, [
'device' => Str::rand(128),
], $this->config['time']),
]), TRUE);
$refresh = array_intersect_key($param, $this->config);
$params['user'] = $this->user;
$params['token'] = $refresh['token'];
$json = json_encode($params, JSON_NUMERIC_CHECK | JSON_UNESCAPED_UNICODE);
$json = json_encode(['unionId' => $unionId, 'createTime' => time(), 'loginIp' => request()->getIp(), 'expire_at' => time() + $this->timeout]);
openssl_private_encrypt($json, $encode, $this->private);
$refresh['refresh'] = base64_encode($encode);
$this->setRefresh($refresh['refresh']);
$redis = $this->getRedis();
foreach ($caches as $cache) {
$redis->del($cache);
}
return $refresh;
}
/**
* @param bool $update
* @param array $param
* @return array
* @throws
*/
private function assembly(array $param, bool $update = FALSE): array
{
if (isset($param['sign'])) {
unset($param['sign']);
}
$param = $this->initialize($param);
asort($param, SORT_STRING);
$_tmp = [];
foreach ($param as $key => $val) {
$_tmp[] = trim($key) . '=>' . trim((string)$val);
}
$param['sign'] = md5(implode(':', $_tmp));
if ($update) {
$this->setCache($param);
}
return $param;
}
/**
* @param array $headers
* @return array
* @throws Exception
*/
public function refresh(array $headers = []): array
{
$this->data = $headers;
if (!openssl_public_decrypt(base64_decode($headers['refresh']), $data, $this->public)) {
throw new AuthException('信息解码失败.');
}
$this->user = $data['user'];
if (!$this->getRedis()->exists('refresh:' . $this->user)) {
throw new AuthException('refresh data error.');
}
$this->getRedis()->del('refresh:' . $this->user);
return $this->create($this->user, $headers);
return base64_encode($encode);
}
/**
* @param array $headers
* @return mixed
* @throws AuthException
*/
public function getTokenUser(array $headers = []): int
{
$this->data = $headers;
if (!openssl_public_decrypt(base64_decode($headers['refresh']), $data, $this->public)) {
throw new AuthException('信息解码失败.');
}
$data = Json::decode($data, true);
if (!isset($headers['token']) || $data['token'] != $headers['token']) {
throw new AuthException('信息解码失败.');
}
return (int)$data['user'];
}
/**
* @param array $param
*
* @return array
*/
private function initialize(array $param): array
{
$_param = [
'version' => '1',
'source' => $this->getSource(),
];
if (!isset($param['device'])) {
$param['device'] = Str::rand(128);
}
return array_merge($_param, $param);
}
/**
* @param array $data
* @throws Exception
*/
private function setCache(array $data)
{
$redis = $this->getRedis();
$redis->hMset($this->authKey($this->getSource(), $data['token']), $data);
$redis->expire($this->authKey($this->getSource(), $data['token']), $this->timeout);
}
/**
* @param string $refresh
* @throws Exception
*/
private function setRefresh(string $refresh)
{
$redis = $this->getRedis();
$redis->set('refresh:' . $this->user, $refresh);
$redis->expire('refresh:' . $this->user, $this->timeout);
}
/**
* @param string $_source
* @param string $token
*
* @param $unionId
* @return string
* @throws Exception
*/
private function authKey(string $_source, string $token): string
private function createEncrypt($unionId): string
{
$source = $this->getSource();
if (!empty($_source)) $source = $_source;
$params[] = $this->jwtHeader();
$params[] = $this->jwtBody($unionId);
return 'Tmp_Token:' . strtoupper($source) . ':' . $token;
$params[] = hash($this->encrypt, $params[0] . $params[1]);
return implode('.', $params);
}
/**
* @param $token
* @return string
* @throws JWTAuthTokenException
*/
public function getSource(): string
public function getUnionId($token): string
{
return $this->data['source'] ?? 'browser';
return $this->unpack($token)['unionId'];
}
/**
* @param int $user
* @param array $param
* @param null $requestTime
*
* @return string
*/
private function token(int $user, array $param = [], $requestTime = NULL): string
{
$str = '';
$user = (string)$user;
$_user = str_split(md5($user . md5($user)));
ksort($_user);
foreach ($_user as $key => $val) {
$str .= md5(sha1($key . $val . $this->key));
}
foreach ($param as $key => $val) {
$str .= md5($str . sha1($key . md5($val)));
}
$str .= sha1(base64_encode((string)$requestTime));
return $this->preg(md5($str . $user));
}
/**
* @param string $str
*
* @return array|string|null 将字符串替换成指定格式
*/
private function preg(string $str): null|array|string
{
return preg_replace('/(\w{10})(\w{3})(\w{4})(\w{9})(\w{6})/', '$1-$2-$3-$4-$5', $str);
}
/**
* @param int $user
* @return string[]
* @throws Exception
*/
public function clear(int $user): array
{
$this->user = $user;
$redis = $this->getRedis();
if (is_bool($refresh = $redis->get('refresh:' . $this->user))) {
return [];
};
openssl_public_decrypt(base64_decode($refresh), $info, $this->public);
$_tmp = [];
if (!empty($info) && $json = json_decode($info, true)) {
if (!isset($json['token'])) {
return [];
}
foreach ($this->source as $value) {
$_tmp[] = $this->authKey($value, $json['token']);
}
}
return $_tmp;
}
/**
* @param array $data
* @param int $user
* @param $token
* @return bool
* @throws AuthException|Exception
* @throws JWTAuthTokenException
*/
public function check(array $data, int $user): bool
public function validator($token): bool
{
$this->data = $data;
$this->user = $user;
if (empty($this->user)) return FALSE;
$cache = $this->getUserModel();
if (empty($cache)) {
return FALSE;
$unpack = $this->unpack($token);
if ($unpack['expire_at'] < time()) {
return false;
}
$merge = $this->assembly(array_merge($cache, [
'token' => $data['token'],
]));
$check = array_diff_assoc($this->initialize($cache), $merge);
return !((bool)count($check));
return true;
}
/**
* @return mixed
* @throws
*/
public function getCurrentOnlineUser(): int
{
$this->data = request()->getHeaders();
return $this->loadByCache();
/**
* @param $token
* @return string
* @throws JWTAuthTokenException
* @throws Exception
*/
public function refresh($token): string
{
return $this->create($this->unpack($token)['unionId']);
}
/**
* @param string $token
* @param string $source
* @return mixed
* @throws AuthException
* @throws JWTAuthTokenException
*/
public function getOnlineUserByToken(string $token, string $source = 'BROWSER'): int
private function unpack(string $token): string
{
$this->data['token'] = $token;
$this->data['source'] = $source;
return $this->loadByCache();
}
/**
* @return int
* @throws AuthException
* @throws Exception
*/
private function loadByCache(): int
{
$model = $this->getUserModel();
if (empty($model)) {
return (int)$this->addError('授权信息已过期!');
if (count($explode = explode('.', $token)) != 3) {
throw new JWTAuthTokenException('JWT Voucher Format Error.');
}
if (!isset($model['user'])) {
return (int)$this->addError('授权信息错误!');
if (hash($this->encrypt, $explode[0] . $explode[1]) != $explode[2]) {
throw new JWTAuthTokenException('JWT Sign Validator Fail.');
}
if (!$this->check($this->data, (int)$model['user'])) {
return (int)$this->addError('授权信息不合法!');
if (!openssl_public_decrypt(base64_decode($explode[1]), $decode, $this->public)) {
throw new JWTAuthTokenException('JWT Voucher Unpack Error.');
}
$this->expireRefresh();
return (int)$model['user'];
}
/**
* @param array $header
* @return mixed
* @throws AuthException
* @throws Exception
*/
public static function checkAuth(array $header = []): mixed
{
$instance = Snowflake::app()->getJwt();
if (empty($header)) {
$header = request()->getHeaders();
}
$instance->data = $header;
$model = $instance->getUserModel();
if (empty($model) || !isset($model['user'])) {
return false;
}
if (!$instance->check($header, (int)$model['user'])) {
return false;
}
$instance->expireRefresh();
return $model['user'];
}
/**
* @param null $token
* @param null $source
* @throws Exception
*/
public function expireRefresh($token = null, $source = null)
{
if (!empty($token)) {
$this->data['token'] = $token;
}
if (!empty($source)) {
$this->data['source'] = $source;
}
if (!isset($this->data['token'])) {
return;
}
$key = $this->authKey($this->getSource(), $this->data['token']);
$this->getRedis()->expire($key, $this->timeout);
}
/**
* @return bool|array
* @throws Exception
*/
private function getUserModel(): bool|array
{
if (!isset($this->data['token'])) {
return $this->addError('暂无访问权限!');
}
$key = $this->authKey($this->getSource(), $this->data['token']);
return $this->getRedis()->hGetAll($key);
}
/**
* @return Redis|\Redis
* @throws
*/
private function getRedis(): Redis|\Redis
{
return Snowflake::app()->getRedis();
return Json::decode($decode, true);
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace Snowflake\Jwt;
trait JwtHelper
{
/** @var int $user */
private int $user;
private array $data;
private array $source = ['browser', 'android', 'iphone', 'pc', 'mingame'];
private array $config = ['token' => ''];
private ?int $timeout = 7200;
private string $key = 'www.xshucai.com';
private string $secret = '';
private string $encrypt = 'HS256';
private ?string $public = '-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6BuML3gtLGde7QKNuNST
UCB9gdHC7XIpOc7Wx2I64Esj3UxWHTgp3URj0ge8zpy7A3FfBdppR7d1nwoD6Xad
jqfjEWpTy4WwGYsOfH0tFl3wAmse0lebF4NFsS9pzrikQT6c9qsVm88pCjvg4i5t
WhTMEnpTFDYoDR0KXlLXltQMudBBUHFaVwP0wKJ/cGX7R1Mrv35K4MXwQFOuGZkP
hsp2rO9x5LjtSKIXbexy7WhUu6QMjD/XzgsXr9UF+ExYmBGXRVWgNFLMkiaCZ2Uz
WlQhpQrA5/wKd76dCzjvqw9M32OiZl2lCKT73cV8GUvt7BNsM1SiPhqfY7nhO6y3
cwIDAQAB
-----END PUBLIC KEY-----';
private ?string $private = '-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA6BuML3gtLGde7QKNuNSTUCB9gdHC7XIpOc7Wx2I64Esj3UxW
HTgp3URj0ge8zpy7A3FfBdppR7d1nwoD6XadjqfjEWpTy4WwGYsOfH0tFl3wAmse
0lebF4NFsS9pzrikQT6c9qsVm88pCjvg4i5tWhTMEnpTFDYoDR0KXlLXltQMudBB
UHFaVwP0wKJ/cGX7R1Mrv35K4MXwQFOuGZkPhsp2rO9x5LjtSKIXbexy7WhUu6QM
jD/XzgsXr9UF+ExYmBGXRVWgNFLMkiaCZ2UzWlQhpQrA5/wKd76dCzjvqw9M32Oi
Zl2lCKT73cV8GUvt7BNsM1SiPhqfY7nhO6y3cwIDAQABAoIBADPihJHP8XktmmCs
43Vfv5Z3zNaKR2LA1Eph3E0xviuJYHkFqXJarbESqqW2qRQeoQeB/lXWnxYzAo4M
tRcpNss+6FlqRVUHi3gKR7C4Yq3PTemcfIVUpAy7gYa8LJDTYZRcJMZXNDtiMbBh
9kFZU4SBhaTTx2KLQKS9yyWOqzbBvyLXN+1+Wy477M9+MXXTKw79dO+pML6cR0yl
pNfVR5FX5L/GB5vOtQB/Aqg/CKT8NC5MzWPnKY+TPCCHZyoZuB9dLDuWOlqsN4QX
Y4B8fFca5yRwzHra5aGoqdaT/zGctt+I6V/f/KNQCo36f9LPxeXg1+FHvvtTj5WZ
N8CGPzECgYEA9R7lRMXzrHE4rK0DhxQXIFbIKKtxrimqZQdbwOUeYYD2R6CDSItK
z88RSYElmd6wiS7fYIaheXNqJ8Yu6SQFBF/yshBwjQVl9NJG94LJlgx1XnVZEju6
OZjMUOhHXBymtXnLo16pDRl8odc4MFLRH25/vLtwChUr+Qoyt54GzFUCgYEA8mjL
jdh94JAmcdnDXsKgjNOGyNWGDVvWoFmy8lEQsMXY1JJnEd3YfDM2prmv3vaoiXzi
YkSETl6ZUtJqh78MnHCBY1vI6EAcKQAF/kvP2TataRCXNcGNQwn2mtq+B+heTta6
Di8jjAdmdUAYHbmOQryBudiRYG7JEF038elzvKcCgYEAq81ByFguGBkrLev94vkz
1Fi+5bJ0dSuC4Fit+J8eEhz/gOiB26C1iL2LUkeQgS5R8XTG37K9DpDUQJhpXMMA
OTa+tgtLt6um8FdJokUq4V5ODSyWh28RcTklSzfifC8gsWVyU0kPl7zbW9uq6EPD
ixI5uaBuQMLiFSUOsx+xiBkCgYEAtqXHWeVZUy7KCNavomK7XeCzmfdovgAIw2FS
t8nk7YzlR6XYC1pAl7Ru5Ujb/v+TFaUHXkuJ9RLKK+Fna0jEU8thcl/iDTzg+vON
kIHG5j+Qga2CgXqI2Y5URXGz5XlsNbMNFUrnWcbpqEbW5O6/BgHLLSDEyQgwbygN
0zS3g9kCgYEAhssb7kOljdIul4lY5MXc67Zf1dp6S2bucLOxsG6cRW07b3pBz7QF
5aPE7ZwnkzTnA4HuGGauKj+qKGAR7ve55XClAq/XipiVFrjwV/t3LC6j5DoqTJYR
mlAZUEjsoaT9vjvjGTxl3uCm0TX5KTgtSJIt2kA1tYVjQef+/iZTHxY=
-----END RSA PRIVATE KEY-----';
/**
* @param string $publicKey
*/
public function setPublic(string $publicKey)
{
$this->public = $publicKey;
}
/**
* @param int $timeout
*/
public function setTimeout(int $timeout)
{
$this->timeout = $timeout;
}
/**
* @param string $timeout
*/
public function setKey(string $timeout)
{
$this->key = $timeout;
}
/**
* @param string $privateKey
*/
public function setPrivate(string $privateKey)
{
$this->private = $privateKey;
}
}