From 80466766692aac1d3bdeaf601ef6904ca9052add Mon Sep 17 00:00:00 2001 From: ctexthuang Date: Mon, 15 Sep 2025 14:45:29 +0800 Subject: [PATCH] feat : admin aspect --- app/Aspect/AdminReturnAspect.php | 82 ++++++++ app/Constants/AdminCode.php | 9 +- app/Controller/Admin/AdminUserController.php | 7 +- app/Controller/Admin/LoginController.php | 2 +- .../{Token => Admin}/AdminTokenMiddleware.php | 3 +- app/Middleware/Admin/PermissionMiddleware.php | 107 ++++++++++ .../RefreshAdminTokenMiddleware.php | 3 +- app/Service/Admin/BaseAdminService.php | 2 + app/Trait/AdminUserTrait.php | 14 +- app/Trait/ClientIpTrait.php | 192 ++++++++++++++++++ app/Trait/ParserRouterTrait.php | 25 +++ 11 files changed, 430 insertions(+), 16 deletions(-) create mode 100644 app/Aspect/AdminReturnAspect.php rename app/Middleware/{Token => Admin}/AdminTokenMiddleware.php (90%) create mode 100644 app/Middleware/Admin/PermissionMiddleware.php rename app/Middleware/{Token => Admin}/RefreshAdminTokenMiddleware.php (96%) create mode 100644 app/Trait/ClientIpTrait.php create mode 100644 app/Trait/ParserRouterTrait.php diff --git a/app/Aspect/AdminReturnAspect.php b/app/Aspect/AdminReturnAspect.php new file mode 100644 index 0000000..56c100a --- /dev/null +++ b/app/Aspect/AdminReturnAspect.php @@ -0,0 +1,82 @@ +adminId = Context::get('current_admin_id',0); + } + + public function process(ProceedingJoinPoint $proceedingJoinPoint) + { + // 直接从方法参数获取请求数据 + $requestData = $proceedingJoinPoint->getArguments()[0] ?? []; + + // 执行原方法并获取返回值 + $responseData = $proceedingJoinPoint->process(); + + // 写日志 + $this->writeLog($requestData, $responseData); + + // 返回 + return $requestData; + } + + /** + * @param array $requestData + * @param array $responseData + * @return void + */ + private function writeLog(array $requestData = [], array $responseData = []): void + { + Coroutine::create(function () use ($requestData, $responseData) { + $context = [ + 'user_id' => $this->adminId, + 'method' => $this->request->getMethod(), + 'router' => $this->request->getUri(), + 'ip' => $this->getClientIp(), + 'request_data' => $requestData, + 'response_data' => $responseData, + ]; + + $this->logger->request()->info('admin_request_log', $context); + }); + } +} diff --git a/app/Constants/AdminCode.php b/app/Constants/AdminCode.php index ae10a79..b463bc9 100644 --- a/app/Constants/AdminCode.php +++ b/app/Constants/AdminCode.php @@ -11,9 +11,10 @@ use Hyperf\Constants\EnumConstantsTrait; #[Constants] final class AdminCode extends ResultCode { - #[Message("登录失败")] - public const int LOGIN_ERROR = 10001; + #[Message("账号已禁用")] + public const int DISABLED = 423; + + #[Message("暂无权限")] + public const int FORBIDDEN = 403; - #[Message("验证已过期")] - public const int LOGIN_TOKEN_ERROR = 10002; } diff --git a/app/Controller/Admin/AdminUserController.php b/app/Controller/Admin/AdminUserController.php index e3b55f5..8848cc4 100644 --- a/app/Controller/Admin/AdminUserController.php +++ b/app/Controller/Admin/AdminUserController.php @@ -6,18 +6,19 @@ namespace App\Controller\Admin; use App\Annotation\Permission; use App\Annotation\ResponseFormat; -use App\Middleware\Token\AdminTokenMiddleware; +use App\Middleware\Admin\AdminTokenMiddleware; +use App\Middleware\Admin\PermissionMiddleware; use App\Service\Admin\AdminUser\UserService; use Hyperf\Di\Annotation\Inject; use Hyperf\HttpServer\Annotation\Controller; use Hyperf\HttpServer\Annotation\Middleware; use Hyperf\HttpServer\Annotation\RequestMapping; -use Hyperf\Validation\Annotation\Scene; #[Controller(prefix: "admin/adminUser")] #[ResponseFormat('admin')] -#[Middleware(AdminTokenMiddleware::class)] +#[Middleware(middleware: AdminTokenMiddleware::class, priority: 100)] +#[Middleware(middleware: PermissionMiddleware::class, priority: 99)] class AdminUserController { /** diff --git a/app/Controller/Admin/LoginController.php b/app/Controller/Admin/LoginController.php index 159d191..f5de2fd 100644 --- a/app/Controller/Admin/LoginController.php +++ b/app/Controller/Admin/LoginController.php @@ -6,7 +6,7 @@ namespace App\Controller\Admin; use App\Annotation\ResponseFormat; use App\Controller\AbstractController; -use App\Middleware\Token\RefreshAdminTokenMiddleware; +use App\Middleware\Admin\RefreshAdminTokenMiddleware; use App\Request\Admin\LoginRequest; use App\Service\Admin\Login\LoginService; use App\Service\Admin\Login\RefreshService; diff --git a/app/Middleware/Token/AdminTokenMiddleware.php b/app/Middleware/Admin/AdminTokenMiddleware.php similarity index 90% rename from app/Middleware/Token/AdminTokenMiddleware.php rename to app/Middleware/Admin/AdminTokenMiddleware.php index bb645c6..18806ac 100644 --- a/app/Middleware/Token/AdminTokenMiddleware.php +++ b/app/Middleware/Admin/AdminTokenMiddleware.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace App\Middleware\Token; +namespace App\Middleware\Admin; use App\Constants\ResultCode; use App\Exception\ErrException; use App\Interface\JwtInterface; +use App\Middleware\Token\AbstractTokenMiddleware; use Lcobucci\JWT\Token\RegisteredClaims; use Lcobucci\JWT\UnencryptedToken; use function Hyperf\Support\env; diff --git a/app/Middleware/Admin/PermissionMiddleware.php b/app/Middleware/Admin/PermissionMiddleware.php new file mode 100644 index 0000000..1cb8dec --- /dev/null +++ b/app/Middleware/Admin/PermissionMiddleware.php @@ -0,0 +1,107 @@ +getToken()?->claims()?->get(RegisteredClaims::ID) ?? 0; + if ($adminId <= 0) throw new ErrException('账户不存在'); + + $this->adminUserInfo = $this->getAdminUserInfo($adminId); + if ($this->adminUserInfo->status == AdminUserStatusCode::DISABLE) throw new ErrException('账号已禁用',AdminCode::DISABLED); + + // 超级管理员提前下场 不用判断权限 + if ($this->adminUserInfo->isSuperAdmin()) return $handler->handle($request); + + $this->check($request->getAttribute(Dispatched::class)); + + return $handler->handle($request); + } + + /** + * @param Dispatched $dispatched + * @return bool + */ + private function check(Dispatched $dispatched): bool + { + $parseResult = $this->parse($dispatched->handler->callback); + if (! $parseResult) return true; + + [$controller, $method] = $parseResult; + $annotations = AnnotationCollector::getClassMethodAnnotation($controller, $method); + $classAnnotation = AnnotationCollector::getClassAnnotation($controller, Permission::class); + /** + * @var Permission[] $permissions + */ + $classAnnotation && $permissions[] = $classAnnotation; + $methodPermission = Arr::get($annotations, Permission::class); + $methodPermission && $permissions[] = $methodPermission; + + foreach ($permissions as $permission) { + $this->handlePermission($permission); + } + + return true; + } + + /** + * @param Permission $permission + * @return void + */ + private function handlePermission(Permission $permission): void + { + $operation = $permission->getOperation(); + $codes = $permission->getCode(); + + foreach ($codes as $code) { + $isMenu = $this->adminUserInfo->hasPermission($code); + + if ($operation === Permission::OPERATION_AND && !$isMenu) throw new ErrException('暂无权限',AdminCode::FORBIDDEN); + + if ($operation === Permission::OPERATION_OR && $isMenu) return; + } + + if ($operation === Permission::OPERATION_OR) throw new ErrException('暂无权限',AdminCode::FORBIDDEN); + } +} diff --git a/app/Middleware/Token/RefreshAdminTokenMiddleware.php b/app/Middleware/Admin/RefreshAdminTokenMiddleware.php similarity index 96% rename from app/Middleware/Token/RefreshAdminTokenMiddleware.php rename to app/Middleware/Admin/RefreshAdminTokenMiddleware.php index 5265ec2..6832dc6 100644 --- a/app/Middleware/Token/RefreshAdminTokenMiddleware.php +++ b/app/Middleware/Admin/RefreshAdminTokenMiddleware.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace App\Middleware\Token; +namespace App\Middleware\Admin; use App\Constants\ResultCode; use App\Exception\ErrException; use App\Interface\JwtInterface; +use App\Middleware\Token\AbstractTokenMiddleware; use Lcobucci\JWT\Token\RegisteredClaims; use Lcobucci\JWT\UnencryptedToken; use Lcobucci\JWT\Validation\RequiredConstraintsViolated; diff --git a/app/Service/Admin/BaseAdminService.php b/app/Service/Admin/BaseAdminService.php index 1efb7e3..dee815c 100644 --- a/app/Service/Admin/BaseAdminService.php +++ b/app/Service/Admin/BaseAdminService.php @@ -12,6 +12,7 @@ namespace App\Service\Admin; use App\Lib\Jwt\RequestScopedTokenTrait; use App\Lib\Return\AdminReturn; +use Hyperf\Context\Context; use Hyperf\Di\Annotation\Inject; use Hyperf\HttpServer\Contract\RequestInterface; use Lcobucci\JWT\Token\RegisteredClaims; @@ -46,6 +47,7 @@ abstract class BaseAdminService public function __construct() { $this->adminId = (int) $this->getToken()?->claims()?->get(RegisteredClaims::ID) ?? 0; + if ($this->adminId > 0) Context::set('current_admin_id', $this->adminId); } /** diff --git a/app/Trait/AdminUserTrait.php b/app/Trait/AdminUserTrait.php index 8af5267..6e9a697 100644 --- a/app/Trait/AdminUserTrait.php +++ b/app/Trait/AdminUserTrait.php @@ -4,8 +4,10 @@ namespace App\Trait; use App\Cache\Redis\RedisCache; use App\Cache\Redis\RedisKey; +use App\Model\AdminUser; use App\Repository\AdminUserRepository; use Hyperf\Context\Context; +use Hyperf\Database\Model\Model; use Hyperf\Di\Annotation\Inject; trait AdminUserTrait @@ -24,9 +26,9 @@ trait AdminUserTrait /** * @param int $adminId - * @return array|null + * @return AdminUser|Model|mixed|string|null */ - public function getAdminUserInfo(int $adminId): array|null + public function getAdminUserInfo(int $adminId): mixed { $key = RedisKey::getAdminUserInfoKey($adminId); if (Context::has($key)) { @@ -34,16 +36,16 @@ trait AdminUserTrait } if ($this->redis->with()->exists($key)) { - $userInfo = $this->redis->with()->get($key); + $userInfo = unserialize($this->redis->with()->get($key)); Context::set($key,$userInfo); - return json_decode($userInfo,true); + return $userInfo; } - $userInfo = $this->adminUserRepository->findById($adminId)?->toArray() ?? null; + $userInfo = $this->adminUserRepository->findById($adminId) ?? null; if (!$userInfo) return null; Context::set($key, $userInfo); - $this->redis->with()->set($key, json_encode($userInfo), 3600); + $this->redis->with()->set($key, serialize($userInfo), 3600); return $userInfo; } diff --git a/app/Trait/ClientIpTrait.php b/app/Trait/ClientIpTrait.php new file mode 100644 index 0000000..1f9c84d --- /dev/null +++ b/app/Trait/ClientIpTrait.php @@ -0,0 +1,192 @@ +request->server('remote_addr'); + + if (! $this->isFromTrustedProxy()) return [$ip]; + + return $this->getTrustedValues(ClientIpRequestConstant::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip]; + } + + /** + * 判断此请求是否来自受信任的代理。这可以用于确定是否信任 + * @return bool + */ + private function isFromTrustedProxy(): bool + { + return ( + self::$trustedProxies && + IpUtils::checkIp($this->request->server('remote_addr',''),self::$trustedProxies) || + self::isTrustedRemoteAddr() + ); + } + + /** + * @param int $type + * @param string|null $ip + * @return array|mixed|null[]|string[] + */ + private function getTrustedValues(int $type, ?string $ip = null): mixed + { + $cacheKey = $type . "\0" . ((self::$trustedHeaderSet & $type) ? $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[$type]) : ''); + $cacheKey .= "\0" . $ip . "\0" . $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED]); + + if (isset($this->trustedValuesCache[$cacheKey])) return $this->trustedValuesCache[$cacheKey]; + + $clientValues = []; + $forwardedValues = []; + + if ( + (self::$trustedHeaderSet & $type) && + $this->request->hasHeader(ClientIpRequestConstant::TRUSTED_HEADERS[$type]) + ) { + foreach (explode(',', $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[$type])) as $value) { + $clientValues[] = ($type === ClientIpRequestConstant::HEADER_X_FORWARDED_PORT ? '0.0.0.0' : trim($value)); + } + } + + if ( + (self::$trustedHeaderSet & ClientIpRequestConstant::HEADER_FORWARDED) && + (isset(ClientIpRequestConstant::FORWARDED_PARAMS[$type])) && + $this->request->hasHeader(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED]) + ) { + $forward = $this->request->getHeaderLine(ClientIpRequestConstant::TRUSTED_HEADERS[ClientIpRequestConstant::HEADER_FORWARDED]); + $parts = HeaderUtils::split($forward, ',;='); + $param = ClientIpRequestConstant::FORWARDED_PARAMS[$type]; + + foreach ($parts as $subParts) { + if (($value = HeaderUtils::combine($subParts)[$param] ?? null) === null) continue; + + if ($type === ClientIpRequestConstant::HEADER_X_FORWARDED_PORT) { + if ( + str_ends_with($value, ']') || + ($value = mb_strrchr($value, ':')) === false + ) $value = $this->isSecure() ? ':443' : ':80'; + + $value = '0.0.0.0' . $value; + } + + $forwardedValues[] = $value; + } + } + + if (null !== $ip) { + $clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip); + $forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip); + } + + if ($forwardedValues === $clientValues || !$clientValues) return $this->trustedValuesCache[$cacheKey] = $forwardedValues; + + if (!$forwardedValues) return $this->trustedValuesCache[$cacheKey] = $clientValues; + + if (!$this->isForwardedValid) return (($ip = $this->trustedValuesCache[$cacheKey]) !== null) ? ['0.0.0.0',$ip] : []; + + $this->isForwardedValid = false; + throw new ErrException('转发报头无效,请检查服务器配置。'); + } + + /** + * @return bool + */ + private function isSecure(): bool + { + return $this->request->getHeaderLine(ClientIpRequestConstant::HEADER_X_FORWARDED_PROTO) === 'https' + || ($this->request->getServerParams()['https'] ?? '') === 'on'; + } + + /** + * @param array $clientIps + * @param string $ip + * @return array|null[]|string[] + */ + private function normalizeAndFilterClientIps(array $clientIps, string $ip): array + { + if (!$clientIps) return []; + + $clientIps[] = $ip; + $firstTrustedIp = null; + + foreach ($clientIps as $key => $clientIp) { + if (mb_strpos($clientIp, '.')) { + // ipv4 + $i = mb_strpos($clientIp, '.'); + + if ($i) $clientIps[$key] = $clientIp = mb_substr($clientIp, 0, $i); + } elseif (str_starts_with($clientIp, '[')) { + // ipv6 + $i = mb_strpos($clientIp, ']',1); + $clientIps[$key] = $clientIp = mb_substr($clientIp, 1, $i - 1); + } + + if (!filter_var($clientIp, FILTER_VALIDATE_IP)) { + unset($clientIps[$key]); + continue; + } + + if (IpUtils::checkIp($clientIp, self::$trustedProxies)) { + unset($clientIps[$key]); + + $firstTrustedIp ??= $clientIp; + } + } + + return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp]; + } +} \ No newline at end of file diff --git a/app/Trait/ParserRouterTrait.php b/app/Trait/ParserRouterTrait.php new file mode 100644 index 0000000..51a66a0 --- /dev/null +++ b/app/Trait/ParserRouterTrait.php @@ -0,0 +1,25 @@ +