diff --git a/app/Constants/Admin/UserCode.php b/app/Constants/Admin/UserCode.php new file mode 100644 index 0000000..88d70aa --- /dev/null +++ b/app/Constants/Admin/UserCode.php @@ -0,0 +1,21 @@ +return->error($throwable->getMessage(),[],$throwable->getCode()); + + // 阻止异常冒泡 + $this->stopPropagation(); + + return $response->withHeader("Content-Type", "application/json") + ->withStatus(200) + ->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE))); + } + + // 交给下一个异常处理器 + return $response; + } + + public function isValid(Throwable $throwable): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/Exception/Handler/ValidationDataExceptionHandler.php b/app/Exception/Handler/ValidationDataExceptionHandler.php new file mode 100644 index 0000000..dfdcffb --- /dev/null +++ b/app/Exception/Handler/ValidationDataExceptionHandler.php @@ -0,0 +1,63 @@ +path(); + + $urlArr = explode('/',$url); + + if ($urlArr[0] == 'admin') { + $result = $this->adminReturn->error($throwable->validator->errors()->first()); + }else{ + //todo api + $result = $this->adminReturn->error($throwable->validator->errors()->first()); + } + + // 阻止异常冒泡 + $this->stopPropagation(); + + if (!is_array($result)){ + return $response->withHeader("Content-Type", "application/json") + ->withStatus(200) + ->withBody(new SwooleStream($result)); + } + + return $response->withHeader("Content-Type", "application/json") + ->withStatus(200) + ->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE))); + } + + // 交给下一个异常处理器 + return $response; + } + + public function isValid(Throwable $throwable): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/Extend/SystemUtil.php b/app/Extend/SystemUtil.php new file mode 100644 index 0000000..681c662 --- /dev/null +++ b/app/Extend/SystemUtil.php @@ -0,0 +1,60 @@ +getHeaderLine('x-forwarded-for'); + if (self::verifyIp($ip_addr)) { + return $ip_addr; + } + + $ip_addr = $request->getHeaderLine('remote-host'); + if (self::verifyIp($ip_addr)) { + return $ip_addr; + } + + $ip_addr = $request->getHeaderLine('x-real-ip'); + if (self::verifyIp($ip_addr)) { + return $ip_addr; + } + + $ip_addr = $request->getServerParams()['remote_addr'] ?? '0.0.0.0'; + if (self::verifyIp($ip_addr)) { + return $ip_addr; + } + + return '0.0.0.0'; + } + + /** + * 验证ip + * @param $realIp + * @return mixed + */ + static function verifyIp($realIp): mixed + { + return filter_var($realIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + } +} \ No newline at end of file diff --git a/app/Lib/AdminReturn.php b/app/Lib/AdminReturn.php index 03b6d34..8494faf 100644 --- a/app/Lib/AdminReturn.php +++ b/app/Lib/AdminReturn.php @@ -15,7 +15,7 @@ class AdminReturn * @param array $debug * @return array */ - public static function success(string $msg = 'success', array $data = [], int $code = ReturnCode::SUCCESS, array $debug = []): array + public function success(string $msg = 'success', array $data = [], int $code = ReturnCode::SUCCESS, array $debug = []): array { $res = [ 'code' => $code, @@ -34,7 +34,7 @@ class AdminReturn * @param array $debug * @return array */ - public static function error(string $msg = 'error', array $data = [], int $code = ReturnCode::ERROR, array $debug = []): array + public function error(string $msg = 'error', int $code = ReturnCode::ERROR, array $data = [], array $debug = []): array { $res = [ 'code' => $code, diff --git a/app/Lib/Crypto/AdminPasswordCrypto.php b/app/Lib/Crypto/AdminPasswordCrypto.php new file mode 100644 index 0000000..6f90bff --- /dev/null +++ b/app/Lib/Crypto/AdminPasswordCrypto.php @@ -0,0 +1,42 @@ +salt . hash("sha256", $this->salt . $this->data)); + } catch (Exception) { + return ''; + } + } + + /** + * 解密 不需要解密 + * @return string + */ + public function decrypt(): string + { + return ''; + } +} \ No newline at end of file diff --git a/app/Lib/Crypto/ApiCrypto.php b/app/Lib/Crypto/ApiCrypto.php new file mode 100644 index 0000000..27b8207 --- /dev/null +++ b/app/Lib/Crypto/ApiCrypto.php @@ -0,0 +1,76 @@ +key = config('system.api_return_key'); + } + + /** + * api加密接口 + * @return string + */ + public function encrypt(): string + { + try { + //设置偏移量 + $iv = substr(md5($this->data), 0, 16); + //使用 openssl 加密数据 + $encrypted = openssl_encrypt($this->data,'AES-128-CBC',$this->key,OPENSSL_RAW_DATA,$iv); + return $iv.'|'.base64_encode($encrypted); + } catch (Exception) { + return ''; + } + } + + /** + * api解密接口 + * @return string + */ + public function decrypt(): string + { + try { + $array = explode('|',$this->data); + //获取偏移量 + $iv = $array[0]; + //获取加密数据 + $encrypted = base64_decode($array[1]); + //使用 openssl 解密数据 并返回 + return openssl_decrypt($encrypted, 'AES-128-CBC',$this->key,OPENSSL_RAW_DATA,$iv); + } catch (Exception) { + return ''; + } + } +} \ No newline at end of file diff --git a/app/Lib/Crypto/CryptoFactory.php b/app/Lib/Crypto/CryptoFactory.php new file mode 100644 index 0000000..23f0df3 --- /dev/null +++ b/app/Lib/Crypto/CryptoFactory.php @@ -0,0 +1,57 @@ +cryptoInterface = $apiCrypto; + break; + case 'jwt': + $jwtCrypto = new JwtCrypto(); + $this->cryptoInterface = $jwtCrypto; + break; + case 'admin-password': + $adminCrypto = new AdminPasswordCrypto(); + $this->cryptoInterface = $adminCrypto; + $this->cryptoInterface->salt = $key; + break; + default: + throw new Exception('The encryption algorithm does not exist'); + } + + $this->cryptoInterface->data = $dataStr; + return $this->cryptoInterface; + } +} \ No newline at end of file diff --git a/app/Lib/Crypto/CryptoInterface.php b/app/Lib/Crypto/CryptoInterface.php new file mode 100644 index 0000000..6b73b99 --- /dev/null +++ b/app/Lib/Crypto/CryptoInterface.php @@ -0,0 +1,20 @@ +key = config('system.jwt_key'); + $this->expire = (int)config('system.jwt_expire'); + } + + /** + * jwt 加密 + * @return string + */ + public function encrypt(): string + { + try { + $time = time(); + $payload = [ + 'iat' => $time, //签发时间 + 'nbf' => $time, //(Not Before):某个时间点后才能访问,比如设置time+30,表示当前时间30秒后才能使用 + 'exp' => $time + $this->expire, + 'data' => json_decode($this->data,true), + ]; + + return JWT::encode($payload, $this->key,'HS256'); + } catch (Exception) { + return ''; + } + } + + /** + * jwt 解密 + * @return array + */ + public function decrypt(): array + { + try { + return (array)JWT::decode($this->data, new Key($this->key, 'HS256')); + } catch (Exception) { + return []; + } + } +} \ No newline at end of file diff --git a/app/Model/AdminUser.php b/app/Model/AdminUser.php new file mode 100644 index 0000000..bff2586 --- /dev/null +++ b/app/Model/AdminUser.php @@ -0,0 +1,56 @@ + 'integer', 'avatar' => 'integer', 'status' => 'integer', 'is_del' => 'integer', 'role_id' => 'integer']; + + const CREATED_AT = 'create_time'; + + const UPDATED_AT = null; + + /** + * @param $account + * @return Builder|\Hyperf\Database\Model\Model|null + */ + public function getAdminInfoByAccount($account): \Hyperf\Database\Model\Model|Builder|null + { + return $this->where('username', $account)->where('is_del',1)->first(); + } +} diff --git a/app/Service/Admin/BaseService.php b/app/Service/Admin/BaseService.php index 9304715..d8a1e1e 100644 --- a/app/Service/Admin/BaseService.php +++ b/app/Service/Admin/BaseService.php @@ -10,10 +10,27 @@ declare(strict_types=1); namespace App\Service\Admin; +use App\Lib\AdminReturn; use Hyperf\Context\Context; +use Hyperf\Di\Annotation\Inject; +use Hyperf\HttpServer\Contract\RequestInterface; abstract class BaseService { + /** + * 请求对象 + * @var RequestInterface + */ + #[Inject] + protected RequestInterface $request; + + /** + * 通用返回对象 + * @var AdminReturn $return + */ + #[Inject] + protected AdminReturn $return; + /** * 管理员id * @var int $adminId diff --git a/app/Service/Admin/User/LoginService.php b/app/Service/Admin/User/LoginService.php index 97ef803..d708043 100644 --- a/app/Service/Admin/User/LoginService.php +++ b/app/Service/Admin/User/LoginService.php @@ -10,13 +10,68 @@ declare(strict_types=1); namespace App\Service\Admin\User; +use App\Constants\Admin\UserCode; +use App\Constants\AdminCode; +use App\Exception\AdminException; +use App\Extend\SystemUtil; use App\Lib\AdminReturn; +use App\Lib\Crypto\CryptoFactory; +use App\Model\AdminUser; use App\Service\Admin\BaseService; +use App\Service\Common\AppMakeService; +use Exception; +use Hyperf\Di\Annotation\Inject; class LoginService extends BaseService { + + /** + * 注入管理员模型 + * @var AdminUser $adminUserModel + */ + #[Inject] + protected AdminUser $adminUserModel; + + /** + * 注入加密工厂 + * @var CryptoFactory $cryptoFactory + */ + #[Inject] + protected CryptoFactory $cryptoFactory; + + /** + * 后台登录 + * @return array + * @throws Exception + */ public function handle(): array { - return AdminReturn::success(); + $userInfo = $this->adminUserModel->getAdminInfoByAccount($this->request->input('account')); + if (!$userInfo) throw new AdminException('账号不存在'); + + if ($userInfo->status == UserCode::DISABLE) throw new AdminException(UserCode::getMessage($userInfo->status),AdminCode::LOGIN_ERROR); + + if ($this->cryptoFactory->cryptoClass('admin-password',$this->request->input('password'),$userInfo->salt) != $userInfo->password) throw new AdminException('密码错误!'); + + $userInfo->last_login_time = date('Y-m-d H:i:s'); + $userInfo->last_login_ip = SystemUtil::getClientIp(); + $userInfo->save(); + + if (!$userInfo->save()) throw new AdminException('登录失败'); + + $token = $this->cryptoFactory->cryptoClass('jwt',json_encode([ + 'id' => $userInfo->id, + 'role' => $userInfo->role_id, + ]))->encrypt(); + + return $this->return->success('success',[ + 'token' => $token, + 'info' => [ + 'admin_id' => $userInfo->id, + 'avatar' => $userInfo->avatar, + 'name' => $userInfo->chinese_name, + 'role_id' => $userInfo->role_id, + ] + ]); } } \ No newline at end of file diff --git a/app/Service/Common/AppMakeService.php b/app/Service/Common/AppMakeService.php new file mode 100644 index 0000000..3eea1a2 --- /dev/null +++ b/app/Service/Common/AppMakeService.php @@ -0,0 +1,46 @@ + [ 'http' => [ Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, + App\Exception\Handler\AdminExceptionHandler::class, + App\Exception\Handler\ValidationDataExceptionHandler::class, App\Exception\Handler\AppExceptionHandler::class, ], ], diff --git a/config/autoload/system.php b/config/autoload/system.php new file mode 100644 index 0000000..16503f0 --- /dev/null +++ b/config/autoload/system.php @@ -0,0 +1,20 @@ + env('API_RETURN_KEY','hhl@shenzhen'), + // jwt 加密 key + 'jwt_key' => env('JWT_KEY','hhl@shenzhen'), + // jwt 过期时间 + 'jwt_expire' => env('JWT_EXPIRE',86400 * 30), +]; \ No newline at end of file diff --git a/sync/database/admin.sql b/sync/database/admin.sql new file mode 100644 index 0000000..af284cc --- /dev/null +++ b/sync/database/admin.sql @@ -0,0 +1,25 @@ +/* + * Server Type : MySQL +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- 后台用户表 +DROP TABLE IF EXISTS `app_admin_user`; +CREATE TABLE `app_admin_user` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `username` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户密码', + `salt` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '盐值', + `avatar` int NOT NULL DEFAULT 0 COMMENT '用户头像', + `chinese_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '中文名', + `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '手机号', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '用户状态 1 正常 2 禁用', + `last_login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '最后登录IP', + `last_login_time` datetime NOT NULL COMMENT '最后登录时间', + `is_del` tinyint(1) NOT NULL DEFAULT '1' COMMENT '用户状态 1 正常 2 删除 涉及到后台操作日志表', + `role_id` tinyint(1) NOT NULL DEFAULT '0' COMMENT '角色', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='后台用户表'; \ No newline at end of file