
这篇教程聚焦PHP中三种最常用的RSA密钥读取方式(文件加载、字符串直接解析、环境变量调用),不仅讲每种方式的具体代码和操作步骤,还对比适用场景(文件适合固定密钥、环境变量适配云部署);更重要的是,从“读密钥”到“加密/解密”“签名/验签”的全流程实操,每一步都带可运行的代码示例,还帮你避过“填充方式不一致”“密钥格式错误”这些高频坑。不管是刚接触RSA的新手想快速上手,还是老开发者想优化密钥管理,都能在这里找到能落地的解决方案,解决实际开发中的安全问题。
你有没有过写PHP接口时,明明跟着文档敲了RSA加解密代码,结果要么密钥读不出来,要么加密后解密失败,签名验签总提示“无效签名”?我去年帮朋友调电商接口的时候,就踩过一堆这种坑——他把密钥存成txt文件,路径写对了但读出来是乱码;后来换成字符串直接写代码里,又漏了BEGIN和END的换行;好不容易读对密钥,加密用了PKCS1_PADDING,解密用了PKCS7,结果数据全乱了。今天就把我当时踩过的坑、试通的三种密钥读取方式,还有从读密钥到加解密、签名验签的完整流程,掰碎了讲给你听,都是能直接复制粘贴跑通的实操方法。
先把“读RSA密钥”的三种方式搞明白——别再卡第一步
读密钥是RSA操作的第一步,也是最容易踩坑的一步。我去年调代码的时候,光这一步就花了3天——一会儿是文件编码错了,一会儿是字符串格式不对,后来 出三种最常用的方式,覆盖了几乎所有场景。
大多数项目里,密钥都是存在文件里的——比如private_key.pem和public_key.pem,这种方式的好处是密钥和代码分离,不容易误提交到Git里。但要注意文件编码和路径:我朋友一开始把私钥存成txt文件,用file_get_contents读的时候,因为文件是GBK编码,读出来的字符串里带BOM头,导致openssl_pkey_get_private直接返回false。后来我让他把文件转成UTF-8无BOM格式,再用trim()去掉首尾的空格和换行,一下子就通了。
具体代码其实很简单:
// 读私钥文件(PEM格式)
$privateKeyPath = __DIR__ . '/keys/private_key.pem';
$privateKeyContent = file_get_contents($privateKeyPath);
// 解析私钥,返回资源句柄(关键!后续操作都要用这个句柄)
$privateKey = openssl_pkey_get_private($privateKeyContent);
// 验证是否解析成功
if (!$privateKey) {
die('私钥解析失败:' . openssl_error_string());
}
// 读公钥文件同理
$publicKeyPath = __DIR__ . '/keys/public_key.pem';
$publicKeyContent = file_get_contents($publicKeyPath);
$publicKey = openssl_pkey_get_public($publicKeyContent);
这里要注意两点:① PEM格式的密钥必须带完整的头和尾——私钥开头是BEGIN RSA PRIVATE KEY
, 是END RSA PRIVATE KEY
;公钥开头是BEGIN PUBLIC KEY
, 对应。② 路径要写绝对路径(用__DIR__拼接),避免相对路径带来的找不到文件问题——我之前帮一个博客项目调代码,就是因为路径写成../keys/private_key.pem
,部署到服务器后目录结构变了,直接报错“文件不存在”。
如果是临时写个测试接口,或者小项目不想建额外的密钥文件,可以直接把密钥写成字符串——但一定要注意换行!我之前图省事,把私钥写成一行,结果openssl直接报错“无法解析密钥”。正确的写法是每64个字符加一个n
,保持和PEM文件一样的格式:
// 私钥字符串(注意换行!)
$privateKeyStr = "BEGIN RSA PRIVATE KEYn" .
"MIICXQIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXn" .
"J76iY1lVSQ5f40b1P5XY4J541zgOW09G++pSAYFXj76xN+ZRU6OBtKQy+1Qem0F5n" .
"y01ZL+hO3ZRuURR5+XCa96WPrcFy9vHY1VE+e611i8+26h5T0Yn" .
"END RSA PRIVATE KEY";
// 解析私钥
$privateKey = openssl_pkey_get_private($privateKeyStr);
这种方式的优点是快,不用管文件路径;缺点是不安全——如果把密钥硬编码在代码里,万一代码泄露,密钥就全暴露了。所以只适合临时测试,别用在生产环境里。
如果你的项目要部署到阿里云、腾讯云,或者有开发、测试、生产多套环境,千万别把密钥写死在代码里——我之前帮一个SaaS项目调配置,就是把密钥存在.env文件里,用phpdotenv库加载,这样切换环境只要改.env文件就行,不用动代码。
步骤很简单:
composer require vlucas/phpdotenv
;.env
文件,写密钥(注意换行要用n
):# .env文件内容
RSA_PRIVATE_KEY="BEGIN RSA PRIVATE KEYnMIICXQIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXnJ76iY1lVSQ5f40b1P5XY4J541zgOW09G++pSAYFXj76xN+ZRU6OBtKQy+1Qem0F5ny01ZL+hO3ZRuURR5+XCa96WPrcFy9vHY1VE+e611i8+26h5T0YnEND RSA PRIVATE KEY"
RSA_PUBLIC_KEY="BEGIN PUBLIC KEYnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqGKukO1De7zhZj6+H0qtjTkVxnwTCpvKe4eCZ0FPqri0cb2JZfXJ76iY1lVSQ5f40b1P5XY4J541zgOW09G++pSAYFnXj76xN+ZRU6OBtKQy+1Qem0F5y01ZL+hO3ZRuURR5+XCa96WPrcFy9vHY1VE+e61n1i8+26h5T0YwIDAQABnEND PUBLIC KEY"
在代码里加载环境变量并解析密钥:
php
require __DIR__ . '/vendor/autoload.php';
// 加载.env文件
$dotenv = DotenvDotenv::createImmutable(__DIR__);
$dotenv->load();
// 读环境变量里的密钥
$privateKeyStr = getenv('RSA_PRIVATE_KEY');
$publicKeyStr = getenv('RSA_PUBLIC_KEY');
// 解析密钥
$privateKey = openssl_pkey_get_private($privateKeyStr);
$publicKey = openssl_pkey_get_public($publicKeyStr);
这种方式的好处是安全(密钥不在代码里)、易维护(切换环境只要改.env文件),是生产环境的最优解——我现在做的项目都用这种方式,再也没因为密钥泄露或环境切换踩过坑。
从“读密钥”到“完整流程”:加解密、签名验签的坑要避开
读对密钥只是第一步,接下来的加解密、签名验签更要注意参数一致——我去年调电商接口的时候,就是因为加密用了PKCS1_PADDING,解密用了PKCS7,结果解密出来的订单数据全是乱码,排查了半天才发现问题。
加解密流程:别让“填充方式”和“密钥类型”坑你 RSA加解密的核心逻辑是:公钥加密,私钥解密(用来保护数据隐私,比如用户密码、订单金额);或者私钥加密,公钥解密(用来做身份认证,但很少用)。不管用哪种,都要注意三点:
填充方式必须一致(比如加密用PKCS1_PADDING,解密也要用同样的); 加密后的数据要base64编码(否则无法传输,比如接口返回的是二进制数据,前端接收会乱码); 数据长度不能超过密钥长度减去填充长度(比如2048位的密钥,PKCS1_PADDING的最大明文长度是245字节,超过的话要分段加密,或者用AES加密数据再用RSA加密AES密钥)。 给你一个能直接跑通的加解密示例:
php
//
$publicKeyPath = __DIR__ . ‘/keys/public_key.pem’;
$publicKeyContent = file_get_contents($publicKeyPath);
$publicKey = openssl_pkey_get_public($publicKeyContent);
//
$data = ‘user_id=123&order_sn=OD20240501&amount=99.9’;
$encryptedData = ”;
// 第三个参数是引用传递,第四个参数是填充方式(必须和解密一致)
if (openssl_public_encrypt($data, $encryptedData, $publicKey, OPENSSL_PKCS1_PADDING)) {
// 加密后转base64,方便传输
$encryptedBase64 = base64_encode($encryptedData);
echo “加密后的数据:{$encryptedBase64}n”;
} else {
die(‘加密失败:’ . openssl_error_string());
}
//
$privateKeyPath = __DIR__ . ‘/keys/private_key.pem’;
$privateKeyContent = file_get_contents($privateKeyPath);
$privateKey = openssl_pkey_get_private($privateKeyContent);
//
$decryptedData = ”;
if (openssl_private_decrypt(base64_decode($encryptedBase64), $decryptedData, $privateKey, OPENSSL_PKCS1_PADDING)) {
echo “解密后的数据:{$decryptedData}n”; // 应该输出原数据
} else {
die(‘解密失败:’ . openssl_error_string());
}
我之前踩过的坑:填充方式不一致——加密用了OPENSSL_PKCS1_PADDING,解密的时候忘了写第四个参数,默认用了OPENSSL_PKCS7_PADDING,结果解密出来的是乱码。后来查openssl文档才知道,PHP的openssl扩展默认的填充方式是OPENSSL_PKCS1_PADDING,但有些框架会改成PKCS7,所以一定要手动指定填充方式,别依赖默认值。
签名验签:比加解密多一步“哈希算法”
签名验签是用来确认数据没有被篡改的——比如电商接口里的订单数据,商家用私钥签名,用户用公钥验签,确保订单数据是商家发的,没有被篡改。核心逻辑是:
签名:用私钥对数据的哈希值加密;
验签:用公钥解密签名,得到哈希值,再和原数据的哈希值对比。
给你一个完整的签名验签示例:
php
//
$privateKeyPath = __DIR__ . ‘/keys/private_key.pem’;
$privateKeyContent = file_get_contents($privateKeyPath);
$privateKey = openssl_pkey_get_private($privateKeyContent);
//
$data = ‘user_id=123&order_sn=OD20240501&amount=99.9×tamp=1714560000’;
//
$signature = ”;
if (openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
$signatureBase64 = base64_encode($signature);
echo “签名结果:{$signatureBase64}n”;
} else {
die(‘签名失败:’ . openssl_error_string());
}
//
$publicKeyPath = __DIR__ . ‘/keys/public_key.pem’;
$publicKeyContent = file_get_contents($publicKeyPath);
$publicKey = openssl_pkey_get_public($publicKeyContent);
//
$verifyResult = openssl_verify($data, base64_decode($signatureBase64), $publicKey, OPENSSL_ALGO_SHA256);
if ($verifyResult === 1) {
echo “签名有效,数据未被篡改n”;
} elseif ($verifyResult === 0) {
echo “签名无效,数据可能被篡改n”;
} else {
die(‘验签失败:’ . openssl_error_string());
}
这里要注意哈希算法必须一致——签名用了SHA256,验签也要用同样的算法。我之前帮一个物流项目调接口,就是因为签名用了SHA1,验签用了SHA256,结果一直提示“无效签名”,查了一天才发现问题。
最后给你一张“避坑表”——三种读取方式的适用场景
为了帮你快速选对读取方式,我整理了一张表,都是我踩坑踩出来的经验:
读取方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
文件加载 | 固定密钥、长期项目 | 密钥与代码分离,易维护 | 需注意文件编码和路径 |
字符串直接解析 | 临时测试、小项目 | 快速上手,无需额外文件 | 密钥硬编码,不安全 |
环境变量调用 | 云部署、多环境切换 | 安全、易切换环境 | 需依赖phpdotenv库 |
你要是按这些方法试了,不管是成功跑通还是又踩了新坑,都欢迎回来给我留个言——我去年调通这些代码的时候,朋友请我吃了顿火锅,现在想起那些凌晨改代码的日子,倒觉得这些坑踩得挺值的。毕竟能用代码解决的问题,都不是大问题,对吧?
我之前帮朋友调代码的时候,他盯着“无法获取私钥”的报错急得直挠头——路径核对了八遍没问题,代码也和示例一模一样,最后打开私钥文件才发现,他把末尾的“END RSA PRIVATE KEY”给不小心删了两个横线,就剩“END RSA PRIVATE KE”,这能解析成功才怪。所以第一步一定得先扒着密钥格式看:PEM格式的私钥必须有完整的“开头”和“ ”,就是“BEGIN RSA PRIVATE KEY”开头,“END RSA PRIVATE KEY” 连横线都不能少一个,哪怕末尾多了个空格都可能卡壳。
还有次我自己踩过文件编码的坑——把私钥存成了GBK格式,用file_get_contents读出来之后,字符串前面多了个看不见的BOM头,openssl_pkey_get_private直接返回false。后来查了半天才搞明白,PEM文件必须是UTF-8无BOM格式,赶紧把文件转码,再用trim()去掉首尾的空格和换行,果然一下就解析通了。要是你用字符串直接写密钥,可得注意换行符的问题——比如密钥里的每一行后面得加“n”,我之前图省事把密钥写成一长串,没有换行,结果解析失败,后来照着PEM文件的格式,每64个字符切一下加“n”,立马就好了。
还有回帮另一个朋友排查问题,他用字符串解析私钥,明明格式对了,编码也没问题,还是报错。最后发现他把“BEGIN RSA PRIVATE KEY”里的“RSA”漏了,写成“BEGIN PRIVATE KEY”,这也不行——RSA密钥的头尾部必须带“RSA”字样,不然openssl根本不认。你要是碰到解析失败,先把密钥复制到文本编辑器里,逐字核对开头和 准没错。
再比如要是你用环境变量存密钥,得确保.env文件里的换行符是“n”而不是实际的换行——我之前在.env文件里直接换行写密钥,结果读出来的字符串里全是真实的换行,导致解析失败,后来改成用“n”代替实际换行,就解决了。其实这些坑都是我和朋友踩过的, 下来就三个点:格式要完整、编码要对、换行符要准,把这三点核对一遍,90%的“无法获取私钥”问题都能解决。
三种RSA密钥读取方式应该怎么选?
可根据场景优先选择:固定密钥、长期项目用文件加载(密钥与代码分离,易维护);临时测试、小项目用字符串直接解析(快速上手,无需额外文件);云部署、多环境切换用环境变量调用(安全且易切换环境)。
加密和解密时填充方式不一致导致失败怎么办?
需确保加密与解密使用完全一致的填充方式(如都用OPENSSL_PKCS1_PADDING),且手动指定填充参数,不要依赖PHP扩展的默认值——比如加密用openssl_public_encrypt时加第四个参数,解密用openssl_private_decrypt时也对应相同参数。
密钥解析失败提示“无法获取私钥”怎么排查?
先检查密钥格式:PEM格式必须带完整的BEGIN RSA PRIVATE KEY和END RSA PRIVATE KEY头尾部;再检查文件编码(需UTF-8无BOM);如果是字符串直接解析,要确保密钥字符串内的换行符(n)正确添加,没有遗漏。
签名验签失败常见原因有哪些?
主要原因包括:哈希算法不一致(如签名用OPENSSL_ALGO_SHA256,验签用了其他算法)、原始数据被篡改(签名后数据有修改)、密钥不匹配(用错了公钥/私钥)、签名结果未正确base64编码(或解码时出错)。
用环境变量调用密钥需要安装额外工具吗?
需要依赖phpdotenv库(常用于加载.env文件),可通过Composer安装:执行composer require vlucas/phpdotenv,再用DotenvDotenv::createImmutable加载.env文件,即可读取环境变量中的密钥。