CakePHP 3中表单字段的加密/解密



我想有一些表单字段加密时,他们被添加/编辑和解密时,他们被蛋糕查找。以下是在v2.7.2中为我工作的代码:

core.php
Configure::write('Security.key','secretkey');

app/模型/patient.php。

public $encryptedFields = array('patient_surname', 'patient_first_name');
public function beforeSave($options = array()) {
    foreach($this->encryptedFields as $fieldName){
        if(!empty($this->data[$this->alias][$fieldName])){
            $this->data[$this->alias][$fieldName] = Security::encrypt(
                $this->data[$this->alias][$fieldName],
                Configure::read('Security.key')
            );
        }
    }
    return true;
}
public function afterFind($results, $primary = false) {
    foreach ($results as $key => $val) {
        foreach($this->encryptedFields as $fieldName) {
            if (@is_array($results[$key][$this->alias])) {
                $results[$key][$this->alias][$fieldName] = Security::decrypt(
                    $results[$key][$this->alias][$fieldName],
                    Configure::read('Security.key')
                );
            }
        }
    }
    return $results;
}

正如我所理解的,我必须将$this->data[]替换为模型生成的实体和afterFind方法与虚拟字段,但我只是不能把它们放在一起。

解决这个问题的方法不止一种(请注意,下面的代码是未经测试的示例代码!在使用这些之前,您应该首先掌握新的基础知识。

自定义数据库类型

一种是自定义数据库类型,它在将值绑定到数据库语句时进行加密,在获取结果时进行解密。这是我比较喜欢的选项。

下面是一个简单的例子,假设db列可以保存二进制数据。

src/数据库/类型/CryptedType.php

这应该是不言自明的,转换到数据库时加密,转换到PHP时解密。

<?php
namespace AppDatabaseType;
use CakeDatabaseDriver;
use CakeDatabaseType;
use CakeUtilitySecurity;
class CryptedType extends Type
{
    public function toDatabase($value, Driver $driver)
    {
        return Security::encrypt($value, Security::getSalt());
    }
    public function toPHP($value, Driver $driver)
    {
        if ($value === null) {
            return null;
        }
        return Security::decrypt($value, Security::getSalt());
    }
}

src/config/bootstrap.php

注册自定义类型

use CakeDatabaseType;
Type::map('crypted', 'AppDatabaseTypeCryptedType');

src/模型/表/PatientsTable.php

最后将可加密列映射到注册的类型,就是这样,从现在开始,一切都将自动处理。

// ...
use CakeDatabaseSchemaTable as Schema;
class PatientsTable extends Table
{
    // ...
    
    protected function _initializeSchema(Schema $table)
    {
        $table->setColumnType('patient_surname', 'crypted');
        $table->setColumnType('patient_first_name', 'crypted');
        return $table;
    }
    // ...
}

参见Cookbook>数据库访问&ORM祝辞数据库基础;添加自定义类型

beforeSave和结果格式化器

一个不那么干燥和紧密耦合的方法,基本上是你的2的端口。x代码,将使用beforeSave回调/事件和结果格式化程序。例如,结果格式化程序可以附加在beforeFind事件/回调中。

beforeSave中,您可以使用Entity::has(), Entity::get()Entity::set(),甚至使用数组访问,因为实体实现ArrayAccess

结果格式化器基本上是一个after find钩子,您可以使用它轻松地遍历结果并修改它们。

这是一个基本的例子,不需要更多的解释:

// ...
use CakeEventEvent;
use CakeORMQuery;
class PatientsTable extends Table
{
    // ...
    
    public $encryptedFields = [
        'patient_surname',
        'patient_first_name'
    ];
    
    public function beforeSave(Event $event, Entity $entity, ArrayObject $options)
    {
        foreach($this->encryptedFields as $fieldName) {
            if($entity->has($fieldName)) {
                $entity->set(
                    $fieldName,
                    Security::encrypt($entity->get($fieldName), Security::getSalt())
                );
            }
        }
        return true;
    }
    
    public function beforeFind(Event $event, Query $query, ArrayObject $options, boolean $primary)
    {
        $query->formatResults(
            function ($results) {
                /* @var $results CakeDatasourceResultSetInterface|CakeCollectionCollectionInterface */
                return $results->map(function ($row) {
                    /* @var $row array|CakeDataSourceEntityInterface */
                    
                    foreach($this->encryptedFields as $fieldName) {
                        if(isset($row[$fieldName])) {
                            $row[$fieldName] = Security::decrypt($row[$fieldName], Security::getSalt());
                        }
                    }
                    
                    return $row;
                });
            }
        );  
    }
    // ...
}

为了稍微解耦,您还可以将其移动到一个行为中,以便您可以轻松地跨多个模型共享它。

参见

  • 食谱比;数据库访问&ORM祝辞数据库基础;添加自定义类型
  • 食谱比;数据库访问&ORM祝辞查询生成器>新增计算字段
  • 食谱比;教程,例子在书签教程第2部分;持久化标签字符串
  • 食谱比;数据库访问&ORM祝辞行为
  • API比;蛋糕 数据源 EntityTrait
  • API比;表蛋糕 ORM

编辑:@npm关于虚拟属性不工作的说法是正确的。现在我很生自己的气,因为我回答得不好。我发帖前没有检查一下,真是活该。

为了使它正确,我实现了一个使用行为的版本,在读取字段时对其解密,并在将其写入数据库时对其加密。

注意:此代码目前不包含任何自定义查找器,因此它不支持通过加密字段进行搜索。

$this->Patient->findByPatientFirstname('bob'); // this will not work

/src/模型/行为/EncryptBehavior.php

<?php
/**
 * 
 */
namespace CakeORMBehavior;
use ArrayObject;
use CakeCollectionCollection;
use CakeDatasourceEntityInterface;
use CakeDatasourceResultSetInterface;
use CakeEventEvent;
use CakeORMBehavior;
use CakeORMEntity;
use CakeORMQuery;
use CakeORMTable;
use CakeORMTableRegistry;
use CakeUtilityInflector;
use CakeUtilitySecurity;
use CakeLogLog;
/**
 * Encrypt Behavior
 */
class EncryptBehavior extends Behavior
{
    /**
     * Default config
     *
     * These are merged with user-provided configuration when the behavior is used.
     *
     * @var array
     */
    protected $_defaultConfig = [
        'key' => 'YOUR_KEY_KERE', /* set them in the EntityTable, not here */
        'fields' => []
    ];

    /**
     * Before save listener.
     * Transparently manages setting the lft and rght fields if the parent field is
     * included in the parameters to be saved.
     *
     * @param CakeEventEvent $event The beforeSave event that was fired
     * @param CakeORMEntity $entity the entity that is going to be saved
     * @return void
     * @throws RuntimeException if the parent to set for the node is invalid
     */
    public function beforeSave(Event $event, Entity $entity)
    {
        $isNew = $entity->isNew();
        $config = $this->config();

        $values = $entity->extract($config['fields'], true);
        $fields = array_keys($values);
        $securityKey = $config['key'];
        foreach($fields as $field){ 
            if( isset($values[$field]) && !empty($values[$field]) ){
                $entity->set($field, Security::encrypt($values[$field], $securityKey));
            }
        }
    }
    /**
     * Callback method that listens to the `beforeFind` event in the bound
     * table. It modifies the passed query
     *
     * @param CakeEventEvent $event The beforeFind event that was fired.
     * @param CakeORMQuery $query Query
     * @param ArrayObject $options The options for the query
     * @return void
     */
    public function beforeFind(Event $event, Query $query, $options)
    {
        $query->formatResults(function ($results){
            return $this->_rowMapper($results);
        }, $query::PREPEND);
    }
    /**
     * Modifies the results from a table find in order to merge the decrypted fields
     * into the results.
     *
     * @param CakeDatasourceResultSetInterface $results Results to map.
     * @return CakeCollectionCollection
     */
    protected function _rowMapper($results)
    {
        return $results->map(function ($row) {
            if ($row === null) {
                return $row;
            }
            $hydrated = !is_array($row);
            $fields = $this->_config['fields'];
            $key = $this->_config['key'];
            foreach ($fields as $field) {
                $row[$field] = Security::decrypt($row[$field], $key);
            }
            if ($hydrated) {
                $row->clean();
            }
            return $row;
        });
    }
}

/src/模型/表/PatientsTable.php

<?php
namespace AppModelTable;
use AppModelEntityPatient;
use CakeORMQuery;
use CakeORMRulesChecker;
use CakeORMTable;
use CakeValidationValidator;
use CakeCoreConfigure;
/**
 * Patients Model
 *
 */
class PatientsTable extends Table
{
    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config)
    {
        parent::initialize($config);
        $this->table('patients');
        $this->displayField('id');
        $this->primaryKey('id');
        // will encrypt these fields automatically
        $this->addBehavior('Encrypt',[
            'key' => Configure::read('Security.key'),
            'fields' => [
                'patient_surname',
                'patient_firstname'
            ]
        ]);
    }
}

我能感受到你的痛苦。cakephp 3中的ORM层与cake2完全不同。他们将实体模型和表ORM拆分为两个不同的类,并删除了afterFind。我会考虑使用虚拟属性。我想它可能适合你的用例。

在下面的示例

<?php
namespace AppModelEntity;
use CakeORMEntity;
use CakeUtilitySecurity;
use CakeCoreConfigure;
class Patient extends Entity
{
    protected function _setPatientSurname($str)
    {
        $this->set('patient_surname', Security::encrypt($str, Configure::read('Security.key'));
    }
    protected function _setPatientFirstname($str)
    {
        $this->set('patient_firstname', Security::encrypt($str, Configure::read('Security.key'));
    }
    protected function _getPatientSurname()
    {
        return Security::decrypt($this->patient_surname, Configure::read('Security.key'));
    }
    protected function _getPatientFirstname()
    {
        return Security::decrypt($this->patient_first_name, Configure::read('Security.key'));
    }
}

最新更新