Java验证Apple Identity Token E256签名时签名不匹配问题求助
Apple Sign In JWT签名验证失败排查(JWT signature does not match locally computed signature)
问题描述
刚接触Apple生态,按照Apple官方《Sign in with Apple REST API验证用户》指南,尝试在Java后端验证iOS端Sign In With Apple流程发送的Apple Identity Token(JWT)的JWS E256签名。参考Apple论坛思路写了代码,但使用io.jsonwebtoken:jjwt:0.9.1库时,报错:JWT signature does not match locally computed signature,需要排查问题。
测试代码
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import org.junit.jupiter.api.Test; import org.springframework.boot.web.client.RestTemplateBuilder; import java.math.BigInteger; import java.security.KeyFactory; import java.security.PublicKey; import java.security.spec.KeySpec; import java.security.spec.RSAPublicKeySpec; import java.util.Base64; import java.util.List; public class AppleTest { @Test public void test(String appleToken) throws Exception { AppleKeySet appleKeySet = new RestTemplateBuilder().build() .getForObject("https://appleid.apple.com/auth/keys", AppleKeySet.class); List<Key> applePublicKeys = appleKeySet.getKeys(); Key key = applePublicKeys.get(0); BigInteger n = new BigInteger(1, Base64.getUrlDecoder().decode(key.getN())); BigInteger e = new BigInteger(1, Base64.getUrlDecoder().decode(key.getE())); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); KeySpec publicKeySpec = new RSAPublicKeySpec(n, e); PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); Claims claims = Jwts.parser() .setSigningKey(publicKey) .parseClaimsJws(appleToken) .getBody(); } }
Key实体类
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Key { @JsonProperty("kty") private String kty; @JsonProperty("kid") private String kid; @JsonProperty("use") private String use; @JsonProperty("alg") private String alg; @JsonProperty("n") private String n; @JsonProperty("e") private String e; }
排查方向与解决方案
1. 未匹配对应kid的公钥
你当前直接取了Apple返回密钥列表的第一个,但Apple的公钥集合包含多个不同kid的密钥,JWT头部的kid必须和验证用的公钥kid完全匹配。解决步骤:
- 解析JWT的头部,提取其中的
kid字段 - 从Apple返回的
keys列表中筛选出kid匹配的密钥,再用该密钥验证签名
2. Base64解码方式错误
Apple返回的n和e是无填充的URL安全Base64编码,而Base64.getUrlDecoder()默认处理带填充的编码,直接解码会导致数据错误。需要先补全填充字符:
// 补全URL Base64的填充字符 private static String padBase64Url(String input) { int padding = (4 - input.length() % 4) % 4; if (padding > 0) { input += "=".repeat(padding); } return input; }
解码时先调用这个方法处理n和e:
String paddedN = padBase64Url(key.getN()); String paddedE = padBase64Url(key.getE()); BigInteger n = new BigInteger(1, Base64.getUrlDecoder().decode(paddedN)); BigInteger e = new BigInteger(1, Base64.getUrlDecoder().decode(paddedE));
3. JJWT版本兼容性问题
io.jsonwebtoken:jjwt:0.9.1是较老版本,对JWS算法的处理可能和Apple的签名标准存在差异。建议升级到JJWT最新稳定版本(如0.11.5+),新版本对URL Base64、RSA算法的支持更完善,API也更简洁。
修正后的示例代码
import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.Jwts; import org.junit.jupiter.api.Test; import org.springframework.boot.web.client.RestTemplateBuilder; import java.math.BigInteger; import java.security.KeyFactory; import java.security.PublicKey; import java.security.spec.RSAPublicKeySpec; import java.util.Base64; import java.util.List; import java.util.Optional; public class AppleTest { @Test public void test(String appleToken) throws Exception { // 1. 获取Apple公钥集合 AppleKeySet appleKeySet = new RestTemplateBuilder().build() .getForObject("https://appleid.apple.com/auth/keys", AppleKeySet.class); List<Key> applePublicKeys = appleKeySet.getKeys(); // 2. 解析JWT头部提取kid JwsHeader header = Jwts.parser().parseClaimsJws(appleToken).getHeader(); String tokenKid = header.getKeyId(); // 3. 匹配对应kid的公钥 Optional<Key> matchedKey = applePublicKeys.stream() .filter(key -> tokenKid.equals(key.getKid())) .findFirst(); if (matchedKey.isEmpty()) { throw new IllegalArgumentException("找不到匹配kid的Apple公钥"); } Key key = matchedKey.get(); // 4. 补全填充后解码n和e String paddedN = padBase64Url(key.getN()); String paddedE = padBase64Url(key.getE()); BigInteger n = new BigInteger(1, Base64.getUrlDecoder().decode(paddedN)); BigInteger e = new BigInteger(1, Base64.getUrlDecoder().decode(paddedE)); // 5. 生成公钥并验证签名 KeyFactory keyFactory = KeyFactory.getInstance("RSA"); RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); Claims claims = Jwts.parser() .setSigningKey(publicKey) .parseClaimsJws(appleToken) .getBody(); } // 补全URL Base64填充字符 private static String padBase64Url(String input) { int padding = (4 - input.length() % 4) % 4; if (padding > 0) { input += "=".repeat(padding); } return input; } }
内容的提问来源于stack exchange,提问作者CR Sardar
相关产品推荐
相关产品推荐

