PAYATHON 2026

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 中:

  1. 输入 bXktYXBpLWtleQ==。
  2. 勾选 “secret base64 encoded”。
  3. Jwt.io 将其解码为 my-api-key。
  4. 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 时,直接使用。

备注:内容仅供参考。