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

JJWT中用RSA公钥字节解析JWS令牌的问题与疑问

JJWT多签名算法JWS验证问题解答

背景

我的服务需要验证来自多个来源的JWS,每个来源共享Base64编码的密钥,且不限制签名算法。使用JJWT库时发现,byte[] signingKey仅支持HMAC签名,用于RSA/EC签名时会抛出如下错误:

java.lang.IllegalArgumentException: Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.
        at io.jsonwebtoken.lang.Assert.isTrue(Assert.java:35)
        at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:363)
        at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:529)
        at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:589)
        at io.jsonwebtoken.impl.ImmutableJwtParser.parseClaimsJws(ImmutableJwtParser.java:173)

通过相关issue了解到,可以实现io.jsonwebtoken.SigningKeyResolverAdapter,解析JWS头信息后提供对应的java.security.Key实例,根据签名算法匹配对应密钥完成验证。以下是从Base64字节数组获取RSA公钥的代码示例:

final String publicKeyBase64 = "...";
final byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
final PublicKey publicKey = (PublicKey) keyFactory.generatePublic(x509EncodedKeySpec);

技术问询

  1. 该自定义解析器方案并非JJWT默认方式,是否存在弊端或问题?
  2. 为何byte[]密钥仅支持HMAC算法?

问题解答

1. 自定义SigningKeyResolver方案的潜在问题

这个方案其实是JJWT官方设计的标准扩展方式,专门用于处理多密钥、多算法这类动态场景,并非非标准用法。但实际使用中需要注意以下几点:

  • 算法扩展维护成本:每次新增支持的签名算法(比如EC),都需要手动在解析逻辑中添加对应的分支处理,容易因遗漏导致验证失败。
  • 性能开销:当前代码每次验证token都会重新解析Base64生成PublicKey对象,高频验证场景下会有不必要的性能损耗,建议用「算法标识+密钥Base64」作为键,缓存已解析好的Key实例。
  • 多来源适配:如果服务需要验证多个来源的JWS,当前代码仅固定使用一个publicKeyBase64,实际场景需要结合token中的iss(签发者)或其他自定义标识,从配置/密钥管理服务中匹配对应的Base64密钥。
  • 异常处理完善:密钥解析失败时的异常要做好封装,避免泄露敏感信息,同时给出明确的错误提示,方便排查问题。

2. 为何byte[]密钥仅支持HMAC算法

核心原因在于对称与非对称密钥的本质差异:

  • HMAC属于对称算法:它的密钥就是一段无特定结构的字节序列,直接通过SecretKeySpec就能将字节数组转换为可用的密钥,无需额外解析。
  • RSA/EC属于非对称算法:这类密钥有严格的编码规范(比如RSA公钥遵循X.509格式,私钥遵循PKCS#8格式),字节数组中包含密钥的算法参数(如RSA的模数、指数,EC的曲线信息),必须通过KeyFactory结合对应的KeySpec(如X509EncodedKeySpec)解析成PublicKey/PrivateKey实例,才能被Java加密体系(JCA)的签名验证API识别。
  • JJWT的设计考量:如果允许用byte[]传递非对称密钥,用户很可能直接传入未正确解析的字节数组,导致验证失败或安全隐患。因此JJWT通过断言强制区分两种密钥的传入方式,引导用户使用正确的密钥类型。

测试代码

package com.stackoverflow.jjwt;

import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

import javax.crypto.spec.SecretKeySpec;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.security.Keys;

public class Test {
    public static void main(final String[] args) {
        final KeyPair pair = Keys.keyPairFor(SignatureAlgorithm.RS512);
        final PrivateKey privateKey = pair.getPrivate();
        final PublicKey publicKey = pair.getPublic();

        // 这是来源方共享给我的Base64格式公钥
        final String publicKeyBase64 = Base64.getEncoder()
                .encodeToString(publicKey.getEncoded());

        // 这是我的服务需要验证的token
        final String token = Jwts.builder()
                .setSubject("Hurray!")
                .signWith(privateKey)
                .compact();

        decode(token, publicKeyBase64);
        // ↳ 抛出异常:Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.

        decodeWithCustomResolver(token, publicKeyBase64);
        // ↳ 输出:Subject: Hurray!
    }

    public static void decode(final String token, final String publicKeyBase64) {
        try {
            final byte[] publicKeyBytes = Base64.getDecoder()
                    .decode(publicKeyBase64);
            final String subject = Jwts.parserBuilder()
                    .setSigningKey(publicKeyBytes)
                    .build()
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
            System.out.println("Subject: " + subject);
        } catch (final Exception e) {
            System.err.println("Exception: " + e.getMessage());
            e.printStackTrace();
            // java.lang.IllegalArgumentException: Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.
            //     at io.jsonwebtoken.lang.Assert.isTrue(Assert.java:35)
            //     at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:363)
            //     at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:529)
            //     at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:589)
            //     at io.jsonwebtoken.impl.ImmutableJwtParser.parseClaimsJws(ImmutableJwtParser.java:173)
            //     at com.stackoverflow.jjwt.Test.decode(Test.java:52)
            //     at com.stackoverflow.jjwt.Test.main(Test.java:38)
        }
    }

    public static void decodeWithCustomResolver(final String token, final String publicKeyBase64) {
        final SigningKeyResolverAdapter resolver = new SigningKeyResolverAdapter() {
            @Override
            public Key resolveSigningKey(final JwsHeader header, final Claims claims) {
                final SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
                if (alg.isHmac()) {
                    // 非对称算法(RSA、椭圆曲线)不能用这种方式
                    final byte[] keyBytes = this.resolveSigningKeyBytes(header, claims);
                    return new SecretKeySpec(keyBytes, alg.getJcaName());
                } else if (alg.isRsa()) {
                    try {
                        final byte[] publicKeyBytes = Base64.getDecoder()
                                .decode(publicKeyBase64);
                        final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
                        final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
                        return (PublicKey) keyFactory.generatePublic(x509EncodedKeySpec);
                    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                        throw new IllegalArgumentException("检测到RSA算法,但密钥无效", e);
                    }
                }
                throw new IllegalArgumentException("暂不支持椭圆曲线算法"); // 可扩展其他算法
            }
        };
        try {
            final String subject = Jwts.parserBuilder()
                    .setSigningKeyResolver(resolver)
                    .build()
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
            System.out.println("Subject: " + subject);
            // ↳ 输出:Subject: Hurray!
        } catch (final Exception e) {
            System.err.println("Exception: " + e.getMessage());
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 01:15:11