【问题标题】:Encryption/Decryption of Form Fields in CakePHP 3CakePHP 3 中表单字段的加密/解密
【发布时间】:2015-08-27 22:07:34
【问题描述】:

我希望在添加/编辑某些表单字段时对其进行加密,并在通过 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 方法替换为虚拟字段,但我无法将它们全部放在一起。

【问题讨论】:

    标签: php cakephp cakephp-3.0


    【解决方案1】:

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

    自定义数据库类型

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

    这是一个简单的例子,假设 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/config/bootstrap.php

    注册自定义类型。

    use Cake\Database\Type;
    Type::map('crypted', 'App\Database\Type\CryptedType');
    

    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;
        }
    
        // ...
    }
    

    Cookbook > Database Access & ORM > Database Basics > Adding Custom Types

    beforeSave 和结果格式化程序

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

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

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

    这是一个基本示例,不需要过多解释:

    // ...
    
    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,试试VARBINARYBLOB)。如果您想存储字符串数据,那么您必须另外对加密/解密数据进行 base64 编码/解码,但是仍然不需要二进制字符集/排序规则,因为 base64 仅使用 ASCII 字符。跨度>
    • 非常感谢你的帮助伙伴,一切都像魅力一样!
    【解决方案2】:

    编辑:@npm 关于虚拟属性不起作用的说法是正确的。现在我很生气自己给出了一个错误的答案。为我在发布之前不检查它是正确的。

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

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

    例如。

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

    行为

    /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;
            });
        }
    }
    

    表格

    /src/Model/Table/PatientsTable.php

    <?php
    namespace App\Model\Table;
    
    use App\Model\Entity\Patient;
    use Cake\ORM\Query;
    use Cake\ORM\RulesChecker;
    use Cake\ORM\Table;
    use Cake\Validation\Validator;
    use Cake\Core\Configure;
    
    /**
     * 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 App\Model\Entity;
    
    use Cake\ORM\Entity;
    use Cake\Utility\Security;
    use Cake\Core\Configure;
    
    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'));
        }
    
    }
    

    【讨论】:

    • 这会失败的原因有很多,这里有几个: 在编组过程中,访问者将尝试访问不存在的属性。突变器将导致无限循环。当正确使用访问器和修改器时,您最终会在数据库中得到未加密的数据,因为在存储它们时从实体中读取值时,它将被解密。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-02
    相关资源
    最近更新 更多