From 708b0d2454975d01ecb70abe1f5f546875daf94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mr=C2=B7x?= Date: Tue, 1 Sep 2020 13:36:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- system/Abstracts/BaseApplication.php | 3 + system/Abstracts/Config.php | 2 +- system/Exception/AuthException.php | 10 + system/Jwt/Jwt.php | 437 +++++++++++++++++++++++++++ 4 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 system/Exception/AuthException.php create mode 100644 system/Jwt/Jwt.php diff --git a/system/Abstracts/BaseApplication.php b/system/Abstracts/BaseApplication.php index 09821ad1..256c43a8 100644 --- a/system/Abstracts/BaseApplication.php +++ b/system/Abstracts/BaseApplication.php @@ -22,6 +22,7 @@ use Snowflake\Error\ErrorHandler; use Snowflake\Error\Logger; use Snowflake\Exception\ComponentException; use Snowflake\Exception\InitException; +use Snowflake\Jwt\Jwt; use Snowflake\Pool\Connection; use Snowflake\Pool\RedisClient; use Snowflake\Processes; @@ -43,6 +44,7 @@ use Database\DatabasesProviders; * @property DatabasesProviders $db * @property Connection $connections * @property Logger $logger + * @property Jwt $jwt */ abstract class BaseApplication extends Service { @@ -245,6 +247,7 @@ abstract class BaseApplication extends Service 'logger' => ['class' => Logger::class], 'router' => ['class' => Router::class], 'redis' => ['class' => Redis::class], + 'jwt' => ['class' => Jwt::class], ]); } } diff --git a/system/Abstracts/Config.php b/system/Abstracts/Config.php index 967ec62d..02a4c512 100644 --- a/system/Abstracts/Config.php +++ b/system/Abstracts/Config.php @@ -28,7 +28,7 @@ class Config extends Component * @param $key * @param bool $try * @param mixed $default - * @return null + * @return mixed * @throws */ public static function get($key, $try = FALSE, $default = null) diff --git a/system/Exception/AuthException.php b/system/Exception/AuthException.php new file mode 100644 index 00000000..adefeaf7 --- /dev/null +++ b/system/Exception/AuthException.php @@ -0,0 +1,10 @@ + '']; + + private $timeout = 7200; + + private $public = '-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6BuML3gtLGde7QKNuNST +UCB9gdHC7XIpOc7Wx2I64Esj3UxWHTgp3URj0ge8zpy7A3FfBdppR7d1nwoD6Xad +jqfjEWpTy4WwGYsOfH0tFl3wAmse0lebF4NFsS9pzrikQT6c9qsVm88pCjvg4i5t +WhTMEnpTFDYoDR0KXlLXltQMudBBUHFaVwP0wKJ/cGX7R1Mrv35K4MXwQFOuGZkP +hsp2rO9x5LjtSKIXbexy7WhUu6QMjD/XzgsXr9UF+ExYmBGXRVWgNFLMkiaCZ2Uz +WlQhpQrA5/wKd76dCzjvqw9M32OiZl2lCKT73cV8GUvt7BNsM1SiPhqfY7nhO6y3 +cwIDAQAB +-----END PUBLIC KEY-----'; + + private $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 Jwt */ + private static $instance; + + /** + * @param string $publicKey + */ + public function setPublic(string $publicKey) + { + $this->public = $publicKey; + } + + /** + * @param $timeout + */ + public function setTimeout(int $timeout) + { + $this->timeout = $timeout; + } + + /** + * @param string $privateKey + */ + public function setPrivate(string $privateKey) + { + $this->private = $privateKey; + } + + /** + * @return Jwt + */ + private static function getInstance() + { + if (!(static::$instance instanceof Jwt)) { + static::$instance = new Jwt(); + } + return static::$instance; + } + + /** + * @param int|string $unionId + * @param $headers + * + * @return array + * @throws Exception + */ + public function create($unionId, $headers = []) + { + $this->user = $unionId; + $this->config['time'] = time(); + if (empty($headers)) { + $headers = request()->headers->getHeaders(); + } else if ($headers instanceof HttpHeaders) { + $headers = $headers->getHeaders(); + } + + $this->data = $headers; + if (empty($unionId)) { + throw new Exception('您还未登录或已登录超时'); + } + $source = $header['source'] ?? 'browser'; + if (empty($source) || !in_array($source, $this->source)) { + throw new Exception('未知的登录设备'); + } + return $this->createEncrypt($unionId); + } + + /** + * @param $unionId + * @return array + * @throws Exception + * 对相关信息进行加密 + */ + private function createEncrypt($unionId) + { + $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); + + 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, $update = FALSE) + { + 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($val); + } + $param['sign'] = md5(implode(':', $_tmp)); + if ($update) { + $this->setCache($param); + } + return $param; + } + + /** + * @param array $headers + * @return array + * @throws Exception + */ + public function refresh($headers = []) + { + $this->data = $headers; + if (!openssl_public_decrypt(base64_decode($headers['refresh']), $data, $this->public)) { + throw new Exception('信息解码失败.'); + } + + $this->user = $data['user']; + + if (!$this->getRedis()->exists('refresh:' . $this->user)) { + throw new Exception('refresh data error.'); + } + + $this->getRedis()->del('refresh:' . $this->user); + + return $this->create($this->user, $headers); + } + + /** + * @param $param + * + * @return mixed + */ + private function initialize(array $param) + { + $_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 + * + * @return string + * @throws Exception + */ + private function authKey($_source, $token) + { + $source = $this->getSource(); + if (!empty($_source)) $source = $_source; + if (empty($source)) { + throw new Exception("未知的登陆设备"); + } + return 'Tmp_Token:' . strtoupper($source) . ':' . $token; + } + + /** + * @return string + */ + public function getSource() + { + return $this->data['source'] ?? 'browser'; + } + + /** + * @param int $user + * @param array $param + * @param null $requestTime + * + * @return string + */ + private function token($user, $param = [], $requestTime = NULL) + { + $str = ''; + + $_user = str_split(md5($user . md5($user))); + ksort($_user); + foreach ($_user as $key => $val) { + $str .= md5(sha1($key . $val . 'www.xshucai.com')); + } + foreach ($param as $key => $val) { + $str .= md5($str . sha1($key . md5($val))); + } + $str .= sha1(base64_encode($requestTime)); + return $this->preg(md5($str . $user)); + } + + /** + * @param string $str + * + * @return mixed + * 将字符串替换成指定格式 + */ + private function preg($str) + { + $preg = '/(\w{10})(\w{3})(\w{4})(\w{9})(\w{6})/'; + return preg_replace($preg, '$1-$2-$3-$4-$5', $str); + } + + /** + * @param int $user + * @return string[] + * @throws + */ + public function clear($user) + { + $this->user = $user; + $redis = $this->getRedis(); + $refresh = $redis->get('refresh:' . $this->user); + 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 + * @return bool + * @throws + */ + public function check($data, $user) + { + $this->data = $data; + $this->user = $user; + + if (empty($this->user)) return FALSE; + $cache = $this->getUserModel(); + if (empty($cache)) { + return FALSE; + } + + $merge = $this->assembly(array_merge($cache, [ + 'token' => $data['token'], + ])); + $check = array_diff_assoc($this->initialize($cache), $merge); + return !((bool)count($check)); + } + + /** + * @return mixed + * @throws + */ + public function getCurrentOnlineUser() + { + $this->data = request()->headers->getHeaders(); + $model = $this->getUserModel(); + if (empty($model)) { + throw new AuthException('授权信息已过期!'); + } + if (!isset($model['user'])) { + throw new AuthException('授权信息错误!'); + } + if (!$this->check($this->data, $model['user'])) { + throw new AuthException('授权信息不合法!'); + } + $this->expireRefresh(); + + return $model['user']; + } + + /** + * @param array $header + * @throws Exception + */ + public static function checkAuth(array $header = []) + { + $instance = static::getInstance(); + if (empty($header)) { + $header = request()->headers->getHeaders(); + } + + $instance->data = $header; + $model = $instance->getUserModel(); + if (empty($model) || !isset($model['user'])) { + return false; + } + + if (!$instance->check($header, $model['user'])) { + return false; + } + $instance->expireRefresh(); + return $model['user']; + } + + /** + * @throws Exception + */ + private function expireRefresh() + { + $key = $this->authKey($this->getSource(), $this->data['token']); + $this->getRedis()->expire($key, $this->timeout); + } + + /** + * @return bool|array + * @throws AuthException + * @throws Exception + */ + private function getUserModel() + { + if (!isset($this->data['token'])) { + throw new AuthException('暂无访问权限!'); + } + $key = $this->authKey($this->getSource(), $this->data['token']); + return $this->getRedis()->hGetAll($key); + } + + /** + * @return Redis + * @throws Exception + */ + private function getRedis() + { + return Snowflake::get()->getRedis(); + } + +}