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,试试VARBINARY
或BLOB
)。如果您想存储字符串数据,那么您必须另外对加密/解密数据进行 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 中表单字段的加密/解密的主要内容,如果未能解决你的问题,请参考以下文章