一、什么是审计日志?
1. 核心定义
- 审计日志:
- 审计日志用于记录后台管理员的操作行为,例如登录、修改数据、删除记录等。
- 对于修改操作,需要详细记录修改前后的值变化,以便追踪具体改动内容。
二、使用场景
1. 常见使用场景
- 操作追踪:
- 记录管理员对系统的所有操作,尤其是修改操作的具体变更内容。
- 安全监控:
- 监控异常修改操作(如敏感数据被篡改),及时发现潜在的安全威胁。
- 问题排查:
- 当系统出现问题时,通过审计日志快速定位问题原因。
- 合规性要求:
- 满足法律法规(如 GDPR 或 HIPAA)对操作记录的要求。
三、底层原理
1. 审计日志的工作机制
- 作用:
- 确保所有管理员操作都被记录并可追溯,尤其是修改操作的具体变更内容。
- 原理:
- 事件监听:
- 使用 Yii2 的事件机制(
events
)监听用户的 CRUD 操作。
- 使用 Yii2 的事件机制(
- 变更检测:
- 在
EVENT_BEFORE_UPDATE
和EVENT_AFTER_UPDATE
中捕获修改前后的数据。
- 在
- 日志存储:
- 将操作记录存储到数据库表中,包含操作类型、操作时间、操作者信息以及具体的变更内容。
- 行为绑定:
- 使用
behaviors
绑定模型操作(如插入、更新、删除)到日志记录逻辑。
- 使用
- 权限控制:
- 确保只有授权用户才能查看或管理审计日志。
- 事件监听:
2. 具体步骤
- 创建审计日志表:
- 在数据库中创建一个表用于存储审计日志,包括字段用于记录变更内容。
- 定义日志模型:
- 创建一个模型类(如
AuditLog
)用于操作日志表。
- 创建一个模型类(如
- 监听用户操作:
- 使用 Yii2 的事件机制监听用户的 CRUD 操作。
- 记录变更内容:
- 在
EVENT_BEFORE_UPDATE
中捕获修改前的数据,在EVENT_AFTER_UPDATE
中捕获修改后的数据。
- 在
- 存储日志:
- 将操作详情和变更内容写入审计日志表。
- 查询日志:
- 提供接口或页面用于查看审计日志。
四、具体的完整 PHP 实例代码
以下是一个完整的 Yii2 示例代码,展示如何实现后台管理员操作的审计日志,并详细记录修改操作的具体变更内容。
1. 数据库迁移文件
<?php
use yii\db\Migration;
/**
* 创建审计日志表的迁移文件
*/
class m000000_000000_create_audit_log_table extends Migration
{
/**
* 定义一个方法用于创建审计日志表
*/
public function up()
{
$this->createTable('audit_log', [
'id' => $this->primaryKey(), // 主键
'user_id' => $this->integer()->notNull(), // 操作者的用户 ID
'action' => $this->string(255)->notNull(), // 操作类型(如 insert/update/delete)
'model' => $this->string(255)->notNull(), // 操作的模型名称
'model_id' => $this->integer()->notNull(), // 操作的模型主键值
'old_data' => $this->text(), // 修改前的数据(JSON 格式)
'new_data' => $this->text(), // 修改后的数据(JSON 格式)
'created_at' => $this->integer()->notNull(), // 操作时间
]);
// 添加索引以提高查询性能
$this->createIndex('idx-audit_log-user_id', 'audit_log', 'user_id');
$this->createIndex('idx-audit_log-created_at', 'audit_log', 'created_at');
}
/**
* 定义一个方法用于回滚迁移
*/
public function down()
{
$this->dropTable('audit_log'); // 删除审计日志表
}
}
2. 审计日志模型
<?php
namespace app\models;
use yii\db\ActiveRecord;
/**
* 审计日志模型类
*/
class AuditLog extends ActiveRecord
{
/**
* 定义一个方法用于获取表名
*
* @return string 返回表名
*/
public static function tableName()
{
return 'audit_log'; // 返回审计日志表名
}
/**
* 定义一个方法用于记录日志
*
* @param int $userId 用户 ID
* @param string $action 操作类型
* @param string $model 模型名称
* @param int $modelId 模型主键值
* @param array|null $oldData 修改前的数据
* @param array|null $newData 修改后的数据
*/
public static function log($userId, $action, $model, $modelId, $oldData = null, $newData = null)
{
$log = new self(); // 创建审计日志实例
$log->user_id = $userId; // 设置用户 ID
$log->action = $action; // 设置操作类型
$log->model = $model; // 设置模型名称
$log->model_id = $modelId; // 设置模型主键值
$log->old_data = $oldData ? json_encode($oldData) : null; // 将修改前的数据序列化为 JSON 格式
$log->new_data = $newData ? json_encode($newData) : null; // 将修改后的数据序列化为 JSON 格式
$log->created_at = time(); // 设置操作时间
$log->save(); // 保存日志记录
}
}
3. 行为绑定与事件监听
<?php
namespace app\behaviors;
use yii\base\Behavior;
use yii\db\ActiveRecord;
use app\models\AuditLog;
use Yii;
/**
* 审计日志行为类
*/
class AuditLogBehavior extends Behavior
{
/**
* 定义一个方法用于绑定事件
*
* @return array 返回事件绑定数组
*/
public function events()
{
return [
ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert', // 插入后触发
ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate', // 更新前触发
ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate', // 更新后触发
ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete', // 删除后触发
];
}
/**
* 定义一个方法用于处理插入后的事件
*
* @param \yii\base\Event $event 事件对象
*/
public function afterInsert($event)
{
$this->log('insert', null, null); // 记录插入操作
}
/**
* 定义一个方法用于处理更新前的事件
*
* @param \yii\base\Event $event 事件对象
*/
public function beforeUpdate($event)
{
$this->owner->oldAttributes = $this->owner->getOldAttributes(); // 保存修改前的属性
}
/**
* 定义一个方法用于处理更新后的事件
*
* @param \yii\base\Event $event 事件对象
*/
public function afterUpdate($event)
{
$oldData = $this->owner->oldAttributes; // 获取修改前的属性
$newData = $this->owner->getAttributes(); // 获取修改后的属性
$this->log('update', $oldData, $newData); // 记录更新操作
}
/**
* 定义一个方法用于处理删除后的事件
*
* @param \yii\base\Event $event 事件对象
*/
public function afterDelete($event)
{
$this->log('delete', null, null); // 记录删除操作
}
/**
* 定义一个方法用于记录日志
*
* @param string $action 操作类型
* @param array|null $oldData 修改前的数据
* @param array|null $newData 修改后的数据
*/
private function log($action, $oldData, $newData)
{
$model = $this->owner; // 获取当前模型实例
$userId = Yii::$app->user->id ?? null; // 获取当前用户 ID
AuditLog::log(
$userId,
$action,
$model::className(),
$model->getPrimaryKey(),
$oldData,
$newData
); // 调用日志模型记录日志
}
}
4. 应用行为到模型
<?php
namespace app\models;
use yii\db\ActiveRecord;
use app\behaviors\AuditLogBehavior;
/**
* 示例模型类
*/
class ExampleModel extends ActiveRecord
{
/**
* 定义一个方法用于绑定行为
*
* @return array 返回行为数组
*/
public function behaviors()
{
return [
AuditLogBehavior::class, // 绑定审计日志行为
];
}
}
五、总结
1. 为什么需要审计日志?
- 操作追踪:
- 记录管理员的所有操作,尤其是修改操作的具体变更内容。
- 安全监控:
- 监控异常修改操作,及时发现潜在的安全威胁。
- 问题排查:
- 快速定位问题原因。
- 合规性要求:
- 满足法律法规对操作记录的要求。
2. 底层原理总结
- 事件监听:
- 使用 Yii2 的事件机制监听用户的 CRUD 操作。
- 变更检测:
- 在
EVENT_BEFORE_UPDATE
和EVENT_AFTER_UPDATE
中捕获修改前后的数据。
- 在
- 日志存储:
- 将操作记录和变更内容存储到数据库表中。
- 行为绑定:
- 使用
behaviors
绑定模型操作到日志记录逻辑。
- 使用
- 权限控制:
- 确保只有授权用户才能查看或管理审计日志。
3. 注意事项
- 性能优化:
- 对审计日志表添加索引以提高查询性能。
- 数据隐私:
- 敏感数据应加密存储,避免泄露。
- 日志清理:
- 定期清理过期的日志记录,避免占用过多磁盘空间。
1. 功能需求
- 日志列表:
- 显示所有操作日志,包含操作类型、操作者、操作时间、模型名称、主键值、修改前后的数据。
- 分页支持:
- 支持分页显示日志记录,避免一次性加载过多数据。
- 搜索与过滤:
- 支持按操作类型、操作者、时间范围等条件进行搜索和过滤。
- 数据格式化:
- 修改前后的 JSON 数据需要以易读的方式展示(如高亮显示差异)。
二、具体实现步骤
1. 后端实现
- 查询日志:
- 提供一个控制器方法用于查询审计日志,并支持分页和过滤。
- 数据格式化:
- 将 JSON 数据转换为易读的格式。
2. 前端实现
- 表格展示:
- 使用 HTML 表格或第三方组件(如 DataTables 或 Bootstrap Table)展示日志。
- 分页与过滤:
- 提供分页控件和过滤表单,方便用户操作。
- JSON 差异高亮:
- 使用 JavaScript 库(如
diff
或json-diff
)高亮显示修改前后的差异。
- 使用 JavaScript 库(如
三、完整的 PHP 实例代码
1. 后端:日志查询接口
<?php
namespace app\controllers;
use yii\web\Controller;
use yii\data\ActiveDataProvider;
use app\models\AuditLog;
use Yii;
/**
* 审计日志控制器
*/
class AuditLogController extends Controller
{
/**
* 定义一个方法用于查询日志
*
* @return string 返回日志列表页面
*/
public function actionIndex()
{
// 获取请求参数
$action = Yii::$app->request->get('action'); // 操作类型
$userId = Yii::$app->request->get('user_id'); // 用户 ID
$startDate = Yii::$app->request->get('start_date'); // 开始日期
$endDate = Yii::$app->request->get('end_date'); // 结束日期
// 构建查询条件
$query = AuditLog::find();
if ($action) {
$query->andWhere(['action' => $action]); // 过滤操作类型
}
if ($userId) {
$query->andWhere(['user_id' => $userId]); // 过滤用户 ID
}
if ($startDate && $endDate) {
$query->andWhere(['between', 'created_at', strtotime($startDate), strtotime($endDate)]); // 过滤时间范围
}
// 分页查询
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 20, // 每页显示 20 条记录
],
'sort' => [
'defaultOrder' => ['created_at' => SORT_DESC], // 默认按时间倒序排序
],
]);
return $this->render('index', [
'dataProvider' => $dataProvider, // 传递数据提供器到视图
]);
}
}
2. 前端:日志列表页面
<?php
use yii\grid\GridView;
use yii\widgets\ActiveForm;
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = '审计日志';
?>
<h1>审计日志</h1>
<!-- 搜索表单 -->
<?php $form = ActiveForm::begin([
'method' => 'get',
'options' => ['class' => 'form-inline'],
]); ?>
<?= $form->field($model, 'action')->dropDownList(
['insert' => '插入', 'update' => '更新', 'delete' => '删除'],
['prompt' => '选择操作类型']
)->label(false) ?>
<?= $form->field($model, 'user_id')->textInput(['placeholder' => '用户 ID'])->label(false) ?>
<?= $form->field($model, 'start_date')->textInput(['type' => 'date'])->label(false) ?>
<?= $form->field($model, 'end_date')->textInput(['type' => 'date'])->label(false) ?>
<?= Html::submitButton('搜索', ['class' => 'btn btn-primary']) ?>
<?php ActiveForm::end(); ?>
<!-- 日志表格 -->
<?= GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'], // 序号列
'user_id:text:操作者',
'action:text:操作类型',
'model:text:模型名称',
'model_id:text:模型主键值',
[
'attribute' => 'old_data',
'format' => 'raw',
'value' => function ($model) {
return $model->old_data ? '<pre>' . htmlspecialchars($model->old_data) . '</pre>' : '无'; // 格式化旧数据
},
],
[
'attribute' => 'new_data',
'format' => 'raw',
'value' => function ($model) {
return $model->new_data ? '<pre>' . htmlspecialchars($model->new_data) . '</pre>' : '无'; // 格式化新数据
},
],
[
'attribute' => 'created_at',
'format' => 'datetime', // 格式化时间
],
],
]); ?>
3. 前端:JSON 差异高亮
为了更直观地展示修改前后的差异,可以使用 JavaScript 库(如 json-diff
)对 JSON 数据进行高亮显示。
<script src="https://cdn.jsdelivr.net/npm/json-diff/dist/json-diff.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// 获取所有 JSON 数据单元格
const oldDataCells = document.querySelectorAll('.old-data');
const newDataCells = document.querySelectorAll('.new-data');
oldDataCells.forEach((cell, index) => {
const oldData = JSON.parse(cell.textContent);
const newData = JSON.parse(newDataCells[index].textContent);
// 计算差异
const diff = jsonDiff.diff(oldData, newData);
// 替换内容为高亮显示的差异
cell.innerHTML = `<pre>${JSON.stringify(diff, null, 2)}</pre>`;
newDataCells[index].innerHTML = `<pre>${JSON.stringify(diff, null, 2)}</pre>`;
});
});
</script>
四、总结
1. 为什么需要优化查看功能?
- 用户体验:
- 提供清晰的日志展示方式,便于管理员快速定位问题。
- 数据可读性:
- 高亮显示 JSON 差异,提升数据的可读性。
- 高效管理:
- 支持分页和过滤,避免加载过多数据影响性能。
2. 优化点总结
- 分页支持:
- 使用
ActiveDataProvider
实现分页查询。
- 使用
- 搜索与过滤:
- 提供搜索表单,支持按条件筛选日志。
- 数据格式化:
- 将 JSON 数据以易读的方式展示,并高亮显示差异。
- 前端增强:
- 使用 JavaScript 库动态处理 JSON 数据。
3. 注意事项
- 性能优化:
- 对审计日志表添加索引以提高查询性能。
- 数据隐私:
- 敏感数据应加密存储,避免泄露。
- 日志清理:
- 定期清理过期的日志记录,避免占用过多磁盘空间。