
后端Web框架【免费下载链接】symfonyThe Symfony PHP framework项目地址https://gitcode.com/GitHub_Trending/sy/symfony点击查看免费下载Symfony 在src/Symfony/Component/KeyManagement中提供了一个全新的 KeyManagement 组件而src/Symfony/Component/KeyManagement/Bridge/DoctrineDbal是它面向 Doctrine DBAL 的桥接包symfony/doctrine-dbal-key-management核心产物是一个名为EncryptedType的 DBAL 类型。它把任意一个现有 DBAL 类型“装饰”上列级加密写入时值先经过父类型转换、再被EnvelopeEncrypterInterface包成 KeyManagement 信封Envelope读取时反向解密再交还父类型。本文基于该桥接包官方 README 与仓库源码完整讲解注册方式、存储式数据密钥表的设计、跨 KMS 迁移流程与全部权衡取舍读完即可在纯 DBAL 或 Doctrine ORM 项目中落地列级透明加密。注意该 Bridge 目前标记为experimental实验性不享受 Symfony 的向后兼容承诺Backward Compatibility Promise升级时需关注 CHANGELOG.md。核心概念一个装饰型的 DBAL 加密类型EncryptedType的构造函数接收三个参数见 EncryptedType.php父类型Type被装饰的 DBAL 类型如string、text、integer、json等信封加密器同时实现EnvelopeEncrypterInterface与EnvelopeDecrypterInterface的对象密钥名$key具体含义由加密器决定见下文。它的写入路径convertToDatabaseValue()先把值交给父类型的convertToDatabaseValue()做常规规范化JSON 编码、日期格式化等再调用$envelopes-encrypt($key, bytes($plaintext))包成信封读取路径convertToPHPValue()则先把字节解析回Envelope解密后交给父类型的convertToPHPValue()。源码中对 float 做了特殊处理非有限值直接抛异常有限值用var_export()转字符串避免(string)强转受precisionini 设置影响而丢失精度。每个值最终以二进制形式存储自包含信封self-contained信封内携带自己的、被主密钥包装过的数据加密密钥data key存储引用store-backed信封只携带一个 16 字节引用指向数据库中保存的数据密钥行。无论哪种方式主密钥master key始终由 KMS 掌控这是整个设计的根基。基本用法纯 DBAL 与 Doctrine ORM纯 DBAL 应用先构造类型并注册到全局类型注册表use Doctrine\DBAL\Types\Type; use Symfony\Component\KeyManagement\Bridge\DoctrineDbal\EncryptedType; $type new EncryptedType( Type::getTypeRegistry()-get(string), $envelopeEncrypter, alias/app-key, ); Type::getTypeRegistry()-register(app_user_email, $type);注册名app_user_email之后可以在 DBAL 接受类型的任何地方使用——不需要任何 ORM 参与$connection-insert(user, [email janeexample.com], [email app_user_email]); $email $connection-convertToPHPValue( $connection-fetchOne(SELECT email FROM user WHERE id ?, [$id]), app_user_email, );Doctrine ORM 实体ORM 实体像引用其他类型一样引用这个注册名use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class User { #[ORM\Id, ORM\GeneratedValue, ORM\Column] public ?int $id null; #[ORM\Column(type: app_user_email)] public string $email; }父类型的自由度从 EncryptedTypeTest.php 的测试可以看出父类型可以是StringType、IntegerType、FloatType等任意类型均能正确往返round-trip。浮点数的往返在测试中被特别验证testAFloatParentRoundTripsExactly印证了源码中对浮点精度的处理逻辑。在哪里注册类型时机与位置都很关键文档强调两个铁律按(信封加密器, 主密钥 id, 父类型)组合各注册一个实例且必须在第一次查询之前、每个请求都注册绝不能推迟到 Doctrine ORM 的loadClassMetadata事件元数据来自缓存时DoctrineBundle 在生产环境默认开启缓存该事件不会触发缓存的字段映射会引用一个注册表中不存在的类型名导致每次读取都抛UnknownColumnType。类型应和应用声明其余 Doctrine 配置的地方一起声明。为什么不能放在doctrine.dbal.typesdoctrine.dbal.types只能指定一个类名DoctrineBundle 会无参实例化它而EncryptedType需要接收一个加密器——它是服务不是字符串。因此正确做法是声明EncryptedTypes每个加密器一个服务services: app.encrypted_types: class: Symfony\Component\KeyManagement\Bridge\DoctrineDbal\EncryptedTypes public: true arguments: $envelopes: key_management.stored_envelope_encrypter $types: app_user_email: { type: string, key: user.email } app_user_notes: { type: text, key: user.notes }$types映射中每项必填type被包装的 DBAL 类型名与key密钥或作用域名EncryptedTypes::register()会对缺失或非字符串的值抛出InvalidArgumentException见 EncryptedTypes.php。在容器构建完成后调用一次即 Kernel boot// src/Kernel.php public function boot(): void { parent::boot(); $this-container-get(app.encrypted_types)-register(); }boot 足够早能一次性覆盖所有入口点前端控制器、带 schema 工具和迁移的 console、测试内核。而且几乎零成本DBAL 连接只有在第一次查询时才真正触达数据库。延迟注册才是行不通的。连接本身也不行——尽管doctrine.dbal.connection_factory看起来诱人存储型加密器需要连接让连接来提供类型会形成容器无法编译的循环依赖。类型与加密器一一对应类型拿到哪个加密器就决定了该列处于哪种加密体制。一个实体同时持有两种列就需要两个服务每个加密器一个各自注册自己的名字换环境时只需换一个参数。重复注册是安全的调用两次register()是安全的这正是内核重启reboot时的场景类型注册表是全局的、比容器活得久所以已占用的名字会被替换而不是拒绝源码中$registry-has($name) ? $registry-override(...) : $registry-register(...)。把数据密钥放进表DataKeyStore工作方式DataKeyStore把包装过的数据密钥持久化到一张 DBAL 表里于是载荷只携带引用而不是密钥本身。好处是KMS 每个数据密钥、每个进程只联系一次而不是每个加密值一次主密钥可以轮换或整体换成另一个 provider——只需重包装rewrap这些行无需重写任何载荷。use Symfony\Component\KeyManagement\Bridge\DoctrineDbal\DataKeyStore; use Symfony\Component\KeyManagement\StoredEnvelopeEncrypter; $store new DataKeyStore($connection, $clients, aws, alias/app-key); $store-createTable(); $encrypter new StoredEnvelopeEncrypter($store); $envelope $encrypter-encrypt(user.email, janeexample.com);EncryptedType像接收任何加密器一样接收它此时第三个参数命名的是scope作用域而非主密钥——因为StoredEnvelopeEncrypter就是把它当作用域读的见 StoredEnvelopeEncrypter.php 中$this-store-current($key)的调用Type::getTypeRegistry()-register(app_user_email, new EncryptedType( Type::getTypeRegistry()-get(string), $encrypter, user.email, ));该列的所有行共享一个数据密钥每行携带 16 字节引用DataKeyStore::rotate()中Uuid::v7()-toBinary()生成见 DataKeyStore.php。渐进式迁移存量数据若某列已经存了自包含信封给存储型加密器加一个 fallback 并保持注册名不变即可旧行仍通过 KMS 读取新行引用存储的密钥$encrypter new StoredEnvelopeEncrypter($store, new EnvelopeEncrypter($kms));解密路径会根据信封是否携带reference自动分派见StoredEnvelopeEncrypter::decrypt()没有 fallback 时遇到自包含信封会直接拒绝而不是静默误处理。表结构恰好五列不多一列列类型作用idBINARY(16)UUIDv7主键也是每个载荷记录下来的引用scopeVARCHAR(191)数据密钥共享的单位一列、一个租户、一种用途key_materialBLOB被包装过的数据密钥master_key_idVARCHAR(255)负责包装它的主密钥clientVARCHAR(64)配置好的、能解开它的 KMS 客户端源码中buildSchemaTable()DataKeyStore.php与configureSchemaTable()精确地按这五列建表并在scope id上建了key_management_data_keys_scope_idx索引。几个设计要点源码与文档相互印证刻意没有时间戳列UUIDv7 自带创建时刻且按时间排序所以“某作用域最新一行就是当前密钥”由current()里的ORDER BY id DESC免费得出密钥的“退役年龄”也能从引用本身读回来retirementOf()用Uuid::fromBinary($reference)解析并缓存。scope 超过列宽会被拒绝而非截断checkScope()校验MAX_SCOPE_LENGTH 191字节超长抛InvalidArgumentException——防止 MySQL 非严格模式下截断导致两个长 scope 共享一个密钥。列跟随连接的 collation在不区分大小写的 collation 下user.email与User.Email被视为同一个 scope。密钥在铸造 30 天后退役DEFAULT_MAX_AGE_SECONDS 259200030 天可用max_age调整。这不是卫生洁癖而是格式要求每个载荷在 AES-256-GCM 下用随机 96-bit IV 密封NIST SP 800-38D 将该 IV 用量上限定在每密钥 2^32 个载荷30 天相当于一个 scope 内每秒约 1650 个载荷。传null则关闭轮换把这个上限定交给应用自己。退役从不删除行所以它密封过的数据永远可读。表名可配置但需要引号、保留字如order或带连字符的名字不支持与 Lock、Messenger 存储的约束一致。configureSchema()会把这张表并入 Doctrine 生成的 schema与 Lock/Messenger 表被识别的机制相同symfony/doctrine-orm-key-management提供了调用它的监听器KeyManagementBundle把它注册在存储旁边脱离这套布线时用createTable()或自己的迁移建表。Symfony 布线key_management.store把这一切串起来——DataKeyStoreInterface解析为 storeEnvelopeEncrypterInterface注入的是存储型加密器其背后是默认客户端的加密器所以 store 之前写入的载荷继续可读各客户端的加密器通过#[Target(name)]获取。内存中的密钥与事务语义scope 只解析一次然后常驻已解包这正是省下往返的来源。唯一例外是事务内创建的密钥其行每次使用都会被重新查询直到该事务被看到提交——这样事务回滚把行带走时能被发现而不是继续用它加密。常驻也意味着 store 内存里持有明文密钥材料forget()会释放它们Symfony 布线通过kernel.reset标签在工作单元之间调用它所以长驻 worker 的每个请求都从零状态开始。remembered()的判定逻辑明文仍持有、年龄未到、行未被回滚完整地写在源码中。添加你自己的列所有查询都显式命名那五列所以多余列对 store 不可见——只要它们是 nullable 或有默认值插入时不会去填。如何声明取决于谁拥有这张表你拥有表跳过createTable()不要接configureSchema()在自己的迁移里声明五列加你自己的列。这是最简单、也是表一但承载 store 不知道的东西就应优先选择的路线。Doctrine 拥有 schema若 schema 监听器调用了configureSchema()装配出的 schema 只声明五列而数据库里更多doctrine:schema:update会提议删除多余列。在自己的postGenerateSchema监听器里把列加到同一张表让两边一致。在创建密钥时填充rotate()返回的句柄其reference就是该行主键所以可以用装饰器写其余字段final class TenantScopedStore implements RewrappableDataKeyStoreInterface { public function __construct( private DataKeyStore $inner, private Connection $connection, private string $tenantId, ) { } public function rotate(string $scope): DataKeyHandle { $handle $this-inner-rotate($scope); $this-connection-update( key_management_data_keys, [tenant_id $this-tenantId], [id $handle-reference], [id ParameterType::BINARY], ); return $handle; } // current(), get(), all() 和 rewrap() 委托给 $this-inner }把列迁移到另一个 KMS关于 KMS 的一切都碰不到实体、映射或注册的类型名EncryptedType只知道一个信封加密器和一个$key。换 provider 就是换递给类型的加密器代价取决于列里现在是什么。迁移期间两个客户端可同时配置Symfony 应用中它们的加密器通过#[Target(aws)]与#[Target(azure)]可用。情况一行引用存储的数据密钥Doctrine 侧什么都不用做。步骤在新客户端旁声明旧客户端用命令搬数据密钥key-management:rewrap-data-keys --fromaws --toazure --key-id...把 store 指向新客户端之后创建的密钥在那里包装。不读取也不重写任何载荷引用保持不变。当--fromaws --dry-run列出 0 条时旧客户端就可以下线了。RewrapDataKeysCommandRewrapDataKeysCommand.php的实现要点逐行解包、重包装、更新逐行提交所以中断后重跑同一条命令即可续传已被搬走的行不再被--from列出--dry-run只列出不动手。它还提示数据密钥在重包装期间会以明文经过本进程组件尚未抽象 AWS KMSReEncrypt这类服务端重加密原语。如果 store 用CompositeKms包装会记录该客户端所以它的行同时被每个成员包装、由任一成员读取。彻底失去一个成员时先改成员列表再让同一个命令从 composite 客户端跑到它自己——把所有密钥包到新列表下。情况二行自带包装过的数据密钥每一行都由写入它的 provider 的主密钥包装换 provider 意味着重写每一行未重写的行仍需旧客户端才能读。用一个按 key id 路由的解密器让两边在重写期间都可读且所有新写入都走新 providerfinal class MigratingEnvelopes implements EnvelopeEncrypterInterface, EnvelopeDecrypterInterface { public function __construct( private EnvelopeEncrypterInterfaceEnvelopeDecrypterInterface $target, private EnvelopeDecrypterInterface $legacy, ) { } public function encrypt(string $key, #[\SensitiveParameter] string $plaintext, string $aad ): Envelope { return $this-target-encrypt($key, $plaintext, $aad); } public function decrypt(Envelope $envelope, string $aad ): string { return str_starts_with($envelope-keyId ?? , arn:aws:kms:) ? $this-legacy-decrypt($envelope, $aad) : $this-target-decrypt($envelope, $aad); } }用这个加密器注册类型、部署然后重写列。要走 DBAL 而不是 ORM解密前后的值相同工作单元unit of work会算出空变更集永远不发UPDATE$platform $connection-getDatabasePlatform(); $legacy new EncryptedType($parent, new EnvelopeEncrypter($awsKms), alias/app-key); $target new EncryptedType($parent, $encrypter, user.email); foreach ($connection-iterateAssociative(SELECT id, email FROM user ORDER BY id) as $row) { $connection-update( user, [email $target-convertToDatabaseValue($legacy-convertToPHPValue($row[email], $platform), $platform)], [id $row[id]], [email ParameterType::BINARY], ); }全部重写完后只用目标加密器注册类型并下线旧客户端。这一趟是“改变列保护方式却不用付两次代价”的唯一机会所以值得直接写成存储型加密器如上面$target的做法今天的代价相同而下一次换 provider 就退化成情况一——一条命令一行都不用读。同一 provider 内轮换主密钥这完全是另一回事。key id 随信封携带、每次读取交还 KMS所以旧密钥下写的行只要该密钥仍可用就一直能解析什么也不必重写。Requirements环境要求PHP 8.4.1Doctrine DBAL 4.3——它放开了Type的final构造器加密类型才得以声明自己的参数composer.json中doctrine/dbal: ^4.3见 composer.json一个由应用配置好的EnvelopeEncrypterInterface通常经KeyManagementBundle的key_management配置并通过symfony/key-management提供。权衡取舍Trade-offs没有数据密钥存储时每次加密、每次解密各一次 KMS 往返数据密钥不缓存解包后的密钥存活时间不超过一次列写入或读取每行携带自己的包装数据密钥每个加密列约几百字节开销行间无共享状态主密钥无法轮换除非解密并重写引用它的每一行。有存储时前两条反转、第三条消失KMS 每数据密钥每进程一次、每行只带 16 字节引用、换主密钥或换 provider 只重包装密钥行。代价是自足性读一行现在同时需要 store 和 KMS与隔离性整个 scope 共享一个数据密钥。无论哪种方式都要知道的三件事没有精确匹配索引相同明文的两行加密后密文不同因为 nonce 每次都重新抽取。需要按列查询时在兄弟列上存盲索引用应用机密作 HMAC 密钥对明文做的 HMAC。列是无界的类型声明为LONGBLOB、BYTEA等无论映射里length是什么——那个长度描述的是明文而密文会因信封的帧结构超过它按它定长的列会截断密文并连带毁掉认证标签。这与 EncryptedType.php 源码注释、以及getSQLDeclaration()中unset($column[length])的实现完全一致。密文不绑定它的行类型不传 AAD因为convertToDatabaseValue()永远看不到它正在转换的表、列或行。在同一 scope或同一主密钥内一个加密值被拷进同类型的另一行也能在那里解密。当“值被挪到别的行”必须可检测时自己绑定把行身份放进载荷解密后校验。小结这个 Doctrine DBAL Bridge 的价值在于把“KMS 信封加密”与“Doctrine 类型系统”无缝接合纯 DBAL 与 ORM 应用共用同一种EncryptedTypeDataKeyStore把 KMS 往返从每值一次降到每数据密钥每进程一次key-management:rewrap-data-keys让跨 provider 迁移不必碰任何载荷。把主密钥真正留在 KMS、把密钥材料与引用分开管理、按 scope 设计共享粒度是这一整套设计的三个关键杠杆。相关测试可继续在 EncryptedTypeTest.php、DataKeyStoreTest.php 与 EncryptedTypesTest.php 中验证组件本身的架构与本地 KMS 实现见 KeyManagement README。赞分享后端Web框架【免费下载链接】symfonyThe Symfony PHP framework项目地址https://gitcode.com/GitHub_Trending/sy/symfony点击查看免费下载相关推荐Boto3 实战使用 AWS KMS 信封加密Envelope Encryption加密与解密文件Boto3 实战使用 AWS KMS 信封加密Envelope Encryption加密与解密文件 导读 本文基于 Boto3 官方文档中的 KMS 文件后端云原生Tink Python 信封加密Envelope Encryption实战用 Cloud KMS 封装 DEK 加解密文件Tink Python 信封加密Envelope Encryption实战用 Cloud KMS 封装 DEK 加解密文件 导读 本文基于 Tink Py密码学使用 Tink 在 Python 中实现 GCS 客户端侧加密GCS 信封加密Envelope Encryption实战使用 Tink 在 Python 中实现 GCS 客户端侧加密GCS 信封加密Envelope Encryption实战 本指南围绕仓库中 python/密码学应用安全上一篇Godot-MCP终极指南5分钟用自然语言彻底改变你的游戏开发方式下一篇TDengine Anode 管理实战指南TDgpt 分析节点的启动、配置与集群注册创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考