(vue + SpingBoot)前后端分离实现Apple登录的过程

前言

        首先介绍一下为什么写这篇文章。最近,公司有一个项目,是海外的手机游戏想要上到云平台上供各种客户端(web,Android,ios等)可以无需下载游戏即可游玩。其中我负责web端的项目,项目需要接入第三方登录,Google、Apple、FaceBook三种方式登录。在我做项目的准备工作期间,我发现网上的web端接入Apple登录的文档特别少,几乎没有......        所以我打算通过官方文档和一些“简单”的方法实现后发表这篇文章,以便于后续接触到这种场景的人可以通过这篇文章解决你的问题!! 最后说一句:由于项目有点紧急,所以代码的一些不规范和注释少还请谅解,当然我会尽可能说明代码用处啥的~。

官方文档和一些对我有用的帖子

Apple REST API 登录文档

JSON Web 令牌库 - jwt.io (用于生成client_secret)

java 操作 Cookie 跨域(同顶级域名)_huaism的博客-CSDN博客

help in java_java 后端 sign in with apple 随笔_汤一白君的博客-CSDN博客

Sign in with Apple NODE,web端接入苹果第三方登录 – 前端开发,JQUERY特效,全栈开发,vue开发

苹果授权登录(Sign in with Apple)-JAVA后端开发_KaisonChen的博客-CSDN博客

以上这些文档和工具,就是我在开发过程中,所找到的一些较为有用的工具。可以提取不少信息。可能还有一些我用到的帖子,不过我给忘了,就没贴出来了。

步骤

一、首先是需要申请Apple开发者账号,以及配置一些参数。

这个步骤我就不再赘述了,帖子栏最后一条链接里有步骤。

二、获取一些必要的参数。

当申请、配置好开发者账号后,我们需要获取一些必要参数。

(vue + SpingBoot)前后端分离实现Apple登录的过程_第1张图片

这些是我后端所用到的所有参数。

1. KID

官方文档的解释:

 用于生成JWT(client_secret)的一个参数,大小为10个字符串。

例如:

KID = "KOI98S78J6";

2. TEAM_ID (iss)

官方文档的解释:

 用于生成JWT(client_secret)的一个参数,大小为10个字符串。

例如:

TEAM_ID = "JI87S9KI7D";

3. CLIENT_ID

即apple的应用id,一般是域名的反写。

例如:

CLIENT_ID = "com.example.signservice";

4. PRIVATE_KEY

代码中拼写错误,是Private_key(私钥),是以.p8结尾的文本文件,用作生成JWT,是作为请求Token时的参数之一;

改后缀为.txt

图片打码部分是你的KID,请核对好;

然后以txt文件类型打开这个文件,如下图:

 (vue + SpingBoot)前后端分离实现Apple登录的过程_第2张图片

全部复制或者只复制中间的字符串都可,中间的字符串就是你的私钥了。

最后一个url,是生成JWT的链接:https://appleid.apple.com

三、前端跳转到apple登录授权页面。

这部分是由前端去完成的,拼接好链接调用apple的登录授权页面。这里我直接引用一下大佬帖子的内容。

client_id:获取的client_id,必传
redirect_uri: 设置的重定向url,当用户同意授权后,会发起一个该URL的post请求,开发者需要在后台设置相应接口去接收他,服务端通过apple传来的code参数去请求身份令牌,必传。
scope:权限范围,name或者email,或者两个都设。
state:表示客户端的当前状态,可以指定任意值,会原封不动地返回这个值,你可以通过它做些验证,防止一些攻击。


这里面只有client_id,redirect_uri,是必须的,其他如果不设会自动设置默认值。

你可以使用官方提供的按钮,当然也可以不用,当你点击登录按钮后会实际会跳转到一下地址,你可以选择直接手动拼接跳转授权页地址。


https://appleid.apple.com/auth/authorize?client_id=[CLIENT_ID]&redirect_uri=[REDIRECT_URI]&response_type=[RESPONSE_TYPE]&scope=[SCOPES]&response_mode=[RESPONSE_MODE]&state=[STATE] 

四、接收授权码code。 

按照上面的方法拼接好链接,请求到链接那边,用户登录apple账号后,apple服务器将发起一个POST请求至当时设置的redirect_uri,同时附上一个授权码code,id_token用于刷新token,首次登录将只有code和state。

(vue + SpingBoot)前后端分离实现Apple登录的过程_第3张图片

然后此时需要服务端提供一个接口来接受这些参数。这时我们就需要考虑一个问题了。 

        前端是否需要传递参数到后端。(注意)

        例如,我的项目中需要将游戏的主键ID传到后端来进行操作,此时如果我们将接收apple服务器传递参数的接口和后续验证code和token的接口做为同一个接口,即拿到code就直接进行验证的话,那前端那边是跳转在apple的登录授权页,是无法向后端传递游戏ID的。

        所以我们做的这个接口只是一个中转的接口,接收到apple传递的参数后需返回给前端,让前端拿到这些参数随带着前端那边需传递的参数一起来请求我们的验证接口。

        当然,如果前端不需要传参数的话,就可以拿到code直接去验证了。

 然后我们就来写一个中转的接口:(注意是POST请求方式)

@PostMapping("/token")
    public void getToken(AppleIdentity appleIdentity, HttpServletResponse resp) throws IOException {
        Cookie tokenCookie = new Cookie("ID_TOKEN", appleIdentity.getId_token());
        tokenCookie.setMaxAge(100); // Cookie的存活时间(自定义)
        tokenCookie.setDomain("example.com");
        tokenCookie.setPath("/"); // 默认路径
        Cookie codeCookie = new Cookie("CODE", appleIdentity.getCode());
        codeCookie.setMaxAge(100); // Cookie的存活时间(自定义)
        codeCookie.setDomain("example.com");
        codeCookie.setPath("/"); // 默认路径
        resp.addCookie(tokenCookie);
        resp.addCookie(codeCookie);
        resp.sendRedirect("https://xxxxx.example.com/game/play");
    }

这里我是做的重定向去跳转回前端的页面,这里传参的话有两种方式:

        1. 第一种(不推荐):直接在前端链接上拼接参数

https://xxxxx.example.com/game/play?code=xxxxxx&id_token=xxxxxxx

        2. 第二种:存入cookie。

 java 操作 Cookie 跨域(同顶级域名)_huaism的博客-CSDN博客

这个帖子介绍了跨域操作cookie,我这里不多解释了。

最后,提醒一下之前配置apple时,重定向的url配置需要配置到这个中转的接口

(vue + SpingBoot)前后端分离实现Apple登录的过程_第4张图片

 这里是引用了别人的图片,配置位置给大家指出来了,填上你的中转接口即可。

四、使用code等参数请求id_token

将code返回给前端后,前端将code和其他需要传的参数来请求我们的验证接口,验证接口的第一步就是获取到token:

(vue + SpingBoot)前后端分离实现Apple登录的过程_第5张图片

以下是返回参数的字段:

{
    "access_token": "adg61.######.670r9",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "rca7.######.IABoQ",
    "id_token": "eyJra.######..96sZg"
}

填充参数,发送post请求,得到JSON串,解析JSON,得到id_token. 

        Map paramsForm = new HashMap();
        paramsForm.put("grant_type", "authorization_code");
        paramsForm.put("code", appleIdentity.getCode());
        paramsForm.put("client_id", CLIENT_ID);
        paramsForm.put("client_secret", generateClientSecret(KID, TEAM_ID, CLIENT_ID, PRIMARY_KEY));

        Map headers = new HashMap<>();
        headers.put(Header.CONTENT_TYPE, "application/x-www-form-urlencoded");

        HttpKits.buildGetUrl("https://appleid.apple.com/auth/token", paramsForm);
        String tokenResult = null;
        try {
            tokenResult = HttpKits.post("https://appleid.apple.com/auth/token", headers, paramsForm);
        } catch (Exception e) {
            e.printStackTrace();
        }
        logger.info("[auth_token] ======> " + tokenResult);
        if (StringUtils.isBlank(tokenResult)) {
            throw new UtilException("Request Token Failed!");
        }

        JSONObject jsonObject = JSONObject.parseObject(tokenResult);
        String idToken = jsonObject.getString("id_token");
        if (idToken == null) {
            throw new UtilException("ID TOKEN ERROR!");
        }

其中

generateClientSecret(KID, TEAM_ID, CLIENT_ID, PRIMARY_KEY))

这个方法就是用于生成JWT的方法。下面帖子有介绍,不过可能不太方便看。

help in java_java 后端 sign in with apple 随笔_汤一白君的博客-CSDN博客

我这边重新贴一下代码,其中的参数前面已经让大家获取到了。

    /**
     * 生成clientSecret
     *
     * @param kid
     * @param teamId
     * @param clientId
     * @param primaryKey(写完发现,命名有误,privateKey)
     * @return
     */
    public String generateClientSecret(String kid, String teamId,
                                       String clientId, String primaryKey) {
        Map header = new HashMap<>();
        header.put("kid", kid);
        long second = System.currentTimeMillis() / 1000;
        //将private key字符串转换成PrivateKey 对象
        PrivateKey privateKey = null;
        try {
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(
                    readPrimaryKey(primaryKey));
            KeyFactory keyFactory = KeyFactory.getInstance("EC");
            privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 此处只需PrimaryKey
        Algorithm algorithm = Algorithm.ECDSA256(null,
                (ECPrivateKey) privateKey);
        // 生成JWT格式的client_secret
        String secret = JWT.create().withHeader(header).withClaim("iss", teamId)
                .withClaim("iat", second).withClaim("exp", 86400 * 180 + second)
                .withClaim("aud", APPLE_JWT_AUD_URL).withClaim("sub", clientId)
                .sign(algorithm);
        return secret;

    }

    private byte[] readPrimaryKey(String primaryKey) {
        StringBuilder pkcs8Lines = new StringBuilder();
        BufferedReader rdr = new BufferedReader(new StringReader(primaryKey));
        String line = "";
        try {
            while ((line = rdr.readLine()) != null) {
                pkcs8Lines.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 需要注意删除 "BEGIN" and "END" 行, 以及空格
        String pkcs8Pem = pkcs8Lines.toString();
        pkcs8Pem = pkcs8Pem.replace("-----BEGIN PRIVATE KEY-----", "");
        pkcs8Pem = pkcs8Pem.replace("-----END PRIVATE KEY-----", "");
        pkcs8Pem = pkcs8Pem.replaceAll("\\s+", "");
        // Base64 转码
        return Base64.decodeBase64(pkcs8Pem);
    }

上面代码中的Algorithm可能无法引入,需要去JWT令牌库中找到Java的Maven添加依赖项

下面就是JWT令牌库了。

JSON Web Token Libraries - jwt.io

        
            com.auth0
            java-jwt
            4.2.1
        

上面是我用的Maven依赖,大家可以自行选择。

五、id_token验证

获取到id_token之后就是验证id_token了

验证的方法我是用的这个帖子的方法

苹果授权登录(Sign in with Apple)-JAVA后端开发

验证后解析此token可获取到apple用户的信息,返回的数据格式如下:

{
    "at_hash": "CMq1E######Ai8zyw",
    "aud": "com.######.######service",
    "sub": "000724.######8175f70.0554",
    "nonce_supported": true,
    "auth_time": 1668775722,
    "iss": "https://appleid.apple.com",
    "exp": 1668862128,
    "iat": 1668775728
}

我的代码如下:

     try {
            Map map = new HashMap();
            //验证identityToken
            if (!verify(idToken)) {
                throw new UtilException("JSON Validated Failed!");
            }
            //对identityToken解码
            logger.info(" ===> " + idToken);
            JSONObject json = parserIdentityToken(idToken);
            if (json == null) {
                throw new UtilException("JSON Decoded Failed!");
            }
            String userId = json.getString("sub");  
            
            //下面是我项目里的业务逻辑了(换成你们处理拿到的用户信息的业务逻辑就行了)
            MGame mGame = mGameService.getById(appleIdentity.getGameId());
            Map form = new HashMap();
            form.put("uid", userId);
            form.put("game_id", appleIdentity.getGameId());
            form.put("platform_id", appleIdentity.getPlatformId());
            form.put("token", idToken);
            form.put("channel_code", "apple");
            form.put("os_type", appleIdentity.getOsType());
            HttpKits.buildGetUrl("xxxxx", form);
            String result = null;
            try {
                result = HttpKits.post("xxxxx", form);
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (StringUtils.isBlank(result)) {
                throw new UtilException("Login Failed!");
            }

            JSONObject tokenJson = JSONObject.parseObject(result);
            String data = tokenJson.getString("data");
            String code = tokenJson.getString("code");
            if (!code.equals("200")) {
                throw new UtilException("Decoded Failed!");
            }
            JSONObject dataJsonObject = JSONObject.parseObject(data);
            String super_user_id = dataJsonObject.getString("super_user_id");
            String token = dataJsonObject.getString("token");


            map.put("userId", userId);
            map.put("screenType", mGame.getScreenType().toString());
            map.put("superUserId", super_user_id);
            map.put("token", token);
            map.put("info", json.toJSONString());
            return AjaxResult.success("data", map);

        } catch (Exception e) {
            logger.error("app login error:" + e.getMessage(), e);
            throw new UtilException(e);
        }
    private static JSONArray getAuthKeys() throws Exception {
        String url = "https://appleid.apple.com/auth/keys";
        RestTemplate restTemplate = new RestTemplate();
        JSONObject json = restTemplate.getForObject(url, JSONObject.class);
        JSONArray arr = json.getJSONArray("keys");
        return arr;
    }

    public static Boolean verify(String jwt) throws Exception {
        JSONArray arr = getAuthKeys();
        if (arr == null) {
            return false;
        }
        JSONObject authKey = null;

        //先取苹果第一个key进行校验
        authKey = arr.getJSONObject(0);
        if (verifyExc(jwt, authKey)) {
            return true;
        } else {
            //再取第二个key校验
            authKey = arr.getJSONObject(1);
            if (verifyExc(jwt, authKey)) {
                return true;
            } else {
                //再取第三个key校验
                authKey = arr.getJSONObject(2);
                return verifyExc(jwt, authKey);
            }
        }

    }

    /**
     * 对前端传来的identityToken进行验证
     *
     * @param jwt     对应前端传来的 identityToken
     * @param authKey 苹果的公钥 authKey
     * @return
     * @throws Exception
     */
    public static Boolean verifyExc(String jwt, JSONObject authKey) throws Exception {

        Jwk jwa = Jwk.fromValues(authKey);
        PublicKey publicKey = jwa.getPublicKey();

        String aud = "";
        String sub = "";
        if (jwt.split("\\.").length > 1) {
            String claim = new String(Base64.decodeBase64(jwt.split("\\.")[1]));
            aud = JSONObject.parseObject(claim).get("aud").toString();
            sub = JSONObject.parseObject(claim).get("sub").toString();
        }
        JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);
        jwtParser.requireIssuer("https://appleid.apple.com");
        jwtParser.requireAudience(aud);
        jwtParser.requireSubject(sub);

        try {
            Jws claim = jwtParser.parseClaimsJws(jwt);
            if (claim != null && claim.getBody().containsKey("auth_time")) {
                System.out.println(claim);
                return true;
            }
            return false;
        } catch (ExpiredJwtException e) {
            logger.error("apple identityToken expired", e);
            return false;
        } catch (Exception e) {
            logger.error("apple identityToken illegal", e);
            return false;
        }
    }


    /**
     * 对前端传来的JWT字符串identityToken的第二部分进行解码
     * 主要获取其中的aud和sub,aud大概对应ios前端的包名,sub大概对应当前用户的授权的openID
     *
     * @param identityToken
     * @return {"aud":"com.xkj.****","sub":"000***.8da764d3f9e34d2183e8da08a1057***.0***","c_hash":"UsKAuEoI-****","email_verified":"true","auth_time":1574673481,"iss":"https://appleid.apple.com","exp":1574674081,"iat":1574673481,"email":"****@qq.com"}
     */
    public static JSONObject parserIdentityToken(String identityToken) {
        String[] arr = identityToken.split("\\.");
        Base64 base64 = new Base64();
        String decode = new String(base64.decodeBase64(arr[1]));
        String substring = decode.substring(0, decode.indexOf("}") + 1);
        JSONObject jsonObject = JSON.parseObject(substring);
        return jsonObject;
    }

其中应该要注意的点是,原帖子中只取了两个公钥public_key去验证,会有出现验证失败的情况,是因为 https://appleid.apple.com/auth/keys 这个链接的公钥实际是存在三个的(我请求的是三个,具体几个需要自行请求一下查看有多少个公钥),需要三个公钥全部去验证一下。

下面是我请求公钥链接的截图 

(vue + SpingBoot)前后端分离实现Apple登录的过程_第6张图片

到这里,apple登录整个流程就结束了, 流程看起来简单,但是实现起来处处碰壁踩坑,,,其中非常不爽的一点是接口测试必须在线上测试,所以代码中很多的logger.info()方法去打印结果就是因为线上无法获取准确的报错点,所以我就每个结果去打印测试。代码中应该还有很多不足的地方,欢迎大家踊跃地发表意见,期盼大佬们的指点~

以下是我的完整的代码,有需要的自取哈:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwk.Jwk;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.qpyx.common.annotation.SecretBody;
import com.qpyx.common.core.AjaxResult;
import com.qpyx.common.core.entity.AppleIdentity;
import com.qpyx.common.utils.HttpKits;
import com.qpyx.common.utils.StringUtils;
import com.qpyx.common.utils.exception.UtilException;
import com.qpyx.game.entity.MGame;
import com.qpyx.game.service.MGameService;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/apple")
public class AppleLoginController {

    @Autowired
    private MGameService mGameService;

    private static final Logger logger = LoggerFactory.getLogger(AppleLoginController.class);


    public static final String KID = "xx";
    public static final String TEAM_ID = "xx";
    public static final String CLIENT_ID = "xx";
    public static final String PRIMARY_KEY = "xx";
    public static final String APPLE_JWT_AUD_URL = "https://appleid.apple.com";

    @PostMapping("/token")
    public void getToken(AppleIdentity appleIdentity, HttpServletResponse resp) throws IOException {
        Cookie tokenCookie = new Cookie("ID_TOKEN", appleIdentity.getId_token());
        tokenCookie.setMaxAge(100); // Cookie的存活时间(自定义)
        tokenCookie.setDomain("xx.com");
        tokenCookie.setPath("/"); // 默认路径
        Cookie codeCookie = new Cookie("CODE", appleIdentity.getCode());
        codeCookie.setMaxAge(100); // Cookie的存活时间(自定义)
        codeCookie.setDomain("xx.com");
        codeCookie.setPath("/"); // 默认路径
        resp.addCookie(tokenCookie);
        resp.addCookie(codeCookie);
        resp.sendRedirect("https://xx.com/game/play");
    }

    @SecretBody
    @PostMapping("/verify")
    public AjaxResult appleLogin(@RequestBody AppleIdentity appleIdentity) {

        Map paramsForm = new HashMap();
        paramsForm.put("grant_type", "authorization_code");
        paramsForm.put("code", appleIdentity.getCode());
        paramsForm.put("client_id", CLIENT_ID);
        paramsForm.put("client_secret", generateClientSecret(KID, TEAM_ID, CLIENT_ID, PRIMARY_KEY));

        Map headers = new HashMap<>();
        headers.put(Header.CONTENT_TYPE, "application/x-www-form-urlencoded");

        HttpKits.buildGetUrl("https://appleid.apple.com/auth/token", paramsForm);
        String tokenResult = null;
        try {
            tokenResult = HttpKits.post("https://appleid.apple.com/auth/token", headers, paramsForm);
        } catch (Exception e) {
            e.printStackTrace();
        }
        logger.info("[auth_token] ======> " + tokenResult);
        if (StringUtils.isBlank(tokenResult)) {
            throw new UtilException("Request Token Failed!");
        }

        JSONObject jsonObject = JSONObject.parseObject(tokenResult);
        String idToken = jsonObject.getString("id_token");
        if (idToken == null) {
            throw new UtilException("ID TOKEN ERROR!");
        }

        try {
            Map map = new HashMap();
            //验证identityToken
            if (!verify(idToken)) {
                throw new UtilException("JSON Validated Failed!");
            }
            //对identityToken解码
            logger.info(" ===> " + idToken);
            JSONObject json = parserIdentityToken(idToken);
            logger.info(" ====>" + json.toString());
            if (json == null) {
                throw new UtilException("JSON Decoded Failed!");
            }
            String userId = json.getString("sub");

            MGame mGame = mGameService.getById(appleIdentity.getGameId());

            Map form = new HashMap();
            form.put("uid", userId);
            form.put("game_id", appleIdentity.getGameId());
            form.put("platform_id", appleIdentity.getPlatformId());
            form.put("token", idToken);
            form.put("channel_code", "apple");
            form.put("os_type", appleIdentity.getOsType());
            HttpKits.buildGetUrl("xx", form);
            String result = null;
            try {
                result = HttpKits.post("xx", form);
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (StringUtils.isBlank(result)) {
                throw new UtilException("Login Failed!");
            }

            JSONObject tokenJson = JSONObject.parseObject(result);
            String data = tokenJson.getString("data");
            String code = tokenJson.getString("code");
            if (!code.equals("200")) {
                throw new UtilException("Decoded Failed!");
            }
            JSONObject dataJsonObject = JSONObject.parseObject(data);
            String super_user_id = dataJsonObject.getString("super_user_id");
            String token = dataJsonObject.getString("token");


            map.put("userId", userId);
            map.put("screenType", mGame.getScreenType().toString());
            map.put("superUserId", super_user_id);
            map.put("token", token);
            map.put("info", json.toJSONString());
            return AjaxResult.success("data", map);

        } catch (Exception e) {
            logger.error("app login error:" + e.getMessage(), e);
            throw new UtilException(e);
        }
    }

    /**
     * 生成clientSecret
     *
     * @param kid
     * @param teamId
     * @param clientId
     * @param primaryKey(写完发现,命名有误,privateKey)
     * @return
     */
    public String generateClientSecret(String kid, String teamId,
                                       String clientId, String primaryKey) {
        Map header = new HashMap<>();
        header.put("kid", kid);
        long second = System.currentTimeMillis() / 1000;
        //将private key字符串转换成PrivateKey 对象
        PrivateKey privateKey = null;
        try {
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(
                    readPrimaryKey(primaryKey));
            KeyFactory keyFactory = KeyFactory.getInstance("EC");
            privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 此处只需PrimaryKey
        Algorithm algorithm = Algorithm.ECDSA256(null,
                (ECPrivateKey) privateKey);
        // 生成JWT格式的client_secret
        String secret = JWT.create().withHeader(header).withClaim("iss", teamId)
                .withClaim("iat", second).withClaim("exp", 86400 * 180 + second)
                .withClaim("aud", APPLE_JWT_AUD_URL).withClaim("sub", clientId)
                .sign(algorithm);
        return secret;

    }

    private byte[] readPrimaryKey(String primaryKey) {
        StringBuilder pkcs8Lines = new StringBuilder();
        BufferedReader rdr = new BufferedReader(new StringReader(primaryKey));
        String line = "";
        try {
            while ((line = rdr.readLine()) != null) {
                pkcs8Lines.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 需要注意删除 "BEGIN" and "END" 行, 以及空格
        String pkcs8Pem = pkcs8Lines.toString();
        pkcs8Pem = pkcs8Pem.replace("-----BEGIN PRIVATE KEY-----", "");
        pkcs8Pem = pkcs8Pem.replace("-----END PRIVATE KEY-----", "");
        pkcs8Pem = pkcs8Pem.replaceAll("\\s+", "");
        // Base64 转码
        return Base64.decodeBase64(pkcs8Pem);
    }

    private static JSONArray getAuthKeys() throws Exception {
        String url = "https://appleid.apple.com/auth/keys";
        RestTemplate restTemplate = new RestTemplate();
        JSONObject json = restTemplate.getForObject(url, JSONObject.class);
        JSONArray arr = json.getJSONArray("keys");
        return arr;
    }

    public static Boolean verify(String jwt) throws Exception {
        JSONArray arr = getAuthKeys();
        if (arr == null) {
            return false;
        }
        JSONObject authKey = null;

        //先取苹果第一个key进行校验
        authKey = arr.getJSONObject(0);
        if (verifyExc(jwt, authKey)) {
            return true;
        } else {
            //再取第二个key校验
            authKey = arr.getJSONObject(1);
            if (verifyExc(jwt, authKey)) {
                return true;
            } else {
                authKey = arr.getJSONObject(2);
                return verifyExc(jwt, authKey);
            }
        }

    }

    /**
     * 对前端传来的identityToken进行验证
     *
     * @param jwt     对应前端传来的 identityToken
     * @param authKey 苹果的公钥 authKey
     * @return
     * @throws Exception
     */
    public static Boolean verifyExc(String jwt, JSONObject authKey) throws Exception {

        Jwk jwa = Jwk.fromValues(authKey);
        PublicKey publicKey = jwa.getPublicKey();

        String aud = "";
        String sub = "";
        if (jwt.split("\\.").length > 1) {
            String claim = new String(Base64.decodeBase64(jwt.split("\\.")[1]));
            aud = JSONObject.parseObject(claim).get("aud").toString();
            sub = JSONObject.parseObject(claim).get("sub").toString();
        }
        JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);
        jwtParser.requireIssuer("https://appleid.apple.com");
        jwtParser.requireAudience(aud);
        jwtParser.requireSubject(sub);

        try {
            Jws claim = jwtParser.parseClaimsJws(jwt);
            if (claim != null && claim.getBody().containsKey("auth_time")) {
                System.out.println(claim);
                return true;
            }
            return false;
        } catch (ExpiredJwtException e) {
            logger.error("apple identityToken expired", e);
            return false;
        } catch (Exception e) {
            logger.error("apple identityToken illegal", e);
            return false;
        }
    }


    /**
     * 对前端传来的JWT字符串identityToken的第二部分进行解码
     * 主要获取其中的aud和sub,aud大概对应ios前端的包名,sub大概对应当前用户的授权的openID
     *
     * @param identityToken
     * @return {"aud":"com.xkj.****","sub":"000***.8da764d3f9e34d2183e8da08a1057***.0***","c_hash":"UsKAuEoI-****","email_verified":"true","auth_time":1574673481,"iss":"https://appleid.apple.com","exp":1574674081,"iat":1574673481,"email":"****@qq.com"}
     */
    public static JSONObject parserIdentityToken(String identityToken) {
        String[] arr = identityToken.split("\\.");
        Base64 base64 = new Base64();
        String decode = new String(base64.decodeBase64(arr[1]));
        String substring = decode.substring(0, decode.indexOf("}") + 1);
        JSONObject jsonObject = JSON.parseObject(substring);
        return jsonObject;
    }
}

你可能感兴趣的:(java,spring,boot)