Encryption/Decryption 个 CakePHP 3 中的表单字段
Encryption/Decryption of Form Fields in CakePHP 3
我想在 added/edited 时对一些表单域进行加密,并在它们被 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 方法,但我无法将它们放在一起。
编辑:@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;
});
}
}
Table
/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 完全不同。他们将实体模型和 table ORM 拆分为两个不同的 类,并且 afterFind 已被删除。我会看一下使用虚拟属性。我认为您的用例可能是 suitable。
示例如下。
<?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'));
}
}
解决这个问题的方法不止一种(请注意,以下代码是未经测试的示例代码!在使用这些代码之前,您应该先掌握新的基础知识)。
自定义数据库类型
一种是自定义数据库类型,它会在将值绑定到数据库语句时加密,并在获取结果时解密。这是我更喜欢的选项。
这是一个简单的例子,假设数据库列可以保存二进制数据。
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 和结果格式化程序
一种不那么枯燥和更紧密耦合的方法,基本上是您的 2.x 代码的一个端口,将使用 beforeSave
callback/event 和结果格式化程序。例如,结果格式化程序可以附加在 beforeFind
event/callback.
中
在beforeSave
中只是set/get值to/from传递的实体实例,你可以利用Entity::has()
、Entity::get()
和Entity::set()
,甚至使用数组访问,因为实体实现 ArrayAccess
.
结果格式化程序基本上是一个 after find 挂钩,您可以使用它轻松地迭代结果并修改它们。
这是一个基本示例,不需要太多进一步解释:
// ...
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
我想在 added/edited 时对一些表单域进行加密,并在它们被 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 方法,但我无法将它们放在一起。
编辑:@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;
});
}
}
Table
/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 完全不同。他们将实体模型和 table ORM 拆分为两个不同的 类,并且 afterFind 已被删除。我会看一下使用虚拟属性。我认为您的用例可能是 suitable。
示例如下。
<?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'));
}
}
解决这个问题的方法不止一种(请注意,以下代码是未经测试的示例代码!在使用这些代码之前,您应该先掌握新的基础知识)。
自定义数据库类型
一种是自定义数据库类型,它会在将值绑定到数据库语句时加密,并在获取结果时解密。这是我更喜欢的选项。
这是一个简单的例子,假设数据库列可以保存二进制数据。
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 和结果格式化程序
一种不那么枯燥和更紧密耦合的方法,基本上是您的 2.x 代码的一个端口,将使用 beforeSave
callback/event 和结果格式化程序。例如,结果格式化程序可以附加在 beforeFind
event/callback.
在beforeSave
中只是set/get值to/from传递的实体实例,你可以利用Entity::has()
、Entity::get()
和Entity::set()
,甚至使用数组访问,因为实体实现 ArrayAccess
.
结果格式化程序基本上是一个 after find 挂钩,您可以使用它轻松地迭代结果并修改它们。
这是一个基本示例,不需要太多进一步解释:
// ...
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