【JAVA】接入苹果授权登录

一、背景

公司上架APP到Apple Store,审核被打回,原因是APP中接入了其他第三方登录,所以必须要求接入苹果登录,这项新规定,逼不得已只要接入苹果登录。

二、使用JWT方式接入登录

参考网上很多资料,我看到的都是使用JWT方式接入的,之所以要自己再写一篇同样接入方式的文章,是因为在参考过程中,踩了坑,现在把自己测试成功的方式贴出来,避免后者再踩坑。

1.引入JWT相关工具包

<dependency>
     <groupId>com.auth0</groupId>
     <artifactId>jwks-rsa</artifactId>
     <version>0.12.0</version>
 </dependency>
 <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>fastjson</artifactId>
     <version>1.2.80</version>
 </dependency>
 <dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.1</version>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
     <version>2.7.0</version>
 </dependency>

以上内容是测试demo中引入的内容,如果项目已存在相同的包可以不用重复引入,核心包jjwt、jwks-rsa。

2.请求参数说明

  • IdentityToken:identityToken 是App端请求苹果授权中心拿到的JWT,它由点号“.”分割为三部分,分别是头信息,消息体,签名,前两部分可以通过Base64解码为如下信息。

消息头

{
    "alg": "ES256",
    "kid": "ABC123DEFG"
}

消息体(仅罗列主要字段)

{
    "iss": "https://appleid.apple.com",
    "iat": 1437179036,
    "exp": 1493298100,
    "aud": "com.mytest.app",
    "sub": "DEF123GHIJ "
}
字段名称说明
iss签发机构地址
iat授权时间戳
exp过期时间戳
audbundle id
sub用户唯一标识

3.Java代码实现
核心工具类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwk.Jwk;
import io.jsonwebtoken.*;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.Base64;
import java.util.Map;

/**
 * @Classname IOSToeknUtils
 * @Description IOS token操作工具
 * @Date 2023/03/23
 * @Created by Goden
 */
public class IOSToeknUtils {

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

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

    /**
     * 解码identityToken
     * @param identityToken
     * @return
     */
    public static JSONObject parserIdentityToken(String identityToken) {
        String[] arr = identityToken.split("\\.");

        String firstDate = new String(Base64.getDecoder().decode(arr[0]), StandardCharsets.UTF_8);
        String decode = new String(Base64.getDecoder().decode(arr[1]), StandardCharsets.UTF_8);
        JSONObject claimObj = JSON.parseObject(decode);
        // 将第一部分获取到的kid放入消息体中,方便后续匹配对应的公钥使用
        claimObj.put("kid", JSONObject.parseObject(firstDate).get("kid"));
        return claimObj;
    }

    /**
     * 根据kid获取对应的苹果公钥
     * @param kid
     * @return
     */
    public static PublicKey getPublicKey(String kid) {
        try {
            RestTemplate restTemplate = new RestTemplate();
            JSONObject data = restTemplate.getForObject(authUrl, JSONObject.class);
            assert data != null;
            JSONArray jsonArray = data.getJSONArray("keys");
            for (Object obj : jsonArray) {
                Map json = ((Map) obj);
                // 获取kid对应的公钥
                if (json.get("kid").equals(kid)) {
                    Jwk jwa = Jwk.fromValues(json);
                    return jwa.getPublicKey();
                }
            }
        } catch (Exception e) {

        }
        return null;
    }

    /**
     * 对前端传来的identityToken进行验证
     *
     * @param identityToken
     * @param jsonObject
     * @return
     * @throws Exception
     */
    public static Boolean verifyExc(String identityToken, JSONObject jsonObject) throws Exception {
        String kid = (String) jsonObject.get("kid");
        PublicKey publicKey = getPublicKey(kid);

        JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey);
        jwtParser.requireIssuer(authIss);
        jwtParser.requireAudience((String) jsonObject.get("aud"));
        jwtParser.requireSubject((String) jsonObject.get("sub"));
        try {
            Jws<Claims> claim = jwtParser.parseClaimsJws(identityToken);
            if (claim != null && claim.getBody().containsKey("auth_time")) {
                return true;
            }
            return false;
        } catch (ExpiredJwtException e) {
            return false;
        } catch (Exception e) {
            return false;
        }
    }
}

测试类

import com.alibaba.fastjson.JSONObject;

/**
 * @Classname TestIOSLogin
 * @Description 测试IOS登录
 * @Date 2023/3/23
 * @Created by Goden
 */
public class TestIOSLogin {
    public static void main(String[] args) {
        // 请求的JWT
        String identityToken = "eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ." +
                "eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmRpeWl5aW4ub25saW5lNTMiLCJl" +
                "eHAiOjE1OTc2NTAxNzQsImlhdCI6MTU5NzY0OTU3NCwic3ViIjoiMDAxMzc3LmQ0ZDVmMTAwODQ0ZTQzZjdiMWM1O" +
                "WRiMzUyZWZkZmI4LjAyNTkiLCJjX2hhc2giOiJkTDVRdld2VTNjVHBxczNSazlUTnRBIiwiZW1haWwiOiI0OTk4O" +
                "TY1MDdAcXEuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTk3NjQ5NTc0LCJub25jZV9" +
                "zdXBwb3J0ZWQiOnRydWV9." +
                "hM9HjNsMJW2PjYP7SfbzF-GqOt0VnMjYGq4BoU68rkQ-K2lPp_ae5ziX6Bbr3WHg" +
                "6cc3Z8OzGO63OfExvSj9gQTR596CZLvNGXhbI3piTK6597-cYsPCTbY7xHxgdHLuL8XhD-9dXPn9rouVYu4QA1" +
                "8JBQG1Q4sGsRzLEJ5DjOM9x1bkBz4Vu_5LEOefHFHkWN_RPCh_AOJGviDzm81kTkCTWn8jpm0tGdevMR93MOf44" +
                "f7bjP2T8yezl0Vbv09TrnkdAqG0BsihCD0VN9JV7X2eagyumoxTdFfoRiOflFKAaQqohVzcqy9tHOGm_6w5h8bsR" +
                "CmtBC4PnqIFqNy_AQ";
        // 解码后的消息体
        JSONObject playloadObj = IOSToeknUtils.parserIdentityToken(identityToken);
        Boolean success;
        try {
            success = IOSToeknUtils.verifyExc(identityToken, playloadObj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        
        if (!success) {
            // TODO 校验token失败具体操作
            return;
        }
        
        // TODO 检验token成功具体业务操作。。。
    }
}

注意

截止目前,调用接口获取苹果公钥,会返回3个公钥信息,每个公钥信息中都有“kid”这个字段,需要获取解码identityToken头信息中“kid”值匹配的公钥,而不是直接拿返回的公钥数组中第一个公钥来做校验,否者会报如下错误

io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值