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

CakePHP 3中表单字段的加密/解密,php,cakephp,cakephp-3.0,Php,Cakephp,Cakephp 3.0,我希望在添加/编辑表单字段时对其进行加密,并在通过cake查找表单字段时对其进行解密。 以下是在v2.7.2中适用于我的代码: core.php Configure::write('Security.key','secretkey'); app/model/patient.php public $encryptedFields = array('patient_surname', 'patient_first_name'); public function beforeSave($opti

我希望在添加/编辑表单字段时对其进行加密,并在通过cake查找表单字段时对其进行解密。 以下是在v2.7.2中适用于我的代码:

core.php

Configure::write('Security.key','secretkey');
app/model/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方法替换为虚拟字段,但我无法将其全部放在一起。

Edit:@npm关于虚拟属性不工作的说法是正确的。现在我为自己给出了一个糟糕的答案而生气。在我发布之前没有检查它,这是对的

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

注意:此代码当前未包含任何自定义查找程序,因此不支持通过加密字段进行搜索

例如

行为

/src/Model/Behavior/EncryptBehavior.php

<?php
/**
 * 
 */
namespace Cake\ORM\Behavior;

use ArrayObject;
use Cake\Collection\Collection;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\ResultSetInterface;
use Cake\Event\Event;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
use Cake\Utility\Security;
use Cake\Log\Log;

/**
 * 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 \Cake\Event\Event $event The beforeSave event that was fired
     * @param \Cake\ORM\Entity $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 \Cake\Event\Event $event The beforeFind event that was fired.
     * @param \Cake\ORM\Query $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 \Cake\Datasource\ResultSetInterface $results Results to map.
     * @return \Cake\Collection\Collection
     */
    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;
        });
    }
}

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

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

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

src/Database/Type/CryptedType.php

这应该是相当自我解释的,在转换到数据库时加密,在转换到PHP时解密

<?php
namespace App\Database\Type;

use Cake\Database\Driver;
use Cake\Database\Type;
use Cake\Utility\Security;

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/Model/Table/PatientsTable.php

最后,将可加密列映射到已注册的类型,就是这样,从现在起,所有内容都将自动处理

// ...

use Cake\Database\Schema\Table as Schema;

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

    // ...
}
请参见

beforeSave和结果格式化程序 一种不那么枯燥、耦合更紧密的方法,基本上是2.x代码的一个端口,就是使用
beforeSave
callback/event和结果格式化程序。例如,结果格式化程序可以附加到
beforeFind
事件/回调中

beforeSave
中,只需设置/获取传递的实体实例的值,就可以利用
entity::has()
entity::get()
entity::set()
,甚至可以使用数组访问,因为实体实现了
ArrayAccess

结果格式化程序基本上是一个afterfind钩子,您可以使用它轻松地迭代结果并修改它们

下面是一个基本示例,不需要进一步解释:

// ...

use Cake\Event\Event;
use Cake\ORM\Query;

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 \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
                return $results->map(function ($row) {
                    /* @var $row array|\Cake\DataSource\EntityInterface */
                    
                    foreach($this->encryptedFields as $fieldName) {
                        if(isset($row[$fieldName])) {
                            $row[$fieldName] = Security::decrypt($row[$fieldName], Security::getSalt());
                        }
                    }
                    
                    return $row;
                });
            }
        );  
    }

    // ...
}
为了稍微解耦,您还可以将其移动到行为中,以便在多个模型中轻松共享

另见


这将由于许多原因而失败,以下是一些原因:在编组过程中,访问者将尝试访问不存在的属性。突变子会导致内胎循环。正确使用访问器和变异器时,数据库中会出现未加密的数据,因为在存储数据时从实体中读取值时,这些数据将被解密。我尝试了第一种方法,但在尝试保存时,出现了“错误的字符串值:对于第1行的“patient_first_name”列”错误。列的格式为utf8_bin@DanielHolguin二进制类型的列不需要字符集或排序规则,我的意思是,它是二进制数据,因此我怀疑您使用了错误的列类型(假设您使用的是MySQL,请尝试
VARBINARY
BLOB
)。如果您想存储字符串数据,那么您必须另外对加密/解密的数据进行base64编码/解码,但是仍然不需要二进制字符集/排序规则,因为base64只使用ASCII字符。我非常感谢您的帮助,伙计,它的一切工作都像一个符咒!
<?php
namespace App\Database\Type;

use Cake\Database\Driver;
use Cake\Database\Type;
use Cake\Utility\Security;

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());
    }
}
use Cake\Database\Type;
Type::map('crypted', 'App\Database\Type\CryptedType');
// ...

use Cake\Database\Schema\Table as Schema;

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

    // ...
}
// ...

use Cake\Event\Event;
use Cake\ORM\Query;

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 \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
                return $results->map(function ($row) {
                    /* @var $row array|\Cake\DataSource\EntityInterface */
                    
                    foreach($this->encryptedFields as $fieldName) {
                        if(isset($row[$fieldName])) {
                            $row[$fieldName] = Security::decrypt($row[$fieldName], Security::getSalt());
                        }
                    }
                    
                    return $row;
                });
            }
        );  
    }

    // ...
}