PHP/Laravel 使用 Firebase/JWT 时 Base64 密钥签名无效
结论
Jwt.io 中的 “secret base64 encoded” 表示:输入框里的 Secret 是 Base64 文本,验证签名前,Jwt.io 会先将它解码为原始密钥字节。
Firebase\JWT\JWT::encode() 不会自动解码 Base64 密钥,而是直接使用传入的字节生成 HS256 签名。因此,签名端和验证端必须使用完全相同的原始密钥:
HMAC-SHA256(signingInput, rawSecretBytes)
如果配置中保存的是 Base64 编码后的密钥,需要先用 base64_decode() 解码,再传给 JWT::encode()。
当前代码为什么验证失败
当前代码使用的是普通字符串密钥:
$apiKey = 'my-api-key';
$jwtToken = JWT::encode($payload, $apiKey, 'HS256');
实际参与签名的密钥字节是:
my-api-key
在 Jwt.io 中直接输入 my-api-key,不勾选 “secret base64 encoded”,验证端使用的也是这些字节,所以签名有效。
勾选该选项后,Jwt.io 会将输入内容作为 Base64 数据解码。验证端使用的密钥字节不再是字符串 my-api-key,计算出的 HMAC 签名也会不同。
方案一:将现有密钥转换为 Base64 后填入 Jwt.io
如果实际签名密钥仍然是 my-api-key,PHP 代码不需要修改:
use Firebase\JWT\JWT;
$rawSecret = 'my-api-key';
$jwtToken = JWT::encode($payload, $rawSecret, 'HS256');
my-api-key 对应的标准 Base64 文本是:
bXktYXBpLWtleQ==
在 Jwt.io 中:
- 输入
bXktYXBpLWtleQ==。 - 勾选 “secret base64 encoded”。
- Jwt.io 将其解码为
my-api-key。 - Jwt.io 使用解码后的字节验证 HS256 签名。
只有两端最终使用相同的密钥字节,签名验证才能通过。
方案二:配置中保存 Base64 密钥
另一种常见情况是,第三方提供的 API Secret 本身就是 Base64 文本。例如:
$base64Secret = 'bXktYXBpLWtleQ==';
生成 JWT 前,需要先解码这个值:
use Firebase\JWT\JWT;
$base64Secret = 'bXktYXBpLWtleQ==';
$rawSecret = base64_decode($base64Secret, true);
if ($rawSecret === false) {
throw new InvalidArgumentException('JWT secret is not valid Base64.');
}
$jwtToken = JWT::encode($payload, $rawSecret, 'HS256');
在 Jwt.io 中输入原始配置值:
bXktYXBpLWtleQ==
然后勾选 “secret base64 encoded”。
在 Laravel 中,可以把 Base64 文本放入环境变量:
JWT_SECRET_BASE64=bXktYXBpLWtleQ==
再在配置文件中读取:
// config/services.php
return [
'payment' => [
'jwt_secret_base64' => env('JWT_SECRET_BASE64'),
],
];
Controller 或服务类在签名前负责解码:
use Firebase\JWT\JWT;
$encodedSecret = config('services.payment.jwt_secret_base64');
$secret = base64_decode($encodedSecret, true);
if ($secret === false) {
throw new RuntimeException('Invalid Base64 JWT secret configuration.');
}
$jwtToken = JWT::encode($payload, $secret, 'HS256');
验证 token 时,也要使用同一组原始字节:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$encodedSecret = config('services.payment.jwt_secret_base64');
$secret = base64_decode($encodedSecret, true);
if ($secret === false) {
throw new RuntimeException('Invalid Base64 JWT secret configuration.');
}
$decodedPayload = JWT::decode(
$jwtToken,
new Key($secret, 'HS256')
);
不要对 Base64 密钥重复编码
下面这种处理通常是错误的:
$encodedSecret = 'bXktYXBpLWtleQ==';
$jwtToken = JWT::encode(
$payload,
base64_encode($encodedSecret),
'HS256'
);
这里对已经编码过的文本又进行了一次 Base64 编码,最终参与签名的密钥会与接收方使用的密钥不同。
处理前先确认拿到的是哪种值:
- 普通 Secret 可以直接传给
JWT::encode()。 - Base64 Secret 需要先执行
base64_decode($secret, true)。 - JWT 本身使用 Base64URL 编码,并不意味着签名密钥也需要手动进行 Base64URL 编码。
Base64 与 Base64URL 不要混淆
JWT 的 Header、Payload 和 Signature 使用 Base64URL 编码,这部分由 Firebase/JWT 自动处理。
Secret 是否以标准 Base64 格式保存是另一回事。标准 Base64 中常见的字符包括:
+ / =
Base64URL 通常使用:
- _
末尾的 = 也可能被省略。如果第三方明确提供的是 Base64URL 格式密钥,不能直接认定 base64_decode() 可以处理。需要先转换字符、补齐 padding,再进行解码:
function base64UrlDecode(string $value): string
{
$value = strtr($value, '-_', '+/');
$padding = strlen($value) % 4;
if ($padding !== 0) {
$value .= str_repeat('=', 4 - $padding);
}
$decoded = base64_decode($value, true);
if ($decoded === false) {
throw new InvalidArgumentException('Invalid Base64URL value.');
}
return $decoded;
}
只有对方明确说明 Secret 使用 Base64URL 格式时,才需要这样处理。
注意事项
- “secret base64 encoded” 不会改变 HS256 算法,它只决定验证工具如何解释输入的 Secret。
- Base64 是编码方式,不是加密方式,不能提高密钥的安全性。
- HS256 的签名方和验证方共享同一个密钥。任何持有该密钥的人都能生成有效 token。
- 不要将生产环境密钥提交到代码仓库,也不要把它写入日志。
- 不建议把真实的生产密钥粘贴到在线调试网站。可以先用本地测试密钥排查编码问题,再到受控环境中验证生产 token。
- 密钥的处理方式应以支付平台或 API 提供方的格式说明为准。对方提供 Base64 Secret 时,解码后再签名;提供普通文本 Secret 时,直接使用。
备注:内容仅供参考。
