PHPAPI日志审计怎么记录

wen PHP项目 6

本文目录导读:

PHPAPI日志审计怎么记录

  1. 审计日志应该包含的核心字段
  2. 技术选型
  3. 代码实现(以Laravel为例,但核心逻辑通用)
  4. 常见注意事项与坑
  5. 一句话总结

在PHP API开发中,日志审计(Audit Logging)是安全审计和故障排查的核心手段,它需要记录谁、在什么时间、对什么资源、执行了什么操作、结果如何

下面我为你梳理一套完整的PHP API日志审计记录方案,包含技术选型、数据结构、代码实现和最佳实践。


审计日志应该包含的核心字段

不要只记录错误,审计日志需要包含上下文

{
  "audit_id": "uuid-xxxx",          // 唯一ID,方便关联查询
  "timestamp": "2024-05-20T10:30:00Z", // 精确到毫秒
  "user_id": 12345,                 // 操作人ID(匿名用户可为null或"guest")
  "user_ip": "192.168.1.100",       // 请求来源IP
  "user_agent": "Mozilla/5.0 ...",  // 客户端信息
  "request_id": "req-xxxx",         // 请求追踪ID(用于关联微服务链路)
  "http_method": "POST",
  "api_endpoint": "/api/v1/orders",
  "http_status_code": 200,
  "action": "CREATE_ORDER",         // 业务操作类型,如 ORDER_CREATE, USER_LOGIN
  "resource_type": "order",         // 操作的对象类型
  "resource_id": "ORD-20240520-001",// 操作的对象ID
  "target_entity": {                // 变更前的状态(关键字段)
    "status": "draft",
    "total_amount": 100.00
  },
  "new_entity": {                   // 变更后的状态(关键字段)
    "status": "paid",
    "total_amount": 100.00,
    "payment_method": "credit_card"
  },
  "details": "用户支付订单成功",     // 人类可读的描述
  "severity": "info"               // info / warning / critical
}

关键点:审计日志的核心是 "之前是什么,之后是什么"(Before/After Image),这能让你在出现问题时回溯数据变更轨迹。


技术选型

方案 优点 缺点 适用场景
文件日志 (Monolog + 自定义Handler) 简单、无外部依赖、标准化 检索困难、不支持高并发写入、日志轮换需自行处理 小型项目、开发环境
数据库表 (MySQL/PostgreSQL) 结构化、方便查询、支持事务关联 写压力大(高频API会锁表)、存储膨胀快 中小型项目、对写入QPS要求不高(<500/s)
消息队列 (RabbitMQ/Kafka) + Elasticsearch 高吞吐、异步、实时搜索、数据持久化 架构复杂、需要运维ELK栈 中大型生产环境、需要实时检索和可视化
专用的审计日志服务 (如 Audit Trail 工具) 开箱即用、有UI、有合规报告 额外成本、可能不够灵活 企业级、需要满足PCI-DSS/SOC2合规

推荐组合(生产环境)

  • 高频CRUD API:用 Kafka + 消费者写入Elasticsearch(或S3冷存储)
  • 低频管理操作(如管理员配置修改):直接同步写入数据库表(或同时写入数据库+队列)
  • 简单项目:用 Monolog + 固定格式文件,配合 greptail 排查

代码实现(以Laravel为例,但核心逻辑通用)

使用 Middleware 记录请求上下文

// app/Http/Middleware/AuditContextMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class AuditContextMiddleware
{
    public function handle($request, Closure $next)
    {
        // 生成或获取请求ID(如果从网关传入)
        $requestId = $request->header('X-Request-Id') 
                    ?: (string) Str::uuid();
        // 绑定到日志上下文(后续所有日志自动带上这些字段)
        Log::withContext([
            'request_id' => $requestId,
            'client_ip'  => $request->ip(),
            'user_agent' => $request->userAgent(),
            'user_id'    => auth()->id() ?? 'guest',
            'endpoint'   => $request->method() . ' ' . $request->path(),
        ]);
        $response = $next($request);
        return $response;
    }
}

核心:记录业务操作的 Before/After

方案A:在 Model 的 Observers/Events 里记录

// app/Observers/OrderObserver.php
namespace App\Observers;
use App\Models\Order;
use App\Services\AuditLogger;
class OrderObserver
{
    public function updated(Order $order)
    {
        // 只记录关键字段变化
        $changed = $order->getDirty(); 
        $original = $order->getOriginal();
        // 过滤掉无关字段(如 updated_at)
        $auditableFields = ['status', 'total_amount', 'payment_method'];
        $diff = array_intersect_key($changed, array_flip($auditableFields));
        if (!empty($diff)) {
            AuditLogger::log([
                'action'        => 'UPDATE_ORDER',
                'resource_type' => 'order',
                'resource_id'   => $order->id,
                'target_entity' => array_intersect_key($original, $diff), // 旧值
                'new_entity'    => $diff,                                // 新值
                'description'   => "订单 {$order->order_no} 的状态由 {$original['status']} 变更为 {$order->status}",
            ]);
        }
    }
    public function created(Order $order)
    {
        AuditLogger::log([
            'action'        => 'CREATE_ORDER',
            'resource_type' => 'order',
            'resource_id'   => $order->id,
            'target_entity' => null,  // 创建操作没有旧值
            'new_entity'    => $order->toArray(),
            'description'   => "创建订单 {$order->order_no}",
        ]);
    }
}

方案B:在 Service 层手动记录(推荐)

// app/Services/OrderService.php
use App\Services\AuditLogger;
use Illuminate\Support\Facades\DB;
use App\Exceptions\BusinessException;
class OrderService
{
    public function payOrder(int $orderId, array $paymentData)
    {
        DB::beginTransaction();
        try {
            $order = Order::findOrFail($orderId);
            $beforeStatus = $order->status;
            // 执行业务逻辑
            $order->status = 'paid';
            $order->payment_method = $paymentData['method'];
            $order->save();
            // 手动记录审计(更清晰)
            AuditLogger::log([
                'action'        => 'ORDER_PAY_SUCCESS',
                'resource_type' => 'order',
                'resource_id'   => $order->id,
                'target_entity' => [
                    'status' => $beforeStatus,
                    'total'  => $order->total_amount,
                ],
                'new_entity' => [
                    'status'          => 'paid',
                    'payment_method'  => $paymentData['method'],
                ],
                'description' => "用户 {$order->user_id} 支付订单 {$order->order_no} 成功",
                'severity'    => 'info',
            ]);
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            AuditLogger::log([
                'action'        => 'ORDER_PAY_FAILED',
                'resource_type' => 'order',
                'resource_id'   => $orderId,
                'target_entity' => ['error' => $e->getMessage()],
                'description'   => "订单支付失败: " . $e->getMessage(),
                'severity'      => 'error',
            ]);
            throw new BusinessException('支付失败');
        }
    }
}

统一的写入器(支持多后端)

// app/Services/AuditLogger.php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class AuditLogger
{
    public static function log(array $data)
    {
        $auditRecord = [
            'audit_id'       => (string) Str::uuid(),
            'timestamp'      => now()->toIso8601String(),
            'user_id'        => auth()->id() ?? 'system',
            'client_ip'      => request()->ip() ?? '127.0.0.1',
            'request_id'     => request()->header('X-Request-Id') ?? Str::uuid(),
            'http_method'    => request()->method(),
            'api_endpoint'   => request()->path(),
            'http_status'    => http_response_code(),
            ...$data,
        ];
        // 策略1: 本地文件日志(快速开发/调试)
        Log::channel('audit')->info(json_encode($auditRecord));
        // 策略2: 写入数据库(适用于低频或管理后台)
        // DB::table('audit_logs')->insert($auditRecord);
        // 策略3: 发送到消息队列(适用于高并发)
        // Kafka::publish('audit-topic', $auditRecord);
    }
}

配置文件 config/logging.php 增加 audit 频道:

'channels' => [
    'audit' => [
        'driver' => 'daily',  // 按天轮转,保留30天
        'path'   => storage_path('logs/audit.log'),
        'level'  => 'info',
        'days'   => 30,
    ],
],

常见注意事项与坑

  1. 不要在日志里记录敏感数据

    • ❌ 记录 password, credit_card_number, access_token
    • ✅ 记录 payment_method: "credit_card" (不记录卡号)
    • ✅ 记录 user_id 但不记录 full_name
  2. 性能考虑

    • 审计日志写入是 写密集型 操作,高并发场景一定要异步(消息队列)。
    • 如果同步写数据库,考虑使用单独的审计日志数据库,或使用批量插入(攒够100条或等1秒再刷入)。
  3. 存储与保留策略

    • 热数据(最近7天):Elasticsearch 或 MySQL(带索引)
    • 温数据(7天~1年):压缩后存到 S3 / 对象存储
    • 冷数据(>1年):定期清理或归档到 Glacier 类存储
  4. 时区与时间同步

    • 全部使用 UTC 时间存储,前端展示时再转本地时间。
    • 确保服务器 NTP 同步,否则时间线会乱。
  5. 不可篡改性的高级需求(合规要求):

    • 使用 签名链:每条日志包含前一条日志的哈希,形如区块链。
    • 写后立刻转存到只写一次的存储(如 AWS S3 Object Lock)。
    • 或使用专门的审计日志 Saas 服务(如 Google Cloud Audit Logs, AWS CloudTrail)。

一句话总结

  • 对于小型API:用 Monolog 写入 storage/logs/audit.log,按天轮转,配合 grep 查找。
  • 对于中型API:用 MySQL 表存储审计日志,每条记录包含 user_id, action, resource_id, old_value, new_value, created_at,异步插入。
  • 对于大型分布式API:推送到 Kafka,消费后写入 Elasticsearch,配合 Kibana 做可视化和报警。

最后:审计日志的代码实现不难,难在想清楚记录哪些字段以及和业务代码解耦,建议使用 Observer 或 AOP 思想,不要手动在每行业务代码后都写 AuditLogger::log(),否则维护起来会非常痛苦。

抱歉,评论功能暂时关闭!