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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 21:03:31