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

Posted

技术标签:

【中文标题】CakePHP 3 中表单字段的加密/解密【英文标题】:Encryption/Decryption of Form Fields in CakePHP 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 方法替换为虚拟字段,但我无法将它们全部放在一起。

【问题讨论】:

【参考方案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;
                );
            
        );  
    

    // ...

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

另见

Cookbook > Database Access & ORM > Database Basics > Adding Custom Types Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields Cookbook > Tutorials & Examples > Bookmarker Tutorial Part 2 > Persisting the Tag String Cookbook > Database Access & ORM > Behaviors API > \Cake\Datasource\EntityTrait API > \Cake\ORM\Table

【讨论】:

我尝试了您的第一种方法,但是当我尝试保存时,我得到一个“不正确的字符串值:对于第 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'));
    


【讨论】:

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

以上是关于CakePHP 3 中表单字段的加密/解密的主要内容,如果未能解决你的问题,请参考以下文章

如何在php中加密/解密数据?

怎么对已经加密压缩文件进行解密

lua.e2加密文件怎么解密?

用c#做aes加密 为啥在线解密解不了

vue中如何做加密登陆

SHA256 加密后能不能解密