- 后端
- Web框架
【免费下载链接】CodeIgniter4
Open Source PHP Framework (originally from EllisLab)
CodeIgniter 4 的 Encryption Service 提供开箱即用的双向对称加密能力,通过统一的service('encrypter')接口即可完成加密与解密,底层可自动切换 OpenSSL 与 Sodium 两种加密扩展。本文以官方用户指南 libraries/encryption.rst 为主体,结合 系统源码 与 测试用例 深入讲解配置项、密钥生成与存储、CI3 兼容、密钥轮换(4.7.0 新增)以及消息填充等实战要点,读完即可在项目中安全落地数据加密。
重要提醒:不要用本服务(或任何其他加密库)来存储密码!密码必须使用哈希而非加密,请通过 PHP 自带的 Password Hashing 扩展(
password_hash()/password_verify())完成。
一、核心概念:对称加密与 Handler 机制
Encryption Service 是一种双向对称(共享密钥)加密方案:加密与解密使用同一个密钥(key),由服务根据你的参数实例化并初始化一个加密handler(处理器)。handler 必须实现 CodeIgniter 定义的精简接口CodeIgniter\Encryption\EncrypterInterface(见 EncrypterInterface.php),该接口仅包含两个方法:
public function encrypt($data, $params = null); public function decrypt($data, $params = null);目前框架支持两种 PHP 加密扩展,对应两个内置 handler:
- OpenSSL(
OpenSSLHandler,源码) - Sodium(
SodiumHandler,源码)
使用相应扩展可能需要在服务器上额外安装软件,或在 PHP 中显式启用该扩展。框架在 Encryption 管理器构造函数 中会逐一检查扩展是否加载:Sodium 除了要求extension_loaded('sodium'),还要求 libsodium 版本不低于1.0.14(因为需要sodium_pad等 API)。若配置的 driver 不可用,会抛出EncryptionException::forNoHandlerAvailable()。
需要说明的边界:
- 这不是一套完整的加密解决方案。若需要公钥加密等更多能力,建议直接使用 OpenSSL 或其他密码学扩展,也可以考虑基于 libsodium 的面向对象封装包(如 Halite)。
- 从 PHP 7.2 起MCrypt 扩展已被废弃,因此本服务已放弃对它的支持。
二、快速上手:加载服务并加解密
与 CodeIgniter 4 的所有服务一样,加密服务通过Config\Services加载:
$encrypter = service('encrypter');对应的服务工厂实现位于 system/Config/Services.php:
encrypter(?EncryptionConfig $config = null, $getShared = false),默认以Config\Encryption配置实例化Encryption管理器并调用initialize()返回 handler。
假定你已经在配置中设置了起始密钥(见下文“配置”),加密与解密就非常简单——把字符串分别传给encrypt()和decrypt():
$plainText = 'This is a plain-text message!'; $ciphertext = $encrypter->encrypt($plainText); // Outputs: This is a plain-text message! echo $encrypter->decrypt($ciphertext);仅此而已!Encryption 库会自动完成整个过程中所有必要的安全操作(随机 IV、HMAC 认证、密钥派生等),你无需手工处理这些细节。
三、配置项详解
上面的示例使用的是 app/Config/Encryption.php 中的默认配置。各配置项含义如下:
| 选项 | 可选值(括号内为默认值) |
|---|---|
key | 加密起始密钥(starter key) |
driver | 首选 handler,如 OpenSSL 或 Sodium(OpenSSL) |
digest | 消息摘要算法(SHA512) |
blockSize | [仅 SodiumHandler]填充长度,单位字节(16) |
cipher | [仅 OpenSSLHandler]使用的加密算法(AES-256-CTR) |
encryptKeyInfo | [仅 OpenSSLHandler]加密密钥派生信息('') |
authKeyInfo | [仅 OpenSSLHandler]认证密钥派生信息('') |
rawData | [仅 OpenSSLHandler]密文是否保持原始二进制(true;设为false时输出 Base64) |
previousKeys | [4.7.0+]历史密钥列表,用于解密回退('') |
完整的默认配置文件可在 app/Config/Encryption.php 中查看,每个属性都带有详尽的注释说明,例如driver注释明确列出可用驱动为 OpenSSL 与 Sodium,cipher注释说明设为AES-128-CBC可解密 CI3 默认配置加密的数据。
3.1 传入自定义配置对象
你可以通过向Services调用传入自己的配置对象来覆盖配置文件中的设置。$config变量必须是Config\Encryption类的实例:
use Config\Encryption; $config = config(Encryption::class); $config->key = 'aBigsecret_ofAtleast32Characters'; $config->driver = 'OpenSSL'; $encrypter = service('encrypter', $config);从 BaseHandler 构造函数 可以看到,handler 会把配置对象的每个公开属性按同名属性复制到自身,因此key、digest、cipher、blockSize、rawData等配置会真正生效于加密过程。
3.2 保持与 CI3 兼容的配置(4.3.0+)
自 v4.3.0 起,你可以解密由 CodeIgniter 3 的 Encryption 库加密的数据。如果需要解密这类数据,请使用以下设置以保持兼容:
use Config\Encryption; $config = new Encryption(); $config->driver = 'OpenSSL'; // Your CI3's 'encryption_key' $config->key = hex2bin('64c70b0b8d45b80b9eba60b8b3c8a34d0193223d20fea46f8644b848bf7ce67f'); // Your CI3's 'cipher' and 'mode' $config->cipher = 'AES-128-CBC'; $config->rawData = false; $config->encryptKeyInfo = 'encryption'; $config->authKeyInfo = 'authentication'; $encrypter = service('encrypter', $config);要点:CI3 默认使用AES-128-CBC加密、输出 Base64 密文(rawData = false),且 HKDF 派生密钥时使用了'encryption'与'authentication'两个 key info 字符串。上述设置与 EncryptionTest 中验证 CI3 兼容解密的配置完全一致。
3.3 支持的 HMAC 认证算法
用于 HMAC 消息认证时,加密库支持 SHA-2 家族算法:
| 算法 | 原始长度(字节) | Hex 编码长度(字节) |
|---|---|---|
| SHA512 | 64 | 128 |
| SHA384 | 48 | 96 |
| SHA256 | 32 | 64 |
| SHA224 | 28 | 56 |
该表与 OpenSSLHandler 源码中的$digestSize映射完全对应。之所以不包含 MD5、SHA1 等流行算法,是因为它们已不再被认为足够安全,框架不希望鼓励使用它们。如果你确实需要这些算法,可以直接使用 PHP 原生的hash_hmac()函数。未来随着更安全算法的出现和普及,框架会补充支持。
四、默认行为
默认情况下,Encryption 库使用OpenSSL handler。该 handler 采用AES-256-CTR算法加密,使用你配置的密钥,并以SHA512 HMAC进行认证。
从 OpenSSLHandler::encrypt() 的实现可以还原完整默认流程:
- 用
hash_hkdf()从起始密钥派生出加密密钥(encryptKeyInfo参与派生); - 生成随机 IV,以
OPENSSL_RAW_DATA模式执行openssl_encrypt(); - 将
IV + 密文按rawData决定输出原始二进制或 Base64; - 再用
hash_hkdf()派生出认证密钥,对结果计算hash_hmac()并把 HMAC 值前置到结果之前。
解密时(OpenSSLHandler::decrypt())则按 digest 长度切出 HMAC,用hash_equals()常量时间比较校验,认证失败会抛出EncryptionException::forAuthenticationFailed(),随后取出 IV 与密文完成解密。
五、设置你的加密密钥
5.1 密钥长度与随机性要求
你的加密密钥必须足够长,达到所使用加密算法允许的长度:对于 AES-256,即 256 位,也就是32 字节(32 个字符)。
密钥应尽可能随机,绝不能是普通文本字符串,也不能是哈希函数的输出等可预测内容。要生成合格的密钥,可以使用加密库的createKey()方法:
// $key will be assigned a 32-byte (256-bit) random key $key = \CodeIgniter\Encryption\Encryption::createKey(); // for the SodiumHandler, you can use either: $key = sodium_crypto_secretbox_keygen(); $key = \CodeIgniter\Encryption\Encryption::createKey(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);其实现非常简洁(Encryption.php):
public static function createKey($length = 32) { return random_bytes($length); }random_bytes()从操作系统熵源(如/dev/urandom)获取密码学安全随机数据,EncryptionTest 验证了默认返回 32 字节、createKey(16)返回 16 字节。
5.2 把密钥写入配置文件
密钥可以存储在app/Config/Encryption.php中,也可以自行设计存储机制并在加解密时动态传入密钥。存入配置文件的方式:
namespace Config; use CodeIgniter\Config\BaseConfig; class Encryption extends BaseConfig { public $key = 'YOUR KEY'; // ... }5.3 编码密钥与加密结果
你会注意到createKey()输出的是二进制数据,直接复制粘贴很容易损坏,因此可以用bin2hex()或base64_encode()以更友好的方式处理密钥:
// Get a hex-encoded representation of the key: $encoded = bin2hex(\CodeIgniter\Encryption\Encryption::createKey(32)); // Put the same value with hex2bin(), // so that it is still passed as binary to the library: $key = hex2bin('your-hex-encoded-key');同样的技巧也适用于加密结果:
// Encrypt some text & make the results text $encoded = base64_encode($encrypter->encrypt($plaintext));5.4 使用前缀存储密钥:hex2bin:与base64:
存储密钥时可以利用两个特殊前缀:hex2bin:和base64:。当这两个前缀紧邻密钥值之前时,Encryption会智能解析密钥,最终仍以二进制字符串交给加密库:
namespace Config; use CodeIgniter\Config\BaseConfig; class Encryption extends BaseConfig { // In Encryption, you may use public $key = 'hex2bin:<your-hex-encoded-key>'; // or public $key = 'base64:<your-base64-encoded-key>'; // ... }同样,你可以在.env文件中使用这些前缀:
// For hex2bin encryption.key = hex2bin:<your-hex-encoded-key> // or encryption.key = base64:<your-base64-encoded-key>这样既保证了密钥在配置文件与.env中可读、可复制,又确保了传给加密算法的依然是原始二进制密钥。
六、加密密钥轮换(4.7.0+)
6.1 为什么需要轮换
出于安全最佳实践或合规要求,你可能需要定期更换加密密钥。框架自4.7.0起提供previousKeys配置项:在使用新密钥加密所有新数据的同时,仍能解密旧密钥加密过的存量数据。
6.2 工作原理
- 加密始终使用当前的
key值; - 解密先尝试当前
key; - 若解密失败,自动依次回退尝试
previousKeys中的每个历史密钥; - 从而实现无缝轮换、不丢失任何数据。
从源码层面看,当previousKeys非空时,Encryption::initialize() 会用KeyRotationDecorator包装底层 handler。装饰器实现(KeyRotationDecorator.php)正是上述逻辑:encrypt()直接透传给内层 handler(永远使用当前 key);decrypt()先尝试当前 key,捕获EncryptionException后,仅当没有显式传入 key 参数时才逐个尝试历史密钥。
重要:
previousKeys仅用于解密回退。所有新加密操作始终使用当前key。如果你通过encrypt()/decrypt()的$params参数显式传入密钥,则不会启用 previousKeys 回退——这一点在装饰器源码的 第 65-67 行 有明确判断。
6.3 配置方式
在app/Config/Encryption.php中添加旧密钥到$previousKeys属性:
namespace Config; use CodeIgniter\Config\BaseConfig; class Encryption extends BaseConfig { public string $key = 'hex2bin:your_new_encryption_key_in_hex'; public array|string $previousKeys = [ 'hex2bin:your_old_encryption_key_in_hex', 'hex2bin:another_old_key_if_needed', ]; // ... other config options }也可以(推荐)在.env文件中用逗号分隔的字符串配置:
encryption.key = hex2bin:your_new_key encryption.previousKeys = hex2bin:old_key_1,hex2bin:old_key_2框架会自动把逗号分隔的字符串解析为数组并逐个处理每个密钥(Encryption::initialize()中会先过滤空值,参见 Encryption.php)。
6.4 轮换工作流
- 轮换前:数据用
key加密和解密; - 开始轮换:把当前
key值移入previousKeys数组,为key设置新值; - 轮换期间:新数据用新
key加密,旧数据仍可通过previousKeys解密; - 重新加密数据(可选):用新密钥解密并重新加密存量数据;
- 完成轮换:待所有数据重新加密后,从
previousKeys中移除旧密钥。
对应的自动化测试在 tests/system/Encryption/KeyRotationDecoratorTest.php 中:testEncryptionUsesCurrentKey验证新数据始终由当前密钥加密、显式指定旧密钥解密会失败;testKeyRotationDecryptsOldData则完整模拟了“旧密钥加密 → 新密钥 + previousKeys 配置 → 成功解密”的轮换场景。
七、消息填充(Padding)
有时消息的长度会泄露大量关于其性质的信息。例如消息只可能是 "yes"、"no"、"maybe" 三者之一时,即使加密了,仅凭长度就能猜出内容。**填充(Padding)**正是缓解这一问题的技术:把消息长度补齐为某个块大小的倍数。
填充在SodiumHandler中通过 libsodium 原生的sodium_pad/sodium_unpad函数实现(参见 SodiumHandler.php 与 L115-L119)。加密前在明文消息上追加指定字节数的填充,解密后再移除。填充长度通过Config\Encryption的$blockSize属性配置,该值必须大于零,否则加密会抛出EncryptionException(见 SodiumHandler.php)。
重要:不建议自行设计填充实现,务必使用库提供的更安全的实现。另外密码不应填充——用填充来隐藏密码长度并不推荐。客户端向服务器发送密码时应该先哈希(即使只做一次哈希迭代),这样传输数据的长度恒定,服务器也无法轻易获得密码的明文副本。
八、加密 Handler 深入解析
8.1 OpenSSL Handler
OpenSSL 扩展长期是 PHP 的标准组成部分。CodeIgniter 的 OpenSSL handler 使用AES-256-CTR算法。
你提供的key会被用于派生另外两个密钥:一个用于加密,一个用于认证。这是通过HMAC 密钥派生函数(HKDF,HMAC-based Key Derivation Function)实现的。结合源码可以看到完整链条(OpenSSLHandler.php):
- 加密路径:
hash_hkdf($digest, $key, 0, $encryptKeyInfo)派生加密密钥 →openssl_cipher_iv_length()取得 IV 长度 →openssl_random_pseudo_bytes()生成随机 IV →openssl_encrypt($data, 'AES-256-CTR', $encryptKey, OPENSSL_RAW_DATA, $iv); - 认证路径:
hash_hkdf($digest, $key, 0, $authKeyInfo)派生认证密钥 →hash_hmac($digest, $result, $authKey, $rawData)计算并前置 HMAC。
encryptKeyInfo与authKeyInfo的存在,使加密密钥与认证密钥从同一个起始密钥中分离派生,避免了密钥复用带来的安全隐患。
8.2 Sodium Handler
Sodium 扩展自 PHP 7.2.0 起随 PHP 默认捆绑。Sodium 在端到端场景中发送秘密消息时,使用XSalsa20加密、Poly1305做 MAC、XS25519做密钥交换;而在共享密钥的对称加解密场景下,使用XSalsa20加密、HMAC-SHA512认证。
从 SodiumHandler::encrypt() 可以看到实现细节:
- 密钥长度必须恰好等于
SODIUM_CRYPTO_SECRETBOX_KEYBYTES(32 字节),否则抛出forNeedsStarterKey(); random_bytes()生成 nonce;sodium_pad()按blockSize填充明文;sodium_crypto_secretbox()执行 XSalsa20-Poly1305 认证加密,输出nonce + 密文;- 最后调用
sodium_memzero()对明文和密钥内存清零,降低密钥驻留内存的风险。
解密时(L84-L125)先校验密文最小长度,切出 nonce,用sodium_crypto_secretbox_open()打开,再sodium_unpad()去除填充;任何失败(包括sodium_unpad抛出SodiumException)都会以认证失败处理。
8.3 运行时参数覆盖
两个 handler 都支持在调用时通过$params覆盖密钥。$params为数组时取其key元素作为本次操作的起始密钥;为字符串时直接作为密钥;SodiumHandler 还支持在数组中传入blockSize覆盖本次操作的填充长度。详见 EncrypterInterface 的文档注释。
九、密文长度与存储考量
加密后的字符串通常比原始明文字符串更长(取决于具体算法)。长度增长主要受三个因素影响:
- 算法本身(cipher)的特性;
- 初始化向量(IV),它被前置到密文之前;
- HMAC 认证消息,同样被前置。
此外,加密结果还会经过Base64 编码(当rawData为 false 时),以保证无论字符集如何都能安全存储和传输。
选择数据存储机制时请务必考虑这一点:例如Cookie 只能容纳 4K 信息,加密后的内容很可能放不下。实际长度可通过在 OpenSSLHandler::encrypt() 中看到的结构估算:HMAC(digest长度) + IV(算法长度) + 密文,且结果可能再被 Base64 放大约 33%。
十、直接使用 Encryption 服务(不经过 Services)
除了(或额外)使用前面介绍的Services方式,你也可以直接创建 "Encrypter",或修改现有实例的设置:
// create an Encryption instance $encryption = new \CodeIgniter\Encryption\Encryption(); // reconfigure an instance with different settings $encrypter = $encryption->initialize($config);同样,$config必须是Config\Encryption类的实例。initialize()会根据配置重新设置 key、driver、digest,校验驱动可用性(Encryption.php),并返回相应的 handler 实例。
十一、类参考(Class Reference)
CodeIgniter\Encryption\Encryption
Encryption::createKey([int $length = 32]): string|false
- 参数
$length:输出长度; - 返回:指定长度的伪随机加密密钥,失败时返回
false; - 说明:通过从操作系统熵源(即
/dev/urandom)获取随机数据来创建密钥。
Encryption::initialize([?EncryptionConfig $config = null]): EncrypterInterface
- 参数
$config:配置参数; - 返回:
CodeIgniter\Encryption\EncrypterInterface实例; - 抛出:
CodeIgniter\Encryption\Exceptions\EncryptionException(当密钥为空、驱动未知或扩展不可用时); - 说明:用不同设置初始化(重新配置)库。
示例(来自官方文档 encryption/010.php):
$encrypter = $encryption->initialize(['cipher' => 'AES-256-CTR']);CodeIgniter\Encryption\EncrypterInterface
encrypt($data[, $params = null]): string
- 参数
$data:待加密数据;$params:配置参数(密钥),可为数组或字符串或null; - 返回:加密后的密文;
- 抛出:
EncryptionException; - 说明:加密输入数据并返回密文。若传入数组参数,则其中
key元素作为本次操作的起始密钥;也可直接以字符串形式传入起始密钥。若使用 SodiumHandler 并想在运行时指定不同blockSize,在$params数组中传入blockSize键即可。
完整调用形式(encryption/011.php):
$ciphertext = $encrypter->encrypt('My secret message'); $ciphertext = $encrypter->encrypt('My secret message', ['key' => 'New secret key']); $ciphertext = $encrypter->encrypt('My secret message', ['key' => 'New secret key', 'blockSize' => 32]); $ciphertext = $encrypter->encrypt('My secret message', 'New secret key'); $ciphertext = $encrypter->encrypt('My secret message', ['blockSize' => 32]);decrypt($data[, $params = null]): string
- 参数与返回类型同
encrypt(); - 说明:解密输入数据并返回明文。
$params中key元素(数组形式)或字符串密钥作为本次操作的起始密钥;SodiumHandler 同样支持运行时blockSize覆盖。
完整调用形式(encryption/012.php):
echo $encrypter->decrypt($ciphertext); echo $encrypter->decrypt($ciphertext, ['key' => 'New secret key']); echo $encrypter->decrypt($ciphertext, ['key' => 'New secret key', 'blockSize' => 32]); echo $encrypter->decrypt($ciphertext, 'New secret key'); echo $encrypter->decrypt($ciphertext, ['blockSize' => 32]);十二、实践要点小结
- 密码绝不加密,一律哈希:这是使用本服务的前提红线;
- 密钥长度必须达标:OpenSSL 的 AES-256 需要 32 字节随机密钥,Sodium 的 secretbox 要求恰好 32 字节;
- 密钥要随机且非文本:使用
Encryption::createKey()生成,配合hex2bin:/base64:前缀或bin2hex()友好存储,key可放配置文件或.env; - 默认配置已足够安全:AES-256-CTR + HKDF 派生 + SHA512 HMAC 认证,开箱即用;
- 升级 CI3 存量数据:用
AES-128-CBC、rawData = false、encryptKeyInfo = 'encryption'、authKeyInfo = 'authentication'的兼容配置解密; - 密钥轮换:4.7.0+ 使用
previousKeys实现无缝回退,注意它只服务解密、不参与加密,且显式传 key 时不会回退; - 估算密文长度:HMAC + IV + 密文,可能再经 Base64 放大,存储到 Cookie(4K 上限)等受限容器前务必留足空间。
如需深入源码,建议从 Encryption 管理器、OpenSSLHandler、SodiumHandler、KeyRotationDecorator 以及 EncryptionTest、KeyRotationDecoratorTest 入手,它们完整覆盖了本文涉及的所有行为。
- 后端
- Web框架
【免费下载链接】CodeIgniter4
Open Source PHP Framework (originally from EllisLab)
相关推荐
Apache Pulsar 端到端加密(End-to-End Encryption)完整实战指南:对称加密与非对称加密原理、CryptoKeyReader 接口与密钥轮换
Apache Pulsar 端到端加密(End to End Encryption)完整实战指南:对称加密与非对称加密原理、CryptoKeyReader 接口
消息队列后端流处理终极移动端加密完全指南:对称加密、非对称加密与密钥管理最佳实践
终极移动端加密完全指南:对称加密、非对称加密与密钥管理最佳实践 移动端应用开发中,数据安全是用户信任的基石。GitHub 加速计划 / an / android
文档教程移动开发Rook Ceph 密钥管理系统(KMS)接入与 OSD 加密密钥轮换实战指南
Rook Ceph 密钥管理系统(KMS)接入与 OSD 加密密钥轮换实战指南 本指南以 Rook 官方文档为骨架,系统讲解如何在 Rook 管理的 Ceph
云原生存储容器编排运维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考