diff --git a/System/Jwt/JWTAuthMiddleware.php b/System/Jwt/JWTAuthMiddleware.php new file mode 100644 index 00000000..3a1e3220 --- /dev/null +++ b/System/Jwt/JWTAuthMiddleware.php @@ -0,0 +1,51 @@ +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); + } + +} diff --git a/System/Jwt/JWTAuthTokenException.php b/System/Jwt/JWTAuthTokenException.php new file mode 100644 index 00000000..df91a304 --- /dev/null +++ b/System/Jwt/JWTAuthTokenException.php @@ -0,0 +1,27 @@ + '']; - - 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); } } diff --git a/System/Jwt/JwtHelper.php b/System/Jwt/JwtHelper.php new file mode 100644 index 00000000..cc34b7be --- /dev/null +++ b/System/Jwt/JwtHelper.php @@ -0,0 +1,101 @@ + '']; + + 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; + } + + +}