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);
技术问询
- 该自定义解析器方案并非JJWT默认方式,是否存在弊端或问题?
- 为何
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
相关产品推荐
相关产品推荐

