如何验证 Apple 登录的 Jwt 令牌(后端验证)。如何从 Java 中的模数和指数 (n,e) 生成 RSA 公钥

Posted

技术标签:

【中文标题】如何验证 Apple 登录的 Jwt 令牌(后端验证)。如何从 Java 中的模数和指数 (n,e) 生成 RSA 公钥【英文标题】:How To validate Jwt Token for Apple Login (Backend validation). How to generate RSA Public key from modulus and exponent (n,e) in Java 【发布时间】:2020-05-24 02:24:32 【问题描述】:

我正在寻找一种验证苹果登录令牌的方法。 验证必须在后端完成,所以我确信我可以安全地添加一个新帐户。 另一个问题是我需要将 xml 格式的密钥 https://appleid.apple.com/auth/keys 转换为公钥 pem 格式。 我找到了一个可能的解决方案,我将在下面发布。 代码用Java实现

public static void main(String...args) throws Exception 
        String jwtAppleToken = ""; //copy here the token from apple

        //copied from https://appleid.apple.com/auth/keys
        final String base64UrlEncodedModulus = "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w"; 
        final String base64UrlEncodedExp = "AQAB";

        String publicKey = getPemPublicKeyFromBase64UrlEncodedXMLRSAKey(base64UrlEncodedModulus, base64UrlEncodedExp);

        System.out.println(verify(jwtAppleToken, publicKey));

        System.out.println("-----BEGIN PUBLIC KEY-----");
        System.out.println(publicKey);
        System.out.println("-----END PUBLIC KEY-----");

    

【问题讨论】:

【参考方案1】:

与 Jose4 lib 相同的解决方案,

此 HttpsJwksVerificationKeyResolver 将根据列表中的密钥 ID 选择公钥。所以我们不必处理它。

import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;


 HttpsJwks httpsJkws = new HttpsJwks("https://appleid.apple.com/auth/keys");

 HttpsJwksVerificationKeyResolver httpsJwksKeyResolver = new HttpsJwksVerificationKeyResolver(httpsJkws);

 JwtConsumer jwtConsumer = new JwtConsumerBuilder()
          .setVerificationKeyResolver(httpsJwksKeyResolver)
          .setExpectedIssuer("https://appleid.apple.com")
          .setExpectedAudience(<clientId>)
          .build();

 JwtClaims jwtClaims = jwtConsumer.processToClaims(<idToken>);

processToClaims 将抛出适当的异常,只需捕获并采取相应的行动。

希望这可以保持简单,并使其他开发人员更具可读性。

【讨论】:

这绝对是最好的答案。我能够扔掉大量不工作的代码,并用这个简单、优雅的解决方案替换它。非常感谢!【参考方案2】:

这是验证苹果登录令牌的一种可能解决方案。

该实现使用发布于 --> https://appleid.apple.com/auth/keys

的 Apple 公钥

密钥从 XML 格式 (https://appleid.apple.com/auth/keys) 转换为 PEM 格式,然后验证令牌。

部分代码可用于将字符串格式的模数和指数转换为PEM格式的RSA公钥

import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;

public class VerifyAppleToken 

    public static void main(String...args) throws Exception 
        String jwtAppleToken = ""; //copy here the token from apple
        System.out.println("THE TOKEN IS VERIFIED FOR ONE OF APPLE KEYS:"+verify(jwtAppleToken));

        //copied from https://appleid.apple.com/auth/keys
        final String base64UrlEncodedModulus = "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w"; 
        final String base64UrlEncodedExp = "AQAB";

        String publicKey = getPemPublicKeyFromBase64UrlEncodedXMLRSAKey(base64UrlEncodedModulus, base64UrlEncodedExp);

        System.out.println(verify(jwtAppleToken, publicKey));

       //copied from and converted to base64 from base64UrlEncoded https://appleid.apple.com/auth/keys on 
       // 07/02/2020
        final String base64EncodedModulus = "lxrwmuYSAsTfn+lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu/auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY+RNwCWdjNfEaY/esUPY3OVMrNDI15Ns13xspWS3q+13kdGv9jHI28P87RvMpjz/JCpQ5IM44oSyRnYtVJO+320SB8E2Bw92pmrenbp67KRUzTEVfGU4+obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd/JhmqX5CAaT9Pgi0J8lU/pcl215oANqjy7Ob+VMhug9eGyxAWVfu/1u6QJKePlE+w=="; 

        final String base64EncodedExp = "AQAB";

        System.out.println("-----BEGIN PUBLIC KEY-----");
        System.out.println(getPemPublicKeyFromBase64XMLRSAKey(base64EncodedModulus, base64EncodedExp));
        System.out.println("-----END PUBLIC KEY-----");

    

private static boolean verify(String jwtAppleToken) throws NoSuchAlgorithmException, InvalidKeySpecException 
        AppleKeysRetrieverService retriver = new AppleKeysRetrieverService();       
        AppleKeysResponse res = retriver.sendRetriveRequest("https://appleid.apple.com/auth/keys");

        List<AppleKeyDTO> appleKeys = res.getKeys();

        for (AppleKeyDTO appleKeyDTO : appleKeys) 
            final String base64UrlEncodedModulus  = appleKeyDTO.getN();
            final String base64UrlEncodedExp = appleKeyDTO.getE();
            String publicKey1 = getPemPublicKeyFromBase64UrlEncodedXMLRSAKey(base64UrlEncodedModulus, base64UrlEncodedExp);

            if(verify(jwtAppleToken, publicKey1)) 
                return true;
            
        

        return false;
    

     public static boolean verify(String jwtToken, String publicKey) 
            try 
                JwtHelper.decodeAndVerify(jwtToken, new RsaVerifier(getRSAPublicKey(publicKey)));
             catch (Exception e) 
                return false;
            
            return true;
        


     private static RSAPublicKey getRSAPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException 
            KeyFactory keyFactory = java.security.KeyFactory.getInstance("RSA");
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(java.util.Base64.getDecoder().decode(publicKey));
            return (RSAPublicKey) keyFactory.generatePublic(keySpec);
     


    private static String getPemPublicKeyFromBase64UrlEncodedXMLRSAKey(String urlBase64Modulus, String urlBase64Exp) throws NoSuchAlgorithmException, InvalidKeySpecException 
        byte[] e = Base64.getUrlDecoder().decode(urlBase64Exp);
        byte[] n = Base64.getUrlDecoder().decode(urlBase64Modulus);

        BigInteger exponent = new BigInteger(1, e);
        BigInteger modulus = new BigInteger(1, n);

        return getPemPublicKey(modulus, exponent);
    

    private static String getPemPublicKeyFromBase64XMLRSAKey(String base64Modulus, String base64Exp) throws NoSuchAlgorithmException, InvalidKeySpecException 
        byte[] e = Base64.getDecoder().decode(base64Exp);
        byte[] n = Base64.getDecoder().decode(base64Modulus);

        BigInteger exponent = new BigInteger(1, e);
        BigInteger modulus = (new BigInteger(1, n));

        return getPemPublicKey(modulus, exponent);
    

    private static String getPemPublicKey(BigInteger modulus, BigInteger exponent) throws NoSuchAlgorithmException, InvalidKeySpecException 
        RSAPublicKeySpec publicKeySpec = new java.security.spec.RSAPublicKeySpec(modulus, exponent);

        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey myPublicKey = keyFactory.generatePublic(publicKeySpec);

        byte[] park = Base64.getEncoder().encode(myPublicKey.getEncoded());


        return new String(park);
    



检索苹果键:


public class AppleKeysRetrieverService 

    public AppleKeysResponse sendRetriveRequest(String retriveAppleKeysUrl) 
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters()
            .add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));

        String appleKeysResponse = restTemplate
                .getForObject(retriveAppleKeysUrl, String.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(
                DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        AppleKeysResponse res = null;

        try 
            res = objectMapper.readValue(appleKeysResponse, AppleKeysResponse.class);

            return res;
        catch(Exception e) 

            return null;
        
    




public class AppleKeyDTO 

    public String kty;
    public String kid;
    public String sig;
    public String alg;
    public String n;
    public String e;

    public String getKty() 
        return kty;
    
    public void setKty(String kty) 
        this.kty = kty;
    

    public String getKid() 
        return kid;
    
    public void setKid(String kid) 
        this.kid = kid;
    

    public String getSig() 
        return sig;
    
    public void setSig(String sig) 
        this.sig = sig;
    

    public String getAlg() 
        return alg;
    
    public void setAlg(String alg) 
        this.alg = alg;
    

    public String getN() 
        return n;
    
    public void setN(String n) 
        this.n = n;
    

    public String getE() 
        return e;
    
    public void setE(String e) 
        this.e = e;
    






public class AppleKeysResponse 

    private List<AppleKeyDTO> keys;

    public List<AppleKeyDTO> getKeys() 
        return keys;
    

    public void setKeys(List<AppleKeyDTO> keys) 
        this.keys = keys;
    












【讨论】:

以上是关于如何验证 Apple 登录的 Jwt 令牌(后端验证)。如何从 Java 中的模数和指数 (n,e) 生成 RSA 公钥的主要内容,如果未能解决你的问题,请参考以下文章

invalid_client 用于使用苹果登录

OpenID Connect JWT 令牌验证和后端 api 的使用策略 - jwks 还是会话?

当用户单击链接时,如何通过请求将身份验证令牌从本地存储发送到后端

如何从角度 6 中的“响应标头”获取 JWT 令牌

Laravel JWT 身份验证

AWS Cognito 如何验证我自己的 IdP 颁发的身份令牌?