erikwang2013/encryptable

Encryptable(erikwang2013/encryptable)

PHP 8.2+ 环境下,为敏感字段提供「可检索的匿名化 / 加密」能力:写入数据库前加密,经 Eloquent(或手动 API)读出时解密;同时可生成与 MySQL / PostgreSQL 兼容的 SQL 片段,便于在查询条件中对接已加密列。命名空间仍为 Maize\Encryptable\...(历史兼容)。

本仓库在思路与行为上参考并演进自开源项目 laravel-encryptable(Maize Tech)。若需对照原版设计、Issue 与发布说明,请优先查阅该上游仓库。


功能概览

  • Eloquent 自定义 Cast:在模型 $casts 中使用 Encryptable::class,自动加解密指定属性。
  • PHP 侧加解密Encryption::php()->encrypt() / decrypt(),适合命令行、队列、非模型场景。
  • 数据库表达式Encryption::db()->decrypt() 返回可在 SQL 中拼接的解密片段(MySQL / Postgres 语法分支),便于 whereRaw 等与密文列对照查询。
  • 校验规则UniqueEncryptedExistsEncryptedRule::uniqueEncrypted() / Rule::existsEncrypted() 宏(依赖 Laravel 的 illuminate/validation)。
  • 多运行时桥接:在 Laravel 10–12、基于 Illuminate 的 WebmanHyperf 2–3ThinkPHP 6–8 下通过各自方式注册容器与配置;无完整容器时可用 ENCRYPTION_KEYENCRYPTION_CIPHER、可选 ENCRYPTION_PREVIOUS_KEYS 兜底 Encryption::php()(各栈能力与接入方式见下文 「支持的框架」 表格)。
  • Composer 安装钩子:本包为 Composer 插件composer-plugin)。安装/更新时会读取 vendor/composer/installed.phpcomposer.lockcomposer.json,并结合目录结构判断当前项目栈,再按各框架官方路径写入默认配置;未识别则跳过。不会覆盖已有配置文件。详见 「安装 → Composer 插件」
  • 密钥优雅轮换(仅 Encryption::php() 路径):主密钥 + previous_keys / ENCRYPTION_PREVIOUS_KEYS 解密环,可选 rotateToCurrentKey() 渐进重加密;完整行为说明、上线步骤与配置入口见 「配置说明 → 密钥优雅轮换」

环境要求

项目 说明
PHP ^8.2,需启用 openssl 扩展
数据库 文档与实现针对 MySQLPostgreSQLEncryption::db() 的方言检测依赖驱动名)

Composer 包名:erikwang2013/encryptablecomposer.jsonname 字段)。


支持的框架

下表说明各栈的约定兼容范围、接入方式,以及本包能力与 Laravel 专属能力(Eloquent Cast、基于 illuminate/validation 的规则)的对应关系。实际以你项目锁定的 PHP / 框架小版本为准。配置文件在允许 Composer 插件时会自动安装(见「安装」);也可按下文表格手动 cp 或使用 vendor:publish

框架 版本(约定) 接入方式 Eloquent $castsEncryptable Encryption::php() Encryption::db() UniqueEncrypted / ExistsEncrypted 与宏
Laravel 10.x ~ 12.x(运行环境 PHP ≥ 8.2) Composer 包发现注册 EncryptableServiceProvider + config/plugin/erikwang2013/encryptable/app.php(或旧版 config/encryptable.php
Webman 1.x / 2.x,且项目已引入 Illuminate(database / support / validation) 注册 EncryptableServiceProvider + config/plugin/erikwang2013/encryptable/app.php(Composer 插件或手动) ✓(使用 Eloquent 时)
Hyperf 2.x / 3.x extra.hyperf.config 合并 Bridge\Hyperf\ConfigProvider + config/autoload/plugins/erikwang2013/encryptable.php(或旧版 config/autoload/encryptable.php —(非 Laravel 模型 Cast;可在实体/仓储中调用 Encryption::php() ✓(需安装 hyperf/db-connection —(依赖 Illuminate 校验栈)
ThinkPHP 6.x ~ 8.x ThinkphpEncryptable::register($app) + config/plugin/erikwang2013/encryptable/app.php(或旧版 config/encryptable.php —(请用模型获取器/修改器或类型字段自行调用 Encryption::php()

图例: ✓ 表示该能力在本栈有官方桥接或可直接使用; 表示本包未提供该栈的专用实现,需自行在业务层对接。


安装

1
composer require erikwang2013/encryptable

Composer 插件(自动发布配置)

本包为 "type": "composer-plugin",通过 extra.class 注册 Maize\Encryptable\Composer\Plugin。在 erikwang2013/encryptable 被安装或更新后,插件会:

  1. 按顺序汇总 Composer 包名(小写):vendor/composer/installed.php(及 installed.json)、composer.lock、根 composer.jsonrequire / require-dev。这样能反映 vendor 里真实已安装 的包(含传递依赖),避免根 composer.json 未写 workerman/webman 时误判。
  2. 若仍不足以判断,再结合项目目录特征(如 Webman:support/bootstrap.phpstart.php / windows.php 且存在 config/;Laravel:artisanbootstrap/app.php 等;Hyperf:bin/hyperf.phpconfig/autoload/server.php;ThinkPHP:根目录可执行文件 think)。
  3. 仅当识别到下方表格中的框架时才写入文件;目标文件已存在则不覆盖
  4. 仍无法识别时只输出跳过说明,避免在纯 PHP 库里误建 config/
识别到的依赖(示例) 遵循的官方约定 若缺失则创建的文件
laravel/frameworklaravel/lumen-framework Laravel 配置config/ 下 PHP(本包与 Webman 共用同一物理插件路径) config/plugin/erikwang2013/encryptable/app.php(由 EncryptableServiceProvider 合并为 config('encryptable.*')
workerman/webman / webman/* Webman 插件config/plugin/{vendor}/{name}/app.php config/plugin/erikwang2013/encryptable/app.phpconfig('plugin.erikwang2013.encryptable.app.*')
topthink/frameworktopthink/think ThinkPHP 8 配置文件 — 应用 config/;本包采用插件式子路径 config/plugin/erikwang2013/encryptable/app.php(在 ThinkphpEncryptable::register 中注入为 encryptable.*
hyperf/frameworkhyperf/hyperf Hyperf 配置config/autoload/ 下 PHP(相对路径映射为点号键) config/autoload/plugins/erikwang2013/encryptable.phpplugins.erikwang2013.encryptable.*,见 HyperfEncryptableConfig

Laravel / Lumen / ThinkPHP / Webman 共用 config/plugin/erikwang2013/encryptable/app.php(模板 config/stubs/plugin-app.php)。Hyperf 在新装且不存在旧版 config/autoload/encryptable.php 时写入 config/autoload/plugins/erikwang2013/encryptable.php;已有旧路径的项目保持不变,HyperfEncryptableConfig 优先读插件路径,再回退 encryptable.*。若同时识别多种栈(如单体仓库),则对每种缺失文件分别发布。

Composer 2.2+ 默认可能拦截插件,需在业务项目的 composer.json 中允许本包(配置一次即可;若已拒绝过可删 composer.lock 后重试或手动加):

1
2
3
4
5
"config": {
"allow-plugins": {
"erikwang2013/encryptable": true
}
}

也可在 composer require 时按 Composer 交互提示选择信任本插件。

配置文件(手动复制,可选)

若未启用插件或希望自行管理模板,可将包内对应文件复制到框架约定路径(再按需修改 .env 等)。

框架 复制源(vendor 内路径,包名以 erikwang2013/encryptable 为准) 复制到(项目内目标路径)
Laravel / Lumen / ThinkPHP / Webman vendor/erikwang2013/encryptable/config/stubs/plugin-app.php config/plugin/erikwang2013/encryptable/app.php
Laravel(可选旧版) vendor/erikwang2013/encryptable/config/encryptable.php config/encryptable.php(无插件路径时仍会被合并)
Hyperf(推荐) vendor/erikwang2013/encryptable/config/stubs/hyperf-plugin-autoload.php config/autoload/plugins/erikwang2013/encryptable.php
Hyperf(旧版) vendor/erikwang2013/encryptable/config/stubs/hyperf-autoload-encryptable.php config/autoload/encryptable.php(顶层 key / cipherencryptable.*

说明: plugin-app.php 为共用 stub(顶层 keycipher)。Webman 原生读取为 plugin.erikwang2013.encryptable.app.*Laravel / Lumen 合并到 encryptable.*ThinkPHPThinkphpEncryptable::register 中注入为 encryptable.*Hyperf 中 autoload 文件名与相对路径决定键名:推荐路径对应 plugins.erikwang2013.encryptable.*;旧文件 encryptable.php 对应 encryptable.*

Shell 示例:

1
2
3
4
5
6
7
# Laravel / Lumen / ThinkPHP / Webman(共用插件布局)
mkdir -p config/plugin/erikwang2013/encryptable
cp vendor/erikwang2013/encryptable/config/stubs/plugin-app.php config/plugin/erikwang2013/encryptable/app.php

# Hyperf(推荐)
mkdir -p config/autoload/plugins/erikwang2013
cp vendor/erikwang2013/encryptable/config/stubs/hyperf-plugin-autoload.php config/autoload/plugins/erikwang2013/encryptable.php

Laravel 也可使用 vendor:publish(会发布插件路径、可选旧版扁平文件及 Hyperf stub 路径):

1
php artisan vendor:publish --provider="Maize\Encryptable\EncryptableServiceProvider" --tag="encryptable-config"

Laravel(其余步骤)

安装后若已启用包自动发现,会注册 Maize\Encryptable\EncryptableServiceProvider。请确保存在 config/plugin/erikwang2013/encryptable/app.php(Composer 钩子或上表 / vendor:publish),或仅保留旧版 config/encryptable.php

Webman(使用 Illuminate 组件时)

按需安装 illuminate/databaseilluminate/supportilluminate/validation 等。确保存在 config/plugin/erikwang2013/encryptable/app.php(Composer 安装钩子或 vendor:publish 会生成),然后在插件或启动流程中注册:

Maize\Encryptable\EncryptableServiceProvider

运行时读取 config('plugin.erikwang2013.encryptable.app.key').cipher,见 Webman 插件配置说明

Hyperf

  1. 按上表复制或依赖安装钩子生成 config/autoload/plugins/erikwang2013/encryptable.php(或旧版 config/autoload/encryptable.php)。读取顺序为 plugins.erikwang2013.encryptable.*,再回退 encryptable.*
  2. 本包在 composer.jsonextra.hyperf.config 中声明了 Maize\Encryptable\Bridge\Hyperf\ConfigProvider,由 Hyperf 合并依赖注入配置。
  3. 若使用 Encryption::db(),请安装 hyperf/db-connection

ThinkPHP

  1. 推荐 config/plugin/erikwang2013/encryptable/app.php(与 Webman/Laravel 相同 stub)。若仍使用旧版 config/encryptable.php 亦可。
  2. 在应用启动阶段(例如服务注册或引导类中)调用:
1
\Maize\Encryptable\Bridge\ThinkPHP\ThinkphpEncryptable::register($app);

配置说明

复制或发布得到 config/plugin/erikwang2013/encryptable/app.php、旧版 config/encryptable.php,或 Hyperf 下 config/autoload/plugins/erikwang2013/encryptable.php / config/autoload/encryptable.php 后,核心项为:

说明
key 当前主密钥,对应 ENCRYPTION_KEY新密文始终用该密钥加密。
cipher 算法标识,默认 aes-128-ecb,对应 ENCRYPTION_CIPHER;需与数据库侧及既有数据约定一致。密钥环内所有密钥须使用同一 cipher
previous_keys 已下线但仍用于解密尝试的密钥列表,在 key 解密失败后再依次尝试。来自 ENCRYPTION_PREVIOUS_KEYS(逗号分隔或 JSON 数组)或配置项 previous_keys

无 Laravel / 未绑定容器时,Encryption::php() 可回退读取 ENCRYPTION_KEYENCRYPTION_CIPHERENCRYPTION_PREVIOUS_KEYS(见 EnvEncryptableConfig)。

密钥优雅轮换(应用层、零停机思路)

不强制全库立刻重加密的前提下更换主密钥:新写入始终用当前 ENCRYPTION_KEY(主密钥);历史密文只要其加密时所用的密钥仍在「密钥环」(主密钥或 previous_keys)内,即可继续解密。

行为说明(本包实现摘要)

方面 行为
配置契约 EncryptableConfigContract::getPreviousKeys(): array 表示仅用于解密兜底的已下线密钥列表,在主密钥 getKey() 无法解出合法明文后再按顺序尝试。Laravel(encryptable.previous_keys)、Webman 插件 app.php、Hyperf(plugins.* / encryptable.*)、ThinkPHP(encryptable.previous_keys)、无容器时的 EnvEncryptableConfigENCRYPTION_PREVIOUS_KEYS)均已对接。
解析 Maize\Encryptable\Support\PreviousKeysParser 将环境变量或配置统一为 list<string>:支持逗号分隔、["k1","k2"] 形式的 JSON 字符串、或 PHP 数组。
密钥环 基类 Encrypter::getDecryptionKeyRing() 生成 [主密钥, …previous_keys],去空、去与主密钥重复项。环内所有密钥须与主密钥使用同一 cipher
加密 PHPEncrypter::encrypt() 仅使用主密钥getKey()),不会用 previous_keys 加密。
解密与 isEncrypted() 对每个候选密钥尝试 OpenSSL 解密;仅当解密结果以包内约定的脏前缀(crypt:)开头时才视为成功,避免把错误密钥产生的乱码当成明文。Eloquent Encryptable 等经 Encryption::php() 的路径与此一致。
重加密 API PHPEncrypter::rotateToCurrentKey(?string $payload, bool $serialize = true):先用密钥环解密,再用当前主密钥加密。Encryption::php()->rotateToCurrentKey(...) 为其代理。非密文原样返回,null 仍为 null
不适用场景 Encryption::db() / DBEncrypter:生成 SQL 时只嵌入主密钥不读取 previous_keys。数据库函数侧密文轮换需在应用层或迁移脚本中解密再写回,不能依赖本节的密钥环。

建议上线步骤(运维)

  1. 密钥写入 ENCRYPTION_KEY;将旧主密钥加入 ENCRYPTION_PREVIOUS_KEYS(或配置的 previous_keys)。若有多枚旧钥,建议最近退役的排在前面
  2. 发布部署后,读路径自动按「主 → 旧」尝试,业务无需停机切换密文。
  3. (可选)用队列/命令对字段逐条调用 Encryption::php()->rotateToCurrentKey($密文)$serialize 须与写入时一致),使密文逐步改为新主密钥加密;之后可缩短 previous_keys 列表。
  4. previous_keys 中删除某一旧密钥前,须确认已无任何数据仍依赖该密钥加密,否则对应行将无法解密。

配置入口一览

来源 键名
环境变量 ENCRYPTION_KEYENCRYPTION_CIPHERENCRYPTION_PREVIOUS_KEYS
Laravel / 合并后的 encryptable keycipherprevious_keys(见包内 config/encryptable.php 或已发布的插件 app.php
Webman 插件 app.php 同上;框架侧读取路径为 plugin.*.app.*
Hyperf autoload 模板 keycipherprevious_keys(推荐 plugins.erikwang2013.encryptable.* 或旧版 encryptable.*

多个旧密钥怎么配

解密顺序固定为:先用当前主密钥 ENCRYPTION_KEY,不成功再按 previous_keys 数组顺序依次尝试。有多枚旧钥时,建议把最近刚下线的主密钥排在最前,更早的排在后面。

方式一:环境变量 ENCRYPTION_PREVIOUS_KEYS

  • 英文逗号分隔(每项首尾空格会被去掉):
1
ENCRYPTION_PREVIOUS_KEYS=oldKeyOne16bytes!!,olderKeyTwo16byte,ancientKeyThree16b
  • JSON 数组(整段写在 .env 一行;若某枚密钥里可能含逗号,优先用这种方式):
1
ENCRYPTION_PREVIOUS_KEYS=["oldKeyOne16bytes!!","olderKeyTwo16byte","ancientKeyThree16b"]

方式二:Laravel / ThinkPHP 合并配置 encryptable.previous_keys

config/encryptable.php 或插件 app.php 里写 PHP 数组(顺序即尝试顺序;须与当前 cipher 要求的长度一致,例如 aes-128-ecb 常见为 16 字符):

1
2
3
4
5
'previous_keys' => [
'oldKeyOne16bytes!!',
'olderKeyTwo16byte',
'ancientKeyThree16b',
],

包内默认也可用 PreviousKeysParser::parse(env('ENCRYPTION_PREVIOUS_KEYS')),与方式一组合:环境变量里放多枚,由解析器转成数组。

方式三:Webman app.php、Hyperf autoload

字段名同样是 previous_keys:要么与方式二相同的 PHP 数组,要么 PreviousKeysParser::parse(env('ENCRYPTION_PREVIOUS_KEYS')),与英文文档一致。


使用说明

1. 模型 Cast(Laravel / 使用 Eloquent 的 Webman)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Maize\Encryptable\Encryptable;

class User extends Model
{
protected $fillable = ['name', 'email'];

protected $casts = [
'name' => Encryptable::class,
'email' => Encryptable::class,
];
}

读写该模型属性时,会自动经过 PHP 侧加解密。

2. 手动加解密(PHP)

1
2
3
4
5
6
use Maize\Encryptable\Encryption;

$plain = '需要保护的值';
$cipher = Encryption::php()->encrypt($plain);

$restored = Encryption::php()->decrypt($cipher);

3. 生成 SQL 中的解密表达式(DB)

1
2
3
4
use Maize\Encryptable\Encryption;

// 返回可在 SELECT / WHERE 中使用的片段(具体语法随 MySQL / Postgres 变化)
$expr = Encryption::db()->decrypt($encryptedPayloadFromDb);

DBEncrypter::encrypt() 会抛出「不支持」异常;本类主要面向「在 SQL 中解密比对」的场景。

4. 自定义验证规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Maize\Encryptable\Rules\ExistsEncrypted;
use Maize\Encryptable\Rules\UniqueEncrypted;

Validator::make($data, [
'email' => [
'required',
'email',
new ExistsEncrypted('users'),
// 或 Rule::existsEncrypted('users')
],
]);

Validator::make($data, [
'email' => [
'required',
'email',
new UniqueEncrypted('users'),
// 或 Rule::uniqueEncrypted('users')
],
]);

依赖与 Composer 说明

  • 正式依赖仅包含 phpext-opensslpsr/container,避免在 Hyperf 等栈中强行拉入整套 Illuminate。
  • Laravel / Webman(Illuminate):请通过 laravel/framework 或按需引入的 illuminate/* 满足 EncryptableServiceProvider、Cast 与验证规则对 Illuminate 的要求(详见 composer.jsonsuggest 段)。

开发与测试

1
2
3
4
composer install
composer test # Pest
composer analyse # PHPStan(需项目内配置)
composer format # Laravel Pint

参考与致谢

  • 上游参考实现maize-tech/laravel-encryptable — 原始 Laravel 包的设计、文档与社区讨论请以该仓库为准。
  • 本分支在 多框架桥接Composer 元数据配置与容器解析等方面做了扩展与调整;若你正在从上游迁移,请对照两边的 CHANGELOG 与配置项。

许可证

MIT。详见仓库内 LICENSE.md