☰
CodeIgniter 4 加密服务(Encryption Service)完整指南:对称加密、密钥管理与轮换实战
2026/10/11 10:46:32 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】CodeIgniter4

Open Source PHP Framework (originally from EllisLab)

项目地址:https://gitcode.com/gh_mirrors/co/CodeIgniter4
点击查看免费下载

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 编码长度(字节)
SHA51264128
SHA3844896
SHA2563264
SHA2242856

该表与 OpenSSLHandler 源码中的$digestSize映射完全对应。之所以不包含 MD5、SHA1 等流行算法,是因为它们已不再被认为足够安全,框架不希望鼓励使用它们。如果你确实需要这些算法,可以直接使用 PHP 原生的hash_hmac()函数。未来随着更安全算法的出现和普及,框架会补充支持。

四、默认行为

默认情况下,Encryption 库使用OpenSSL handler。该 handler 采用AES-256-CTR算法加密,使用你配置的密钥,并以SHA512 HMAC进行认证。

从 OpenSSLHandler::encrypt() 的实现可以还原完整默认流程:

  1. 用hash_hkdf()从起始密钥派生出加密密钥(encryptKeyInfo参与派生);
  2. 生成随机 IV,以OPENSSL_RAW_DATA模式执行openssl_encrypt();
  3. 将IV + 密文按rawData决定输出原始二进制或 Base64;
  4. 再用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 轮换工作流

  1. 轮换前:数据用key加密和解密;
  2. 开始轮换:把当前key值移入previousKeys数组,为key设置新值;
  3. 轮换期间:新数据用新key加密,旧数据仍可通过previousKeys解密;
  4. 重新加密数据(可选):用新密钥解密并重新加密存量数据;
  5. 完成轮换:待所有数据重新加密后,从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() 可以看到实现细节:

  1. 密钥长度必须恰好等于SODIUM_CRYPTO_SECRETBOX_KEYBYTES(32 字节),否则抛出forNeedsStarterKey();
  2. random_bytes()生成 nonce;
  3. sodium_pad()按blockSize填充明文;
  4. sodium_crypto_secretbox()执行 XSalsa20-Poly1305 认证加密,输出nonce + 密文;
  5. 最后调用sodium_memzero()对明文和密钥内存清零,降低密钥驻留内存的风险。

解密时(L84-L125)先校验密文最小长度,切出 nonce,用sodium_crypto_secretbox_open()打开,再sodium_unpad()去除填充;任何失败(包括sodium_unpad抛出SodiumException)都会以认证失败处理。

8.3 运行时参数覆盖

两个 handler 都支持在调用时通过$params覆盖密钥。$params为数组时取其key元素作为本次操作的起始密钥;为字符串时直接作为密钥;SodiumHandler 还支持在数组中传入blockSize覆盖本次操作的填充长度。详见 EncrypterInterface 的文档注释。

九、密文长度与存储考量

加密后的字符串通常比原始明文字符串更长(取决于具体算法)。长度增长主要受三个因素影响:

  1. 算法本身(cipher)的特性;
  2. 初始化向量(IV),它被前置到密文之前;
  3. 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)

项目地址:https://gitcode.com/gh_mirrors/co/CodeIgniter4
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询