getConfigValue('enabled', false); } public function getScope(): string { return (string) $this->getConfigValue('scope', 'off'); } public function getKeyVersion(): string { return (string) $this->getConfigValue('key_version', 'v1'); } public function getAlgorithm(): string { return (string) $this->getConfigValue('algorithm', 'rsa-aes'); } public function getAesMode(): string { return strtoupper((string) $this->getConfigValue('aes_mode', 'AES-256-CBC')); } public function getPublicKey(): string { $path = (string) $this->getConfigValue('public_key_path', ''); return $path !== '' && is_file($path) ? (string) file_get_contents($path) : ''; } public function getPrivateKey(): string { $path = (string) $this->getConfigValue('private_key_path', ''); return $path !== '' && is_file($path) ? (string) file_get_contents($path) : ''; } public function isJsonRequest(Request $request): bool { $contentType = strtolower((string) $request->header('content-type', '')); return str_contains($contentType, 'application/json'); } public function isJsonResponse(Response $response): bool { $contentType = strtolower((string) ($response->getHeader('Content-Type') ?: '')); return str_contains($contentType, 'application/json'); } public function shouldExcludePath(string $path): bool { $path = '/' . ltrim($path, '/'); $excludePaths = (array) $this->getConfigValue('exclude_paths', []); foreach ($excludePaths as $excludePath) { $excludePath = '/' . ltrim((string) $excludePath, '/'); if ($excludePath === $path || str_starts_with($path, $excludePath . '/')) { return true; } } return false; } public function shouldEncrypt(Request $request): bool { if (!$this->isEnabled()) { return false; } $scope = $this->getScope(); if ($scope === 'off') { return false; } $path = $request->path(); if ($this->shouldExcludePath($path)) { return false; } $controllerClass = $request->controller; $action = $request->action; if (!$controllerClass || !$action) { return false; } if (AnnotationHelper::hasMethodAnnotation($controllerClass, $action, SkipApiEncrypt::class) || AnnotationHelper::hasClassAnnotation($controllerClass, SkipApiEncrypt::class)) { return false; } if ($scope === 'all') { return true; } $encryptedPaths = (array) $this->getConfigValue('encrypted_paths', []); if ($this->matchesEncryptedPath($request->path(), $encryptedPaths)) { return true; } return AnnotationHelper::hasMethodAnnotation($controllerClass, $action, EncryptResponse::class) || AnnotationHelper::hasClassAnnotation($controllerClass, EncryptResponse::class); } public function getEncryptedPaths(): array { $paths = (array) $this->getConfigValue('encrypted_paths', []); return array_values(array_filter(array_map(static function ($path) { return '/' . ltrim((string) $path, '/'); }, $paths))); } public function isEncryptedRequest(Request $request): bool { return (string) $request->header('x-api-encrypt', '') === '1'; } public function decryptRequest(Request $request): array { $encryptedKey = trim((string) $request->header('x-api-encrypt-key', '')); $encryptedIv = trim((string) $request->header('x-api-encrypt-iv', '')); if ($encryptedKey === '' || $encryptedIv === '') { throw new \RuntimeException('缺少加密请求头'); } $privateKey = $this->getPrivateKey(); if ($privateKey === '') { throw new \RuntimeException('接口加密私钥未配置'); } $aesKey = RSAService::decryptOaep($encryptedKey, $privateKey); $iv = RSAService::decryptOaep($encryptedIv, $privateKey); if ($aesKey === '' || $iv === '') { throw new \RuntimeException('会话密钥解密失败'); } Context::set(self::CONTEXT_AES_KEY, $aesKey); Context::set(self::CONTEXT_AES_IV, $iv); Context::set(self::CONTEXT_ACTIVE, true); $body = trim($request->rawBody()); if ($body === '') { return []; } $payload = json_decode($body, true); if (!is_array($payload)) { throw new \RuntimeException('加密请求体格式错误'); } if (($payload['encrypted'] ?? false) !== true) { return $payload; } $cipherText = (string) ($payload['data'] ?? ''); if ($cipherText === '') { throw new \RuntimeException('缺少加密数据'); } $plain = $this->aesDecrypt($cipherText, $aesKey, $iv); $data = json_decode($plain, true); if (!is_array($data)) { throw new \RuntimeException('解密后的请求数据格式错误'); } return $data; } public function encryptResponseBody(string $body): string { $aesKey = (string) Context::get(self::CONTEXT_AES_KEY, ''); $iv = (string) Context::get(self::CONTEXT_AES_IV, ''); if ($aesKey === '' || $iv === '') { throw new \RuntimeException('缺少响应加密会话信息'); } $cipherText = $this->aesEncrypt($body, $aesKey, $iv); return json_encode([ 'encrypted' => true, 'algorithm' => $this->getAlgorithm(), 'key_version' => $this->getKeyVersion(), 'data' => $cipherText, ], JSON_UNESCAPED_UNICODE); } public function applyDecryptedPayload(Request $request, array $data): void { if ($data === []) { return; } if ($request->method() === 'GET') { $request->setGet(array_merge($request->get(), $data)); return; } $request->setPost($data); } public function hasActiveSessionKey(): bool { return (bool) Context::get(self::CONTEXT_ACTIVE, false); } public function generateAesKey(): string { return random_bytes(32); } public function generateIv(): string { return random_bytes(16); } public function aesEncrypt(string $plainText, string $aesKey, string $iv): string { $cipher = openssl_encrypt($plainText, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv); if ($cipher === false) { throw new \RuntimeException('AES 加密失败'); } return base64_encode($cipher); } public function aesDecrypt(string $cipherText, string $aesKey, string $iv): string { $decoded = base64_decode($cipherText, true); if ($decoded === false) { throw new \RuntimeException('AES 密文解码失败'); } $plain = openssl_decrypt($decoded, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv); if ($plain === false) { throw new \RuntimeException('AES 解密失败'); } return $plain; } public function generateKeyPair(int $bits = 2048): array { return RSAService::generateKeys($bits); } protected function getConfigValue(string $key, mixed $default = null): mixed { $fileConfig = config('api_encrypt', []); $dbConfig = $this->getDatabaseConfig(); if (array_key_exists($key, $dbConfig) && $dbConfig[$key] !== null && $dbConfig[$key] !== '') { return $dbConfig[$key]; } return $fileConfig[$key] ?? $default; } protected function getDatabaseConfig(): array { try { /** @var SystemConfigService $service */ $service = Container::get(SystemConfigService::class); $config = $service->config('api_encrypt_setting', [], ['group_code' => 'system']); return is_array($config) ? $config : []; } catch (\Throwable) { return []; } } protected function matchesEncryptedPath(string $path, array $encryptedPaths): bool { $path = '/' . ltrim($path, '/'); foreach ($encryptedPaths as $encryptedPath) { $encryptedPath = '/' . ltrim((string) $encryptedPath, '/'); if ($encryptedPath === $path || str_starts_with($path, $encryptedPath . '/')) { return true; } } return false; } public function persistKeyPair(array $keys): void { $publicPath = (string) $this->getConfigValue('public_key_path', ''); $privatePath = (string) $this->getConfigValue('private_key_path', ''); if ($publicPath === '' || $privatePath === '') { throw new \RuntimeException('接口加密密钥路径未配置'); } $publicDir = dirname($publicPath); $privateDir = dirname($privatePath); if (!is_dir($publicDir)) { mkdir($publicDir, 0777, true); } if (!is_dir($privateDir)) { mkdir($privateDir, 0777, true); } file_put_contents($publicPath, (string) ($keys['public'] ?? '')); file_put_contents($privatePath, (string) ($keys['private'] ?? '')); } }