初始化本地仓库
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\tool;
|
||||
|
||||
use app\service\admin\system\ConfigService as SystemConfigService;
|
||||
use app\attribute\EncryptResponse;
|
||||
use app\attribute\SkipApiEncrypt;
|
||||
use madong\swagger\helper\AnnotationHelper;
|
||||
use support\Container;
|
||||
use support\Context;
|
||||
use support\Request;
|
||||
use Webman\Http\Response;
|
||||
|
||||
class ApiEncryptService
|
||||
{
|
||||
public const CONTEXT_AES_KEY = 'api_encrypt.aes_key';
|
||||
public const CONTEXT_AES_IV = 'api_encrypt.aes_iv';
|
||||
public const CONTEXT_ACTIVE = 'api_encrypt.active';
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (bool) $this->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'] ?? ''));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user