本文目录导读:

- 核心原则
- 场景一:密码存储(不可逆,绝对不要用加密)
- 场景二:数据对称加密(AES-256-GCM,可逆)
- 场景三:现代加密:使用 libsodium (Sodium)
- 场景四:非对称加密(公钥/私钥)
- 总结:我应该用哪个?
- 永远要避开的坑
在 PHP 中进行加解密的最佳实践,需要根据你的具体需求(是保护静态数据、传输数据还是做身份验证)来选择不同的方案,以下是经过安全验证的推荐做法:
核心原则
- 不要自己写加密算法:使用 PHP 内置的
openssl或sodium扩展。 - 永远不要使用
mcrypt:该扩展已废弃且不再安全。 - 区分加密(Encryption)与哈希(Hashing):
- 哈希:用于密码存储、数据完整性校验(不可逆),最佳实践:
password_hash()和password_verify()。 - 加密:用于保护敏感数据(可逆,需要密钥解密)。
- 哈希:用于密码存储、数据完整性校验(不可逆),最佳实践:
密码存储(不可逆,绝对不要用加密)
这是最常见的需求,永远不要存储明文密码,也不要用可逆加密来存储密码。
最佳实践:使用 password_hash() 和 password_verify()
<?php
// 注册时:生成密码哈希
$password = 'user_plain_password';
$hash = password_hash($password, PASSWORD_BCRYPT); // 或 PASSWORD_ARGON2ID
// 将 $hash 存入数据库
// 登录时:验证密码
$inputPassword = 'user_input_password';
$storedHash = '从数据库取出的哈希值';
if (password_verify($inputPassword, $storedHash)) {
echo "密码正确!";
} else {
echo "密码错误!";
}
// 需要重新哈希时(算法更新)
if (password_needs_rehash($storedHash, PASSWORD_ARGON2ID)) {
$newHash = password_hash($inputPassword, PASSWORD_ARGON2ID);
// 更新数据库中的哈希
}
?>
数据对称加密(AES-256-GCM,可逆)
适用于加密小段敏感数据(如身份证号、支付信息),推荐使用 AES-256-GCM(提供认证,防止篡改)。
最佳实践:使用 openssl_encrypt 和 openssl_decrypt
<?php
/**
* 安全加密函数
* @param string $plaintext 明文
* @param string $key 密钥(建议使用随机生成的二进制串,而不是直接写字符串)
* @return string 经过 base64 编码的密文(包含 IV 和 认证标签)
*/
function safeEncrypt(string $plaintext, string $key): string {
// 1. 生成随机初始化向量(IV),AES-256-GCM 的 IV 长度是 12 字节
$iv = openssl_random_pseudo_bytes(12);
// 2. 加密并生成认证标签
$tag = ''; // 用于存储认证标签
$ciphertext = openssl_encrypt(
$plaintext,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA, // 返回原始二进制数据
$iv,
$tag,
'', // 附加认证数据 (AAD),可选,可以加上用户ID等增加安全性
16 // 认证标签长度(16字节)
);
// 3. 组合 IV + 密文 + 认证标签,并 Base64 编码
return base64_encode($iv . $ciphertext . $tag);
}
/**
* 安全解密函数
* @param string $encodedData Base64 编码的密文
* @param string $key 密钥(必须与加密时相同)
* @return string|false 解密后的明文,失败返回 false
*/
function safeDecrypt(string $encodedData, string $key): string|false {
// 1. Base64 解码
$data = base64_decode($encodedData);
// 2. 提取 IV (前12字节),密文,认证标签(最后16字节)
$ivLength = 12;
$tagLength = 16;
// 检查数据长度是否合理
if (strlen($data) < $ivLength + $tagLength) {
return false;
}
$iv = substr($data, 0, $ivLength);
$tag = substr($data, -$tagLength);
$ciphertext = substr($data, $ivLength, -$tagLength);
// 3. 解密并验证认证标签
$plaintext = openssl_decrypt(
$ciphertext,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
return $plaintext;
}
// 使用示例
$key = base64_decode('你的32字节Base64编码密钥'); // 必须长度为32字节(256位)
$plaintext = '这是需要加密的敏感信息';
$encrypted = safeEncrypt($plaintext, $key);
echo "加密后: " . $encrypted . PHP_EOL;
$decrypted = safeDecrypt($encrypted, $key);
echo "解密后: " . $decrypted . PHP_EOL;
?>
关键点:
- 密钥管理:密钥是安全核心,不要写在代码里,使用环境变量、密钥管理服务(华为云KMS、AWS KMS)或安全配置文件(权限严格受限)。
- IV:每次加密都必须使用随机 IV(相同密钥 + 不同 IV = 不同密文)。
- 认证标签:GCM 模式能检测密文是否被篡改。
现代加密:使用 libsodium (Sodium)
PHP 7.2+ 内置了 Sodium 扩展,是当前最推荐、最安全的现代加密库(由Daniel J. Bernstein设计)。
简单加密/解密(对称加密 - XChaCha20-Poly1305)
<?php
// 加密
$plaintext = '敏感数据';
$key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen(); // 生成密钥
// 通常你需要一个 nonce(一次性随机数),建议使用 randombytes
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$ciphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
$plaintext,
$ad = '', // 附加数据(可选)
$nonce,
$key
);
// 存储时:可以将 nonce 和密文一起存储
$stored = base64_encode($nonce . $ciphertext);
// 解密
$decoded = base64_decode($stored);
$nonce = substr($decoded, 0, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$ciphertext = substr($decoded, SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$decrypted = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
$ciphertext,
$ad = '',
$nonce,
$key
);
?>
简单的密钥封装(Secret Key - 用于API令牌等)
Sodium 提供了一个非常方便的对称加密函数 crypto_secretbox,但推荐使用上面的 AEAD 模式,因为它更直观。
非对称加密(公钥/私钥)
适用于数据交换,A 用 B 的公钥加密,只有 B 能用私钥解密。
<?php // 1. 生成密钥对 $keyPair = sodium_crypto_box_keypair(); $secretKey = sodium_crypto_box_secretkey($keyPair); $publicKey = sodium_crypto_box_publickey($keyPair); // 2. 加密(需要接收者的公钥) $message = '秘密消息'; $anonymousMessage = sodium_crypto_box_seal($message, $publicKey); // 使用接收者公钥加密,接收者用私钥解密 // 3. 解密(使用自己的私钥) $decryptedMessage = sodium_crypto_box_seal_open($anonymousMessage, $keyPair); ?>
应用场景:
- 前端用公钥加密信用卡信息,后端用私钥解密。
- 通信双方交换会话密钥(混合加密)。
我应该用哪个?
| 需求场景 | 推荐方案 | 备注 |
|---|---|---|
| 存储密码 | password_hash() + password_verify() |
永远不要尝试解密密码 |
| 保护数据库敏感字段 | openssl_encrypt (AES-256-GCM) 或 Sodium crypto_aead |
妥善保管密钥(环境变量/KMS) |
| 安全传输数据、API通信 | Sodium crypto_box_seal 或 crypto_aead |
或使用 HTTPS + JWT |
| 前端→后端加密 | Web Crypto API (前端) + PHP Sodium (后端) | 使用非对称加密或混合加密 |
| 防止CSRF/伪造 | 使用随机令牌 + HMAC 或 JWT | 不直接属于加解密,是签名 |
永远要避开的坑
- EBC 模式:只加密,不认证,易被重放和篡改。
- 使用
sha1/md5做密码哈希:秒破。 - 把
key和iv写死在代码里。 - 对密文不做完整性校验(如不使用 GCM / CCM)。
- 使用
serialize()或json_encode()加密:结构信息泄露。 - 使用自定义 xxtea、异或 等“花式”算法。
如果你能提供更具体的场景(加密用户手机号”、“与第三方接口加密通信”),我可以给出更精确的代码示例。