From 223d57483c8226531f7afa07fb2c68b72edc29b6 Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 26 Sep 2025 15:52:45 +0800 Subject: [PATCH] Merge branch 'develop' # Conflicts: # app/controller/IndexController.php --- app/bootstrap/Audit.php | 31 ++++++++ app/common/audit/AuditBuffer.php | 77 +++++++++++++++++++ app/common/logic/AuthLogic.php | 1 + app/common/traits/Auditable.php | 48 ++++++++++++ app/controller/IndexController.php | 44 ++++++++++- app/middleware/Audit.php | 114 +++++++++++++++++++++++++++++ app/process/CheckWeb.php | 62 ++++++++++++++++ app/queue/redis/AuditLog.php | 72 ++++++++++++++++++ config/bootstrap.php | 3 + config/middleware.php | 3 +- 10 files changed, 450 insertions(+), 5 deletions(-) create mode 100644 app/bootstrap/Audit.php create mode 100644 app/common/audit/AuditBuffer.php create mode 100644 app/common/traits/Auditable.php create mode 100644 app/middleware/Audit.php create mode 100644 app/process/CheckWeb.php create mode 100644 app/queue/redis/AuditLog.php diff --git a/app/bootstrap/Audit.php b/app/bootstrap/Audit.php new file mode 100644 index 0000000..f94327c --- /dev/null +++ b/app/bootstrap/Audit.php @@ -0,0 +1,31 @@ +pushModelChange($model->getTable(), $model->getKey(), [], $model->getAttributes()); + }); + Model::updated(function ($model) { + $dirty = $model->getDirty(); + if (!$dirty) return; + $old = []; $new = []; + foreach ($dirty as $k=>$v) { + $old[$k] = $model->getOriginal($k); + $new[$k] = $model->getAttribute($k); + } + AuditBuffer::instance()->pushModelChange($model->getTable(), $model->getKey(), $old, $new); + }); + Model::deleted(function ($model) { + AuditBuffer::instance()->pushModelChange($model->getTable(), $model->getKey(), $model->getAttributes(), []); + }); + } + +} \ No newline at end of file diff --git a/app/common/audit/AuditBuffer.php b/app/common/audit/AuditBuffer.php new file mode 100644 index 0000000..bdb9974 --- /dev/null +++ b/app/common/audit/AuditBuffer.php @@ -0,0 +1,77 @@ +action = $action; + } + + public function setMeta(array $meta): void + { + $this->meta = $meta + $this->meta; + } + + /** 模型字段级变更推送(由 Trait 调用) */ + public function pushModelChange(string $scene, $objectId, array $old, array $new): void + { + $keys = array_unique(array_merge(array_keys($old), array_keys($new))); + foreach ($keys as $k) { + $ov = $old[$k] ?? null; + $nv = $new[$k] ?? null; + if ($ov === $nv) continue; + + [$ovMasked, $nvMasked] = $this->maskIfNeeded($k, $ov, $nv); + $this->changes[] = [ + 'scene' => $scene, + 'object_id' => $objectId, + 'field' => $k, + 'old' => $ovMasked, + 'new' => $nvMasked, + ]; + } + } + + private function maskIfNeeded(string $field, $old, $new): array + { + foreach ($this->maskFields as $m) { + if (stripos($field, $m) !== false) { + return [$old===null?null:'***', $new===null?null:'***']; + } + } + if (preg_match('/(mobile|phone|tel|idcard|id_card)/i', $field)) { + $mask = function ($v) { + if ($v === null) return null; + $s = (string)$v; + return mb_substr($s, 0, 3).'****'.mb_substr($s, -4); + }; + return [$mask($old), $mask($new)]; + } + return [$old, $new]; + } +} \ No newline at end of file diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php index c79b1d1..ee449b9 100644 --- a/app/common/logic/AuthLogic.php +++ b/app/common/logic/AuthLogic.php @@ -425,6 +425,7 @@ class AuthLogic 'exp' => $exp, 'typ' => 'access', 'uid' => $user->id, + 'uname' => $user->name, 'role_ids' => $role_ids, 'system_ids' => $system_ids ]; diff --git a/app/common/traits/Auditable.php b/app/common/traits/Auditable.php new file mode 100644 index 0000000..e66341a --- /dev/null +++ b/app/common/traits/Auditable.php @@ -0,0 +1,48 @@ +pushModelChange(self::scene($m), $m->getKey(), [], $m->getAttributes()); + }); + + static::updated(function (Model $m) { + if (self::isMuted($m)) return; + $dirty = $m->getDirty(); + if (!$dirty) return; + $old = []; $new = []; + foreach (array_keys($dirty) as $k) { + $old[$k] = $m->getOriginal($k); + $new[$k] = $m->getAttribute($k); + } + AuditBuffer::instance()->pushModelChange(self::scene($m), $m->getKey(), $old, $new); + }); + + static::deleted(function (Model $m) { + if (self::isMuted($m)) return; + AuditBuffer::instance()->pushModelChange(self::scene($m), $m->getKey(), $m->getAttributes(), []); + }); + } + + protected static function scene(Model $m): string + { + return property_exists($m, 'auditScene') && $m->auditScene ? $m->auditScene : $m->getTable(); + } + + protected static function isMuted(Model $m): bool + { + if (property_exists($m, 'auditDisabled') && $m->auditDisabled) return true; + return $m->getTable() === 'sys_op_log'; // 避免自记录 + } +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index fb87763..a72264c 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -10,6 +10,7 @@ use support\Request; use support\Db; use support\Cache; use support\Redis; +use GuzzleHttp\Client; class IndexController { @@ -101,11 +102,46 @@ class IndexController public function test(Request $request) { + $result = $this->tocurl('https://magicbridgeauto.com'); + if (preg_match('/magicbridgeauto/', $result['body'])) { + echo "未被篡改"; + flg("【magicbridgeauto】未被篡改"); + } else { + echo "已被篡改"; + flg("【magicbridgeauto】已被篡改"); + } + } - $info = \app\common\model\PurchaseOrders::find(14); - $service = new \app\common\service\PurchaseOrdersService(); - $result = $service->startApproval($info); - return json($result); + public function tocurl($url, $header = null, $content = null) + { + $ch = curl_init(); + if(substr($url,0,5)=='https'){ + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查 + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在 + curl_setopt($ch, CURLOPT_SSLVERSION, 1); + } + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $url); + if (is_array($header)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + } + curl_setopt($ch, CURLOPT_POST, true); + if (!empty($content)) { + if (is_array($content)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($content)); + } else if (is_string($content)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $content); + } + } + $response = curl_exec($ch); + $error=curl_error($ch); + if($error){ + die($error); + } + $header = curl_getinfo($ch); + curl_close($ch); + $data = array('header' => $header,'body' => $response); + return $data; } } \ No newline at end of file diff --git a/app/middleware/Audit.php b/app/middleware/Audit.php new file mode 100644 index 0000000..deb99d5 --- /dev/null +++ b/app/middleware/Audit.php @@ -0,0 +1,114 @@ +getStatusCode(); + return $response; + } catch (\Throwable $e) { + $httpCode = $e->getCode() ?: 500; + $message = mb_substr($e->getMessage(), 0, 180); + $status = 0; + throw $e; + } finally { + $latency = (int)((microtime(true) - $start) * 1000); + $this->enqueue($request, $httpCode, $status, $message, $latency); + } + } + + + protected function enqueue(Request $req, int $httpCode, int $status, ?string $message, int $latencyMs): void + { + $buf = AuditBuffer::instance(); + + $user = $this->currentUser($req->user); + $paramsBrief = $this->briefParams($req->all()); + $ua = (string)($req->header('user-agent') ?? ''); + if (mb_strlen($ua) > 191) $ua = mb_substr($ua, 0, 191); + + $payload = [ + 'user_id' => $user['id'] ?? null, + 'user_name' => $user['name'] ?? null, + 'route' => $req->path(), + 'method' => $req->method(), + 'ip' => $req->getRealIp(), + 'ua' => $ua, + 'action' => $buf->action ?? $req->action, + 'status' => $status, + 'http_code' => $httpCode, + 'latency_ms' => $latencyMs, + 'params_brief' => $paramsBrief, // 数组,消费者再编码 + 'changes' => $buf->changes, // 数组,消费者再编码 + 'message' => $message, + 'ts' => time() + ]; + + try { + Queue::send('audit_log', $payload); // 异步 + } catch (\Throwable $e) { + // 遵循“绝不回落同步写”,仅记录错误 + Log::error('audit enqueue failed: '.$e->getMessage()); + } + } + + protected function briefParams(array $params): array + { + // $mask = ['password','pwd','token','secret','access_token','refresh_token','file','content','html']; + $mask = []; + $out = []; + foreach ($params as $k=>$v) { + if (in_array($k, $mask, true)) { $out[$k] = '***'; continue; } + if (is_array($v)) { $out[$k] = '[array('.count($v).')]'; continue; } + $s = (string)$v; + $out[$k] = mb_strlen($s) > 120 ? mb_substr($s, 0, 120).'…' : $s; + } + return $out; + } + + protected function inferAction(Request $req): ?string + { + $m = strtoupper($req->method()); + if ($m === 'POST') return 'create'; + if ($m === 'PUT' || $m === 'PATCH') return 'update'; + if ($m === 'DELETE') return 'delete'; + return 'view'; + } + + protected function currentUser($user): array + { + $user = (array)$user; + + if (!$user) { + return [ + 'id' => 0, + 'name' => 'guest' + ]; + } else { + if (!isset($user['uname'])) { + $user['uname'] = User::where('id', $user['uid'])->value('name'); + } + } + + return [ + 'id' => $user['uid'] ?? 0, + 'name' => $user['uname'] ?? 'guest' + ]; + } +} \ No newline at end of file diff --git a/app/process/CheckWeb.php b/app/process/CheckWeb.php new file mode 100644 index 0000000..c71a625 --- /dev/null +++ b/app/process/CheckWeb.php @@ -0,0 +1,62 @@ +check(); + }); + } + } + + + public function check() + { + $result = $this->tocurl('https://magicbridgeauto.com'); + if (preg_match('/magicbridgeauto/', $result['body'])) { + flg("【magicbridgeauto】未检查到异常"); + } else { + flg("【magicbridgeauto】疑似被篡改"); + } + } + + + public function tocurl($url, $header = null, $content = null) + { + $ch = curl_init(); + if(substr($url,0,5)=='https'){ + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查 + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在 + curl_setopt($ch, CURLOPT_SSLVERSION, 1); + } + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $url); + if (is_array($header)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + } + curl_setopt($ch, CURLOPT_POST, true); + if (!empty($content)) { + if (is_array($content)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($content)); + } else if (is_string($content)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $content); + } + } + $response = curl_exec($ch); + $error=curl_error($ch); + if($error){ + die($error); + } + $header = curl_getinfo($ch); + curl_close($ch); + $data = array('header' => $header,'body' => $response); + return $data; + } +} \ No newline at end of file diff --git a/app/queue/redis/AuditLog.php b/app/queue/redis/AuditLog.php new file mode 100644 index 0000000..dc8ef88 --- /dev/null +++ b/app/queue/redis/AuditLog.php @@ -0,0 +1,72 @@ + $data['trace_id'] ?? null, + 'user_id' => $data['user_id'] ?? null, + 'user_name' => $data['user_name'] ?? null, + 'route' => $data['route'] ?? null, + 'method' => $data['method'] ?? null, + 'ip' => $data['ip'] ?? null, + 'ua' => $data['ua'] ?? null, + 'action' => $data['action'] ?? null, + 'status' => (int)($data['status'] ?? 1), + 'http_code' => (int)($data['http_code'] ?? 200), + 'latency_ms' => $data['latency_ms'] ?? null, + 'params_brief' => !empty($data['params_brief']) ? json_encode($data['params_brief'], JSON_UNESCAPED_UNICODE) : null, + 'changes' => !empty($data['changes']) ? json_encode($data['changes'], JSON_UNESCAPED_UNICODE) : null, + 'message' => $data['message'] ?? null, + 'created_at' => $now, + 'updated_at' => $now, + ]; + + Db::table('sys_op_log')->insert($row); + } catch (\Throwable $e) { + Log::error('audit_log consume error: '.$e->getMessage()); + return false; // 允许重试与失败处理 + } + return true; + } + + // 消费失败回调 + /* + $package = [ + 'id' => 1357277951, // 消息ID + 'time' => 1709170510, // 消息时间 + 'delay' => 0, // 延迟时间 + 'attempts' => 2, // 消费次数 + 'queue' => 'send-mail', // 队列名 + 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容 + 'max_attempts' => 5, // 最大重试次数 + 'error' => '错误信息' // 错误信息 + ] + */ + public function onConsumeFailure(\Throwable $e, $package) + { + echo "consume failure\n"; + echo $e->getMessage() . "\n"; + // 无需反序列化 + var_export($package); + + Log::error('audit_log message failed after retries: '.json_encode($package, JSON_UNESCAPED_UNICODE)); + } +} \ No newline at end of file diff --git a/config/bootstrap.php b/config/bootstrap.php index 95d2e87..96699de 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -14,4 +14,7 @@ return [ support\bootstrap\Session::class, + + // 日志 + app\bootstrap\Audit::class, ]; diff --git a/config/middleware.php b/config/middleware.php index ae0ad6e..ec8d90a 100644 --- a/config/middleware.php +++ b/config/middleware.php @@ -17,6 +17,7 @@ return [ '' => [ \app\middleware\AccessControl::class, \app\middleware\BasicData::class, - \app\middleware\AuthCheck::class + \app\middleware\AuthCheck::class, + \app\middleware\Audit::class, ] ]; \ No newline at end of file