You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android平台ES256签名JWT后在线验证失败问题求助

Android平台生成ES256签名JWT无法通过在线工具验证的问题

需求说明

  • 密钥必须为P-256(secp256r1)椭圆曲线密钥,采用ECDSA-SHA256算法(对应JWT alg为ES256)
  • JWT Header必须符合:
{
  "alg": "ES256",
  "typ": "JWT"
}
  • Payload必须包含id(UUID格式)和iat(UTC秒级时间戳),示例:
{
  "id": "7986f03e-a4b8-4033-87b9-af8ae0468a0b",
  "iat": 1702390385
}

实现代码

1. 生成P-256密钥对(存储到Android KeyStore)

// Initialize KeyPairGenerator for ECDSA 256
val keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEY_STORE)

keyPairGenerator.initialize(
    KeyGenParameterSpec.Builder(
        keyStoreAlias,
        KeyProperties.PURPOSE_SIGN,
    )
        .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
        .setDigests(KeyProperties.DIGEST_SHA256)
        .build()
)

// Generate the key pair (it will be stored automatically in the Android key store)
keyPairGenerator.generateKeyPair()

2. 编码Header和Payload

val encodedHeader = makeJsonAndEncodeToString(
    "alg" to "ES256",
    "typ" to "JWT",
)
val encodedPayload = makeJsonAndEncodeToString(
    "id" to "27d09c71-bd67-4523-97bb-12e933e9acbb",
    "iat" to 1702462854,
)

private fun makeJsonAndEncodeToString(vararg data: Pair<String, Any>): String {
    return Base64.encodeToString(
        JSONObject(data.toMap()).toString().toByteArray(),
        Base64.NO_PADDING or Base64.URL_SAFE or Base64.NO_WRAP,
    )
}

3. 生成签名并拼接JWT

val signatureInput = "$encodedHeader.$encodedPayload"

val keyStore = loadKeyStore() // 自定义工具方法加载Android KeyStore

// 获取私钥
val entry: KeyStore.Entry = keyStore.getEntry(keyStoreAlias, null)
if (entry !is KeyStore.PrivateKeyEntry) {
    throw IllegalStateException("指定别名<$keyStoreAlias>下不存在私钥")
}

// 初始化签名并计算
val ecdsaSign = Signature.getInstance("SHA256withECDSA")
ecdsaSign.initSign(entry.privateKey)
ecdsaSign.update(signatureInput.toByteArray()) // 默认UTF-8编码

val signedContent = ecdsaSign.sign()
val encodedSignature = Base64.encodeToString(
    signedContent, 
    Base64.NO_PADDING or Base64.URL_SAFE or Base64.NO_WRAP,
)

// 拼接完整JWT
val token: String = "$encodedHeader.$encodedPayload.$encodedSignature"

4. 导出公钥为PEM格式

val keyStore = loadKeyStore()
val publicKey = keyStore.getCertificate(keyStoreAlias)?.publicKey ?: return null
val encoded = Base64.encodeToString(publicKey.encoded, Base64.DEFAULT)

return "-----BEGIN PUBLIC KEY-----\n$encoded-----END PUBLIC KEY-----"

问题现象

运行代码后生成的JWT:

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjI3ZDA5YzcxLWJkNjctNDUyMy05N2JiLTEyZTkzM2U5YWNiYiIsImlhdCI6MTcwMjQ2Mjg1NH0.MEQCIGvk2N-3BDf13FAhAywXe7okYH4DygaViBWk6z6wnrvdAiBgJzHegGu8e9YSC9QiKqHvJxjyCRAX53tmmC_LaaFdRQ

使用jwt.io或token.dev等在线工具解码正常,但签名验证失败,已确认提供的公钥PEM格式正确。

已做的排查

  • 用同一Signature实例和公钥本地验证签名,结果为true
  • 确认Base64编码参数:Token的Header/Payload/Signature使用NO_PADDING | URL_SAFE | NO_WRAP,公钥PEM使用DEFAULT
  • 尝试改用KeyStore.getKey()获取私钥,问题依旧
  • 验证公钥PEM格式正确,在线工具解析显示为ECDSA prime256v1、256位密钥

求问问题出在哪里?


内容的提问来源于stack exchange,提问作者Mackovich

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 10:40:29