苹果登录服务端验证

java版本:

POM文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>AppleLogin</artifactId>
    <version>1.0-SNAPSHOT</version>

    <repositories>
        <repository>
            <id>nexus-aliyun</id>
            <name>Nexus aliyun</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public</url>
        </repository>
    </repositories>

    <dependencies>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

        <dependency>
            <groupId>org.jodd</groupId>
            <artifactId>jodd-http</artifactId>
            <version>4.3.2</version>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

    </dependencies>

</project>

苹果公钥地址返回结果映射实体类:

import lombok.Data;

import java.util.List;

@Data
public class AppleAuthKeys {

    private List<Item> keys;

    @Data
    public static class Item{

        private String kty;

        private String kid;

        private String use;

        private String alg;

        private String n;

        private String e;

    }

}

校验工具类:

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import jodd.http.HttpRequest;
import lombok.extern.slf4j.Slf4j;
import sun.security.rsa.RSAPublicKeyImpl;

import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.util.List;

@Slf4j
public class JWTUtil {

    private static final String ISSUER = "https://appleid.apple.com";

    private static final String AUDIENCE = "这里填写,app bundle id";

    private static final String APPLE_AUTH_KEYS_URL = "https://appleid.apple.com/auth/keys";

    public static Boolean verifyAppleLoginToken(String token,String subject) throws IOException {
        //先从token中解析出HEADER部分的kid,然后从苹果提供的公钥获取url中获取生成rsa公钥所需的n和e
        DecodedJWT decodedJWT = JWT.decode(token);
        String kid = decodedJWT.getHeaderClaim("kid").asString();
        String response = HttpRequest.get(APPLE_AUTH_KEYS_URL).send().body();
        List<AppleAuthKeys.Item> keyList = JSON.parseObject(response, AppleAuthKeys.class).getKeys();
        String n = null, e = null;
        for (AppleAuthKeys.Item item : keyList) {
            if (item.getKid().equals(kid)) {
                n = item.getN();
                e = item.getE();
            }
        }
        if (null == n || null == e) {
            log.error("根据token解析出来的kid获取苹果公钥参数n和e失败,token:{}", token);
            return false;
        }
        //各个版本的base64解码实现不太一样,目前发现只有apache的base64可以解析成功
        BigInteger bigIntModulus = new BigInteger(1, org.apache.commons.codec.binary.Base64.decodeBase64(n));
        BigInteger bigIntPrivateExponent = new BigInteger(1, org.apache.commons.codec.binary.Base64.decodeBase64(e));
        try {
            //使用生成的RSA公钥验证token的SIGNATURE部分是否合法,(同时验证ISSUER,SUBJECT,AUDIENCE是否合法)
            RSAPublicKeyImpl rsaPublicKey = new RSAPublicKeyImpl(bigIntModulus, bigIntPrivateExponent);
            Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, null);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(ISSUER)
                    .withSubject(subject)
                    .withAudience(AUDIENCE)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
        } catch (JWTVerificationException ex1) {
            log.error("苹果token检验失败,失败原因:{},token:{}", ex1.getMessage(), token);
            return false;
        } catch (InvalidKeyException ex2) {
            log.error("苹果token检验失败,公钥生成失败,失败原因:{},token:{}", ex2.getMessage(), token);
            return false;
        }
        log.info("苹果登录token校验成功,n:{},e:{},token:{}", n, e, token);
        return true;
    }

    public static void main(String[] args) throws IOException {
        verifyAppleLoginToken("待验证的苹果登录token","待验证的用户唯一ID");
    }

}

nodejs版本

const NodeRSA = require("node-rsa");
const axios = require("axios");
const jwt = require("jsonwebtoken");

async function getApplePublicKey(kid) {
  let res = await axios.request({
    method: "GET",
    url: "https://appleid.apple.com/auth/keys",
    headers: {
      "Content-Type": "application/json"
    }
  });
  let key = res.data.keys.filter(item => item.kid == kid)[0];
  console.log(key);
  const pubKey = new NodeRSA();
  pubKey.importKey(
    { n: Buffer.from(key.n, "base64"), e: Buffer.from(key.e, "base64") },
    "components-public"
  );
  return pubKey.exportKey(["public"]);
}

const audience = "这里填写 app bundle id";

async function verifyIdToken(token,subject) {
  const kid = jwt.decode(token, { complete: true }).header.kid;
  const applePublicKey = await getApplePublicKey(kid);
  console.log(applePublicKey);
  const jwtClaims = jwt.verify(token, applePublicKey, {
    algorithms: "RS256",
    issuer: "https://appleid.apple.com",
    audience: audience,
    subject: subject
  });
  jwt.verify(token, applePublicKey, { algorithms: "RS256" }, (err, decode) => {
    if (err) {
      console.log("token验证失败:", err.message);
    } else if (decode) {
      console.log("token验证成功:", decode);
    }
  });
}

verifyIdToken("待验证的token","用户唯一ID");