kdesilva / audit-trail
灵活且稳固的cakephp审计日志跟踪插件
Requires
- php: >=7.4
- cakephp/orm: ^4.0
Requires (Dev)
- cakephp/cakephp: ^4.0
- cakephp/cakephp-codesniffer: ^4.0.0
- cakephp/elastic-search: ^3.0
- friendsofcake/process-mq: dev-master
- phpunit/phpunit: ^8.0
Suggests
- cakephp/elastic-search: The default persister engine for audit-stash is elastic search and requires this plugin
- friendsofcake/crud: audit-stash provide Crud Action classes for displaying audit logs
- friendsofcake/process-mq: Use this package if you prefer sending the audit events to RabbitMQ
README
此插件是从lorenzo/audit-stash分叉而来
虽然上述插件非常适合存储更改历史,但它有以下问题或没有我需要的所有功能。
-
在删除事件中未记录原始数据。
- 如果您在添加了一些数据后添加此插件,这将很有用。
-
关联表记录未正确保存(在这种情况下,我考虑了两个具有“hasMany”关系的模型)
- 目前,在CakePHP(4.x)ORM中,当存在“hasMany”关系(例如,考虑两个数据库表:items和item_attributes)时,
EntityTrait::extractOriginal(array $fields)
不返回关联表(item_attributes)原始数据的原始值,而是返回修改后的值。 - 此外,在CakePHP(4.x)ORM中还有一个错误,即使没有对关联表数据进行更改,它也会将关联的('hasMany')实体标记为脏的。
- 无法仅记录关联表的更改数据列
- 目前,在CakePHP(4.x)ORM中,当存在“hasMany”关系(例如,考虑两个数据库表:items和item_attributes)时,
-
当调用saveMany()来保存多个实体时,无法记录审计日志
-
无法通过app.php设置AuditLog行为和/或Table Persister的一些常见配置
-
无法记录外键的友好数据字段
-
创建事件将相同的数据添加到'original'和'changed'列中
-
'id'(主键)字段添加到'original'和'changed'数据中,除非您在每个模型类中将它列入黑名单。(主键作为单独的字段记录)
-
无法在数据库中存储用户友好的字段值。这对于轻松识别记录很有用;特别是,当数据库仅存储更改数据时。因此,我使用了CakePHP Model::setDisplayField()来检索用户友好的字段值。
因此,我决定从原始项目分叉并改进它以支持上述缺失的功能。
安装
您可以使用composer将此插件安装到您的CakePHP应用程序中,并在应用程序的根目录中执行以下命令。
composer require kdesilva/audit-trail
bin/cake plugin load AuditStash
如果您计划使用ElasticSearch作为存储引擎,请参阅lorenzo/audit-stash
配置
表/常规数据库
如果您想使用常规数据库,然后通过CakePHP ORM API使用引擎,则可以使用此插件提供的表持久化器。
为此,您需要相应地配置AuditStash.persister
选项。在您的config/app.php
文件中添加以下配置
'AuditStash' => [ 'persister' => 'AuditStash\Persister\TablePersister' ]
默认情况下,插件将尝试将日志存储在名为audit_logs
的表中,通过具有别名AuditLogs
的表类。您可以在应用程序中创建/覆盖它,如果您需要的话。
您可以在本插件的config/migration
文件夹中找到一个迁移,您可以将它应用到您的数据库中,这将添加一个名为audit_logs
的表,其中包含所有默认列。或者,您可以自己烘焙迁移来创建表。之后,您可以为相应的表类进行迁移。
如果您使用插件默认的迁移,可以使用以下命令创建表和模型类。
bin/cake migrations migrate -p AuditStash -t 20171018185609
bin/cake bake model AuditLogs
表持久化器配置
表格持久化支持各种配置选项,请参阅其API文档以获取更多信息。通常,配置可以通过其config()
(或setConfig()
)方法应用
$this->addBehavior('AuditStash.AuditLog'); $this->behaviors()->get('AuditLog')->persister()->config([ 'extractMetaFields' => [ 'user.name' => 'username' ] ]);
此外,您还可以通过app.php
设置一些常用配置。目前,插件支持'extractMetaFields'和'blacklist'
... 'AuditStash' => [ 'persister' => 'AuditStash\Persister\TablePersister', 'extractMetaFields' => [ 'user.username' => 'username', 'user.customer_id' => 'customer_id', ], 'blacklist' => ['customer_id'], ],
使用AuditStash
在您的任何表类中启用审计日志就像在initialize()
函数中添加一个行为一样简单
class ArticlesTable extends Table { public function initialize(array $config = []) { $this->setDisplayField('article_name'); ... $this->addBehavior('AuditStash.AuditLog'); } }
配置行为
AuditLog
行为可以配置为忽略您的表中的某些字段,默认情况下,它忽略id
、created
和modified
字段
class ArticlesTable extends Table { public function initialize(array $config = []) { $this->setDisplayField('article_name'); ... $this->addBehavior('AuditStash.AuditLog', [ 'blacklist' => ['created', 'modified', 'another_field_name'] ]); } }
如果您喜欢,可以使用whitelist
代替。这意味着只有列在该数组中的字段才会被行为跟踪
class ArticlesTable extends Table { public function initialize(array $config = []) { $this->setDisplayField('article_name'); ... $this->addBehavior('AuditStash.AuditLog', [ 'whitelist' => ['title', 'description', 'author_id'] ]); } }
如果您需要从相关表(即具有外键的表)检索人类友好的数据字段,您可以设置如下所示的foreignKeys
。
public function initialize(array $config = []) { $this->setDisplayField('article_name'); ... $this->addBehavior('AuditStash.AuditLog', [ 'blacklist' => ['customer_id', 'product_id'], 'foreignKeys' => [ 'Categories' => 'name', // foreign key Model => human-friendly field name 'ProductStatuses' => 'status', ], 'unsetAssociatedEntityFieldsNotDirtyByFieldName' => [ 'associated_table_name' => 'field_name_in_associated_table' ] ]); }
如上项目描述中所述,CakePHP (4.x) ORM即使在关联数据没有发生变化的情况下也会返回所有关联数据。因此,如果您需要从关联实体中删除未更改的数据,您需要设置unsetAssociatedEntityFieldsNotDirtyByFieldName
,如上述示例所示。
存储登录用户
通常,将触发某些表更改的用户标识存储起来非常有用。为此,AuditStash
提供了一个能够存储当前URL、IP和登录用户的RequestMetadata
监听器类。您需要将此监听器添加到您的应用程序中的AppController::beforeFilter()
方法
use AuditStash\Meta\RequestMetadata; ... class AppController extends Controller { public function beforeFilter(Event $event) { ... $eventManager = $this->loadModel()->eventManager(); $identity = $this->request->getAttribute('identity'); if ($identity != null) { $eventManager->on( new RequestMetadata($this->request, [ 'username' => $identity['username'], 'customer_id' => $identity['customer_id'], ]) ); } } }
上述代码假设您将从控制器触发表操作,使用控制器的默认Table类。如果您计划在同一个控制器中使用其他Table类进行保存或删除,建议您全局附加监听器
use AuditStash\Meta\RequestMetadata; use Cake\Event\EventManager; ... class AppController extends Controller { public function beforeFilter(Event $event) { ... $identity = $this->request->getAttribute('identity'); if ($identity != null) { EventManager::instance()->on( new RequestMetadata($this->request, [ 'username' => $identity['username'], 'customer_id' => $identity['customer_id'], ]) ); } } }
在日志中存储额外信息
AuditStash
还能够为每个已记录的事件存储任意数据。您可以使用ApplicationMetadata
监听器或创建自己的。如果您选择使用ApplicationMetadata
,您的日志将包含存储的app_name
键和您提供的任何额外信息。您可以在应用程序的任何位置配置此监听器,例如在bootstrap.php
文件中,或者在AppController中直接配置。
use AuditStash\Meta\ApplicationMetadata; use Cake\Event\EventManager; EventManager::instance()->on(new ApplicationMetadata('my_blog_app', [ 'server' => $theServerID, 'extra' => $somExtraInformation, 'moon_phase' => $currentMoonPhase ]));
实现自己的元数据监听器就像将监听器附加到AuditStash.beforeLog
事件一样简单。例如
EventManager::instance()->on('AuditStash.beforeLog', function ($event, array $logs) { foreach ($logs as $log) { $log->setMetaInfo($log->getMetaInfo() + ['extra' => 'This is extra data to be stored']); } });
实现您自己的持久化策略
有合理的理由想要为审计日志使用不同的持久化引擎。幸运的是,此插件允许您实现自己的存储引擎。这就像实现PersisterInterface
接口一样简单
use AuditStash\PersisterInterface; class MyPersister implements PersisterInterface { public function logEvents(array $auditLogs) { foreach ($auditLogs as $log) { $eventType = $log->getEventType(); $data = [ 'timestamp' => $log->getTimestamp(), 'transaction' => $log->getTransactionId(), 'type' => $log->getEventType(), 'primary_key' => $log->getId(), 'display_value' => $event->getDisplayValue(), 'source' => $log->getSourceName(), 'parent_source' => $log->getParentSourceName(), 'original' => json_encode($log->getOriginal()), 'changed' => $eventType === 'delete' ? null : json_encode($log->getChanged()), 'meta' => json_encode($log->getMetaInfo()) ]; $storage = new MyStorage(); $storage->save($data); } } }
最后,您需要配置AuditStash
以使用您的新持久化器。在config/app.php
文件中添加以下行
'AuditStash' => [ 'persister' => 'App\Namespace\For\Your\Persister' ]
或者如果您作为独立插件使用
\Cake\Core\Configure::write('AuditStash.persister', 'App\Namespace\For\Your\DatabasePersister');
配置包含您持久化器的完全限定类名。
处理事务性查询
偶尔,您可能希望将多个数据库更改包装在一个事务中,以便在过程的一部分失败时可以回滚。为了在事务中创建审计日志,需要进行一些额外的设置。首先创建文件src/Model/Audit/AuditTrail.php
,内容如下
<?php namespace App\Model\Audit; use Cake\Utility\Text; use SplObjectStorage; class AuditTrail { protected $_auditQueue; protected $_auditTransaction; public function __construct() { $this->_auditQueue = new SplObjectStorage; $this->_auditTransaction = Text::uuid(); } public function toSaveOptions() { return [ '_auditQueue' => $this->_auditQueue, '_auditTransaction' => $this->_auditTransaction ]; } }
在任何您希望使用Connection::transactional()
的地方,您需要在文件顶部包含以下内容
use App\Model\Audit\AuditTrail; use Cake\Event\Event;
然后,您的交易应类似于以下示例中的BookmarksController
$trail = new AuditTrail(); $success = $this->Bookmarks->connection()->transactional(function () use ($trail) { $bookmark = $this->Bookmarks->newEntity(); $bookmark1->save($data1, $trail->toSaveOptions()); $bookmark2 = $this->Bookmarks->newEntity(); $bookmark2->save($data2, $trail->toSaveOptions()); ... $bookmarkN = $this->Bookmarks->newEntity(); $bookmarkN->save($dataN, $trail->toSaveOptions()); return true; }); if ($success) { $event = new Event('Model.afterCommit', $this->Bookmarks); $table->behaviors()->get('AuditLog')->afterCommit($event, $result, $auditTrail->toSaveOptions()); }
这将保存您的对象的所有审计信息,以及任何相关数据的审计信息。请注意,$result
必须是一个Object实例。不要更改文本“Model.afterCommit”。
保存多个实体
创建文件src/Model/Audit/AuditTrail.php
,如上节所示
... $auditTrail = new AuditTrail(); if ($this->Bookmarks->saveMany($entities, $auditTrail->toSaveOptions())) { ... }