苹果apple账号授权登录第三方APP

Apple官方文档
前言:由于公司最近有个业务需求是要进行Apple账号授权登录,于是我看边看文档边借鉴其他人的写法,发现好多文章都有一个共性,一个是在解析JWT的时候自己设置参数后进行判断,这样做没什么意义,另外一个就是大多数人直接取苹果公钥的第二个值进行验证,这样写法是错的,正常做法是将jwt的hender进行解开,得到kid,然后根据kid对苹果公钥进行匹配得到正确的keys。下面分享我自己的写法。

一:获取苹果公钥

https://appleid.apple.com/auth/keys苹果apple账号授权登录第三方APP_第1张图片
公钥建议去请求得到最新的,因为公钥会经常换,当公钥不是最新时,在解析时会因为匹配不上而导致登录不成功,具体实现看文章下面的getAuthKeys方法。

二:验证JWT

引入JWT所需Maven<dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.3</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>jwks-rsa</artifactId>
            <version>0.12.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
   @Autowired
    protected RestTemplate restTemplate;
   
    /**
     * identityToken授权判断       
     * 这里代码只展示验证整个过程,具体其他业务得结合自己的公司去实现
     */
    public Response<AppleUserDetailsVo > appleLoginVerify(String fullName,String identityToken) throws Exception {
        log.info("登录接受参数----->fullName:" + fullName);//这里要求传fullName是因为苹果在第一次登陆的时候才会返回fullName,而且在验证通过后还获取不到fullName,邮箱的话在验证通过后可以获取到,所以不需要App传邮箱
        if (!verify(identityToken)) {//验证identityToken
            return Response.failure(100001, "Apple identity token expired.");
        }
        //identityToken验证通过之后进行解码,再结合自己所需的参数返回给App,
        AppleUserDetailsVo appleUserDetailsVo = parserIdentityToken(identityToken);
        if (null != appleUserDetailsVo) {
            appleUserDetailsVo.setFullName(fullName);
            appleUserDetailsVo.setFullEmail(appleUserDetailsVo.getEmail());
            appleUserDetailsVo.setGender(0);
            appleUserDetailsVo.setIdentityType(1);
            appleUserDetailsVo.setSource(2);
            appleUserDetailsVo.setPicture(Const.PICTURE);
        } else {
            return Response.failure(200001, "Apple identity token decoding failed.");
        }
        log.info("AppleUserDetailsVo值" + appleUserDetailsVo);
        return Response.data(appleUserDetailsVo);
    }

    /**
     * 对前端传来的JWT字符串identityToken的Payload(第二段)进行解码
     * 主要获取其中的aud和sub,aud大概对应ios前端的包名,sub大概对应当前用户的授权的openID
     */
    private AppleUserDetailsVo parserIdentityToken(String identityToken) throws IOException {
        String[] arr = identityToken.split("\\.");
        String decode = new String(Base64.decodeBase64(arr[1]));
        log.info("苹果登录后获取到的值:" + decode);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return objectMapper.readValue(decode, AppleUserDetailsVo.class);
    }
    


    private Boolean verify(String jwt) throws Exception {
        List<ApplePublicKeyVo> arr = getAuthKeys();
        if (arr == null) {
            return false;
        }
        try {
            //先取jwt中的header来匹配苹果公钥的kid
            String header = new String(Base64.decodeBase64(jwt.split("\\.")[0]));
            ObjectMapper objectMapper = new ObjectMapper();
            ApplePublicKeyVo applePublicKeyVo = objectMapper.readValue(header, ApplePublicKeyVo.class);
            Optional<ApplePublicKeyVo> target;
            target = arr.stream().filter(apple -> apple.equals(applePublicKeyVo)).findFirst();
            return verifyExc(jwt, target.get());
        } catch (Exception e) {
            return false;
        }



    
    /**
     * 对前端传来的identityToken进行验证
     *
     * @param jwt     对应前端传来的 identityToken
     * @param authKey 苹果的公钥 authKey
     */
    private static Boolean verifyExc(String jwt, ApplePublicKeyVo authKey) throws InvalidPublicKeyException {
        try {
            PublicKey publicKey;
            if (!"RSA".equalsIgnoreCase(authKey.getKty())) {
                throw new InvalidPublicKeyException("The key is not of type RSA", (Throwable) null);
            } else {
                KeyFactory kf = KeyFactory.getInstance("RSA");
                BigInteger modulus = new BigInteger(1, Base64.decodeBase64(authKey.getN()));
                BigInteger exponent = new BigInteger(1, Base64.decodeBase64(authKey.getE()));
                publicKey = kf.generatePublic(new RSAPublicKeySpec(modulus, exponent));

            }
            Claims parse = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(jwt).getBody();
            String iss = parse.getIssuer();//苹果官方链接
            String audience = parse.getAudience();//app设置的包名
            int expiration = (int) (parse.getExpiration().getTime() / 1000);//过期时间
            Integer authTime = (Integer) parse.get("auth_time");//签发时间
            if (iss.contains(Const.ISS) && audience.equals(Const.AUD) && authTime < expiration) {//按照苹果官方来做,iss要存在https://appleid.apple.com,audience要与App设置的一致,签发时间要小于过期时间
                return true;
            } else {
                return false;
            }
        } catch (InvalidKeySpecException var4) {
            throw new InvalidPublicKeyException("Invalid public key", var4);
        } catch (NoSuchAlgorithmException var5) {
            throw new InvalidPublicKeyException("Invalid algorithm to generate key", var5);
        } catch (ExpiredJwtException e) {
            log.error("[AppleServiceImpl.verifyExc] [error] [apple identityToken expired]", e);
            return false;
        } catch (Exception e) {
            log.error("[AppleServiceImpl.verifyExc] [error] [apple identityToken illegal]", e);
            return false;
        }
    }


    /**
     * 获取苹果的公钥
     */
    private static List<ApplePublicKeyVo> getAuthKeys() throws IOException {
            String url = "https://appleid.apple.com/auth/keys";//获取苹果公钥
        JSONObject json;
        try {
            json = restTemplate.getForObject(url, JSONObject.class);
            log.info("苹果公钥" + json);
        } catch (Exception e) {
            log.info("AppleLogin获取公钥发生错误,先用本地的公钥" + e);
            json = JSONObject.fromObject(Const.APPLE_KEYS);
        }
        if (json != null && json.optJSONArray("keys") != null) {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.readValue(json.getJSONArray("keys").toString(), new TypeReference<List<ApplePublicKeyVo>>() {
            });
        }
        return null;
    }




@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "授权登录后返回的对象")
public class AppleUserDetailsVo implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "签发者")
    private String iss;

    @ApiModelProperty(value = "目标受众")
    private String aud;

    @ApiModelProperty(value = "过期时间")
    private Integer exp;

    @ApiModelProperty(value = "签发时间")
    private Integer iat;

    @ApiModelProperty(value = "苹果userId")
    private String sub;

    @ApiModelProperty(value = "哈希数列")
    private String c_hash;

    @ApiModelProperty(value = "邮箱")
    private String email;

    @ApiModelProperty(value = "邮箱验证")
    private String email_verified;

    @ApiModelProperty(value = "是否是私有邮箱")
    private String is_private_email;

    @ApiModelProperty(value = "验证时间")
    private Integer auth_time;

    private boolean nonce_supported;

    private Integer real_user_status;

    @ApiModelProperty(value = "自定义参数:第三方拿到的名称")
    private String fullName;

    @ApiModelProperty(value = "自定义参数:第三方拿到的邮箱")
    private String fullEmail;

    @ApiModelProperty(value = "自定义参数:第三方拿到的性别")
    private Integer gender;

    @ApiModelProperty(value = "自定义参数:头像")
    private String picture;

    @ApiModelProperty(value = "自定义参数:身份类型(0邮箱 1是苹果 2是Google 3是 Facebook 4是Twitter)")
    private Integer identityType;

    @ApiModelProperty(value = "自定义参数:注册来源 1是安卓 2是IOS")
    private Integer source;

    @ApiModelProperty(value = "自定义参数:app标识")
    private Integer ascription;
}


@Accessors(chain = true)
@ApiModel(value = "苹果公钥")
public class ApplePublicKeyVo implements Serializable {
    private static final long serialVersionUID = 1L;

    private String kty;

    private String kid;

    private String use;

    private String alg;

    private String n;

    private String e;

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    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 getUse() {
        return use;
    }

    public void setUse(String use) {
        this.use = use;
    }

    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;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ApplePublicKeyVo that = (ApplePublicKeyVo) o;
        return Objects.equals(kid, that.kid) &&
                Objects.equals(alg, that.alg);
    }

    @Override
    public int hashCode() {
        return Objects.hash(kty, kid, use, alg, n, e);
    }
}

以上就是Apple登陆实现的整个流程。

你可能感兴趣的:(java,后端)