1、 业务逻辑分析
IOS授权登录流程与微信授权登录大同小异,唯一区别的在于需要调用苹果api获取公钥,接口地址为:https://appleid.apple.com/auth/keys。
首先是IOS APP端拿到identifyToken交给后端,后端拿到identifyToken后,首先调用IOS的公钥API拿到IOS的公钥,这里会获取到两个公钥,然后使用公钥对identifyToken进行校验,校验通过后,对identityToken进行解码,解码后可以到授权的唯一标识sub,之后做业务侧的注册登录逻辑。
这里有一个坑,就是在校验identifyToken的时候,偶尔会校验不通过,原因是我们通过IOS的公钥API会拿到两个密钥,如果只拿其中一个去做校验,就会出现这种情况,所以当第一个密钥校验不通过的时候,再拿第二个密钥再做一次校验。
大致流程如下图:
代码实现
需要添加的依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.9.0</version>
</dependency>
工具类实现
public class AppleUitl {
private static final Logger logger = LoggerFactory.getLogger(AppleUitl.class);
/**
* 获取苹果的公钥
* @return
* @throws Exception
*/
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 = JSONObject.parseObject(arr.getString(0));
if(verifyExc(jwt, authKey)){
return true;
}else{
//再取第二个key校验
authKey = JSONObject.parseObject(arr.getString(1));
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<Claims> 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;
}
}
业务侧注册登录,具体的注册登录逻辑这里不再详细介绍
/**
* iOS appleid 授权登录
* @param identityToken
* @param request
* @return
*/
@Transactional
public ApiResult appleLogin(String identityToken,HttpServletRequest request){
try {
Map<String, String> map = new HashMap<String, String>();
//验证identityToken
if(!AppleUitl.verify(identityToken)){
return new ApiResult(ApiCode.VALIDATION_ERROR, "授权验证失败");
}
//对identityToken解码
JSONObject json = AppleUitl.parserIdentityToken(identityToken);
if(json == null){
return new ApiResult(ApiCode.VALIDATION_ERROR, "授权解码失败");
}
String ip = Servlets.getRemoteAddr(request);
User user = oauthAppleService.apple_login(json, ip);
//将用户信息存到redis并返回token, token有效期为31天
String token = tokenRedisService.put(user);
map.put("token",token);
return new ApiResult(ApiCode.SUCCESS, "success", map);
}catch (Exception e){
logger.error("app wxLogin error:" + e.getMessage(),e);
return new ApiResult(ApiCode.SYS_EXCEPTION, "系统错误");
}
}