Apple官方文档
前言:由于公司最近有个业务需求是要进行Apple账号授权登录,于是我看边看文档边借鉴其他人的写法,发现好多文章都有一个共性,一个是在解析JWT的时候自己设置参数后进行判断,这样做没什么意义,另外一个就是大多数人直接取苹果公钥的第二个值进行验证,这样写法是错的,正常做法是将jwt的hender进行解开,得到kid,然后根据kid对苹果公钥进行匹配得到正确的keys。下面分享我自己的写法。
https://appleid.apple.com/auth/keys
公钥建议去请求得到最新的,因为公钥会经常换,当公钥不是最新时,在解析时会因为匹配不上而导致登录不成功,具体实现看文章下面的getAuthKeys方法。
引入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登陆实现的整个流程。