JWT的Token生成
Java JWT 签名不一致问题:版本坑点与 PHP 跨语言兼容实战
JWT(JSON Web Token)广泛应用于用户身份验证,但在实际项目中跨语言解析 JWT 时,会踩到各种隐蔽的坑。Java 的 jwt 不同版本对密钥的处理差异,本文提供 Java/PHP 跨语言签名一致的完整解决方案。
🎯 问题背景
项目后端使用 Java 生成 JWT,前端或其他服务需要用 PHP 解码验证。但发现:
- Java 生成的 JWT,在 PHP 解析时签名验证失败。
- 即使 Header、Payload完全一致,PHP HMAC-SHA512 计算出来的签名却不同。
示例:
Java 生成的 JWT:
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiZW1hc3RlciIsImxvZ2luX3VzZXJfa2V5IjoiM2I0YzYtNzY1NCJ9.QlJIV_dOD2_BueD-f9Gs7oZ1SdIbRyqlK8tlyH6mjWxlj3audD_Nzrw8cRy1AdrNYIX1TZgfxtySLJfmyKfVkw
PHP 重新生成后:
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiZW1hc3RlciIsImxvZ2luX3VzZXJfa2V5IjoiM2I0YzYtNzY1NCJ9.NvllM3Yr1dh2LMU-FAPOnTzdFaLgohvLNf0-AIPWUUZhF_i9-hisd4E_vRskw3pPQyOCfVnDROKpNUTTuR8OxA
签名完全不同!
调试代码
打印php和java生成过程中的一些参数
java
private String createToken(Map<String, Object> claims)
    {
        System.out.println("Creating JWT token with claims: " + claims.toString());
        String headerJson = "{\"alg\":\"HS512\"}";
        String payloadJson = null; // 用Jackson或Gson序列化
        try {
            payloadJson = new ObjectMapper().writeValueAsString(claims);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        String header_b64 = TextCodec.BASE64URL.encode(headerJson);
        String payload_b64 = TextCodec.BASE64URL.encode(payloadJson);
        String signingInput = header_b64 + "." + payload_b64;
        System.out.println("Header Base64Url: " + header_b64);
        System.out.println("Payload Base64Url: " + payload_b64);
        System.out.println("Signing input: " + header_b64 + "." + payload_b64);
        System.out.println(Arrays.toString(secret.getBytes(StandardCharsets.UTF_8)));System.out.println("🔗 signing input: " + signingInput);
        System.out.println("🔐 key bytes: " + Arrays.toString(secret.getBytes(StandardCharsets.UTF_8)));
        Mac mac = null;
        try {
            mac = Mac.getInstance("HmacSHA512");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        try {
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA512"));
        } catch (InvalidKeyException e) {
            throw new RuntimeException(e);
        }
        byte[] sig = mac.doFinal(signingInput.getBytes(StandardCharsets.UTF_8));
        System.out.println("🧬 signature (raw hex): " + bytesToHex(sig));
        System.out.println("📤 signature (base64url): " + Base64.getUrlEncoder().withoutPadding().encodeToString(sig));
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        System.out.println("Generated JWT token: " + token);
        return token;
    }
php
function base64url_encode(string $data): string {
	return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
// ==== 接收参数 ====
$username = $argv[1] ?? 'master';
$tokenId  = $argv[2] ?? '351e67e7-692c-4aa2-8f4b-ea83011a7ed4';
$secret   = 'abc123';
//$secret = JwtForJava::fakeJavaKeyDecode($secret); // 模拟 Java 密钥处理,这是后来添加的
$header = ['alg' => 'HS512'];
$claims = [
	'sub' => $username,
	'login_user_key' => $tokenId
];
// ==== 编码阶段 ====
$header_json  = json_encode($header, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$payload_json = json_encode($claims, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$header_b64  = base64url_encode($header_json);
$payload_b64 = base64url_encode($payload_json);
$signing_input = $header_b64 . '.' . $payload_b64;
// ==== 签名阶段 ====
$signature_raw = hash_hmac('sha512', $signing_input, $secret, true);
$signature_b64 = base64url_encode($signature_raw);
$jwt = $header_b64 . '.' . $payload_b64 . '.' . $signature_b64;
// ==== 输出 ====
echo "🧾 原始 Header JSON:\n$header_json\r\n";
echo "📦 原始 Payload JSON:\n$payload_json\r\n";
echo "📤 Base64Url Header:\n$header_b64\r\n";
echo "📤 Base64Url Payload:\n$payload_b64\r\n";
echo "🔗 签名原始串 (signing input):\n$signing_input\r\n";
echo "🔒 HMAC-SHA512 签名 (raw bytes):\n" . bin2hex($signature_raw) . "\r\n";
echo "📤 Base64Url Signature:\n$signature_b64\r\n";
echo "✅ 最终 JWT:\n$jwt\r\n";
echo "✅ secret字节码:\n$jwt\r\n";
foreach (str_split($secret) as $c) {
	echo ord($c) . ' ';
}
echo "🔗 signing input:\n$signing_input\n";
echo "🔐 secret (UTF-8):\n" . implode(' ', array_map('ord', str_split($secret))) . "\n";
$signature_raw = hash_hmac('sha512', $signing_input, $secret, true);
echo "🧬 signature (raw hex):\n" . bin2hex($signature_raw) . "\n";
$signature_b64url = rtrim(strtr(base64_encode($signature_raw), '+/', '-_'), '=');
echo "📤 signature (base64url):\n$signature_b64url\n";
🔍 问题排查:Header、Payload、signing
逐步比对:
✅ Base64 编码:
Header: eyJhbGciOiJIUzUxMiJ9
Payload: eyJzdWIiOiJiZW1hc3RlciIsImxvZ2luX3VzZXJfa2V5IjoiM2I0YzYtNzY1NCJ9
一致。
✅ 原始签名串(signing input):
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiZW1hc3RlciIsImxvZ2luX3VzZXJfa2V5IjoiM2I0YzYtNzY1NCJ9
一致。
❌ HMAC-SHA512 签名(原始字节、base64url):
不一致!
于是怀疑:Java 签名时用的密钥和PHP使用的密钥不一致!但是密钥明文命名一样的。
🧨 最终排查结果:老版本 JJWT 会偷偷 Base64 解码密钥!
在 JJWT(io.jsonwebtoken:jjwt)0.x 版本中,当这么写:
.signWith(SignatureAlgorithm.HS512, secretString)
它内部实际上会对这个字符串进行 Base64 解码:
byte[] keyBytes = TextCodec.BASE64.decode(secretString);
也就是说:
- 你以为传入的是 "abc123"这个密钥;
- 实际上,它会当作 Base64 处理成:base64_decode("abc123")。
✅ 解决方案一:修复 Java 使用方式(推荐)
改为显式指定密钥字节,避免被 JJWT 隐式 decode:
import javax.crypto.spec.SecretKeySpec;
String secret = "abc123";
Key key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
String token = Jwts.builder()
    .setClaims(claims)
    .signWith(key, SignatureAlgorithm.HS512) // ✅ 正确签名方式
    .compact();
JJWT 从 0.11.x 起正式推荐这种写法。
✅ 解决方案二:不动 Java,PHP 模拟(兼容处理)
如果不能改 Java 代码(比如是第三方服务或已有代码),那只能让 PHP 来“配合”它的错误行为。
/**
 * 模拟 Java 对明文 secret 做 base64 decode 的逻辑
 */
function javaStyleSecret(string $secret): string {
    $padLen = 4 - (strlen($secret) % 4);
    if ($padLen < 4) {
        $secret .= str_repeat('=', $padLen);
    }
    return base64_decode($secret);
}
签名时这样写:
$signing_input = $header_base64 . '.' . $payload_base64;
$java_key = javaStyleSecret('abc123');
$signature = hash_hmac('sha512', $signing_input, $java_key, true);
🧪 验证代码片段(PHP):
$claims = [
    'sub' => 'master',
    'login_user_key' => '351e67e7-692c-4aa2-8f4b-ea83011a7ed4'
];
$secret = 'abc123';
// header + payload
$header_json = json_encode(['alg' => 'HS512'], JSON_UNESCAPED_SLASHES);
$payload_json = json_encode($claims, JSON_UNESCAPED_SLASHES);
$header_b64 = rtrim(strtr(base64_encode($header_json), '+/', '-_'), '=');
$payload_b64 = rtrim(strtr(base64_encode($payload_json), '+/', '-_'), '=');
// sign
$signing_input = $header_b64 . '.' . $payload_b64;
$key_bytes = javaStyleSecret($secret);
$signature_raw = hash_hmac('sha512', $signing_input, $key_bytes, true);
$signature_b64 = rtrim(strtr(base64_encode($signature_raw), '+/', '-_'), '=');
// JWT
$jwt = $signing_input . '.' . $signature_b64;
echo "JWT: $jwt
";
📌 小结
| 方式 | 原因 | 修复方法 | 
|---|---|---|
| Java signWith(SignatureAlgorithm, String) | 会 base64 解码 secret | ✅ 传入 SecretKeySpec | 
| PHP 验证不一致 | 用了明文 secret 直接签名 | ✅ PHP 也模拟 base64 解码 secret | 
🧠 经验总结
- 不同语言对 JWT 的处理细节可能存在「隐式行为」差异,尤其是密钥处理。
- 一旦涉及跨语言签名验证,务必检查header、payload、signing input、密钥是否一致处理。
- 推荐使用最新版 JWT 库,明确签名用法,避免神秘 bug 的来源。
封装好的工具类
<?php
namespace app\util;
/**
 * 用于兼容 Java 旧版 jjwt 生成的 HS512 JWT 的工具类
 */
class JwtForJava
{
    /**
     * Base64 URL 安全编码
     */
    private static function base64url_encode(string $data): string
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
    /**
     * Base64 URL 安全解码
     */
    private static function base64url_decode(string $data): string
    {
        $remainder = strlen($data) % 4;
        if ($remainder) {
            $data .= str_repeat('=', 4 - $remainder);
        }
        return base64_decode(strtr($data, '-_', '+/'));
    }
    /**
     * 模拟 Java 旧版本 jjwt 对 secret 的 base64 decode 行为
     */
    private static function javaStyleSecret(string $secret): string
    {
        $padLen = 4 - (strlen($secret) % 4);
        if ($padLen < 4) {
            $secret .= str_repeat('=', $padLen);
        }
        return base64_decode($secret) ?: $secret; // 如果不是合法 base64 就返回原始
    }
    /**
     * 生成 Java 旧版兼容 JWT(不含 typ,HS512)
     */
    public static function generate(array $claims, string $secret): string
    {
        $header_json = json_encode(['alg' => 'HS512'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $payload_json = json_encode($claims, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $header_b64 = self::base64url_encode($header_json);
        $payload_b64 = self::base64url_encode($payload_json);
        $signing_input = $header_b64 . '.' . $payload_b64;
        $java_key = self::javaStyleSecret($secret);
        $signature = hash_hmac('sha512', $signing_input, $java_key, true);
        $signature_b64 = self::base64url_encode($signature);
        return $signing_input . '.' . $signature_b64;
    }
    /**
     * 验证 Java JWT 并解析 claims(返回 null 表示验证失败)
     */
    public static function parse(string $jwt, string $secret): ?array
    {
        $parts = explode('.', $jwt);
        if (count($parts) !== 3) {
            return null;
        }
        [$header_b64, $payload_b64, $signature_b64] = $parts;
        $signing_input = $header_b64 . '.' . $payload_b64;
        $java_key = self::javaStyleSecret($secret);
        $expected_signature = self::base64url_encode(
            hash_hmac('sha512', $signing_input, $java_key, true)
        );
        if (!hash_equals($expected_signature, $signature_b64)) {
            return null;
        }
        $payload_json = self::base64url_decode($payload_b64);
        $claims = json_decode($payload_json, true);
        return is_array($claims) ? $claims : null;
    }
}
    如果这篇文章对你有用,可以关注本人微信公众号获取更多ヽ(^ω^)ノ  ~
 
 


 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号