苹果授权登录(Sign in with Apple)
1、苹果授权登陆方式
第一种: PC/M端授权登陆,采用协议类似于oauth2协议,服务端基于授权码验证
第二种: App端授权登陆,服务端基于JWT的算法验证
第一种方式的验证逻辑如下:
## 获取/刷新token
- 首先获取code:GET
https://appleid.apple.com/auth/authorize?response_type=code&client_id=&redirect_uri=&state=1234
参考上面的后台配置,其中client_id对应的是Services ID,redirect_uri就是后台配置的接收code码的地址
- 根据code获取token:POST
https://appleid.apple.com/auth/token?grant_type=authorization_code&code=code&redirect_uri=url&client_id=id&client_secret=secret
## 获取令牌所需参数:
1. grant_type:'authorization_code'为获取令牌
2. client_id:client_id
3. redirect_uri:redirect_uri
4. code:上一步获取到的授权码
5. codeclient_secret:secret(一个自己生成的jwt https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens)<br>
返回值示例
{ "access_token": "a0996b16cfb674c0eb0d29194c880455b.0.nsww.5fi5MVC-i3AVNhddrNg7Qw", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "r9ee922f1c8b048208037f78cd7dfc91a.0.nsww.KlV2TeFlTr7YDdZ0KtvEQQ", "id_token": "eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuYXBwbGVsb2dpbmRlbW8iLCJleHAiOjE1NjU2NjU1OTQsImlhdCI6MTU2NTY2NDk5NCwic3ViIjoiMDAwMjY2LmRiZTg2NWIwYWE3MjRlMWM4ODM5MDIwOWI5YzdkNjk1LjAyNTYiLCJhdF9oYXNoIjoiR0ZmODhlX1ptc0pqQ2VkZzJXem85ZyIsImF1dGhfdGltZSI6MTU2NTY2NDk2M30.J6XFWmbr0a1hkJszAKM2wevJF57yZt-MoyZNI9QF76dHfJvAmFO9_RP9-tz4pN4ua3BuSJpUbwzT2xFD_rBjsNWkU-ZhuSAONdAnCtK2Vbc2AYEH9n7lB2PnOE1mX5HwY-dI9dqS9AdU4S_CjzTGnvFqC9H5pt6LVoCF4N9dFfQnh2w7jQrjTic_JvbgJT5m7vLzRx-eRnlxQIifEsHDbudzi3yg7XC9OL9QBiTyHdCQvRdsyRLrewJT6QZmi6kEWrV9E21WPC6qJMsaIfGik44UgPOnNnjdxKPzxUAa-Lo1HAzvHcAX5i047T01ltqvHbtsJEZxAB6okmwco78JQA" }
## 刷新令牌所需参数:
1. grant_type:'refresh_token'为刷新令牌
2. client_id:client_id
3. client_secret:client_secret有效期最长6个月
4. refresh_token:上一步获取到的id_token
## 对id_token解密
- 通过 GET:https://appleid.apple.com/auth/keys 接口获取公钥
{ "keys": [ { "kty": "RSA", "kid": "86D88Kf", "use": "sig", "alg": "RS256", "n": "iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ", "e": "AQAB" }, { "kty": "RSA", "kid": "eXaunmL", "use": "sig", "alg": "RS256", "n": "4dGQ7bQK8LgILOdLsYzfZjkEAoQeVC_aqyc8GC6RX7dq_KvRAQAWPvkam8VQv4GK5T4ogklEKEvj5ISBamdDNq1n52TpxQwI2EqxSk7I9fKPKhRt4F8-2yETlYvye-2s6NeWJim0KBtOVrk0gWvEDgd6WOqJl_yt5WBISvILNyVg1qAAM8JeX6dRPosahRVDjA52G2X-Tip84wqwyRpUlq2ybzcLh3zyhCitBOebiRWDQfG26EH9lTlJhll-p_Dg8vAXxJLIJ4SNLcqgFeZe4OfHLgdzMvxXZJnPp_VgmkcpUdRotazKZumj6dBPcXI_XID4Z4Z3OM1KrZPJNdUhxw", "e": "AQAB" } ] }
- 然后我们用jwt.verify通过公钥解密id_token
- 解密后得到的verify.sub就是用户apple账号登录在该程序中的唯一标识,我们可以把它存到程序的数据库中与用户信息做映射,用于标识用户身份
2、重点讲解苹果授权登陆服务端如何验证
2.1、基于授权码验证
首先需要了解如何构建client_secret,详细文档可以参考如下两个:
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens
首先说下client_secret的构建方法:
先在后台生成授权应用APP ID的密钥KEY文件,然后下载密钥文件格式样例: -----BEGIN PRIVATE KEY----- BASE64编码后的密钥 -----END PRIVATE KEY----- public byte[] readKey() throws Exception { String temp = "密钥文件中间的编码字符串"; return Base64.decodeBase64(temp); } 构建client_secret关键代码: String client_id = "..."; // 被授权的APP ID Map<String, Object> header = new HashMap<String, Object>(); header.put("kid", "密钥id"); // 参考后台配置 Map<String, Object> claims = new HashMap<String, Object>(); claims.put("iss", "team id"); // 参考后台配置 team id long now = System.currentTimeMillis() / 1000; claims.put("iat", now); claims.put("exp", now + 86400 * 30); // 最长半年,单位秒 claims.put("aud", "https://appleid.apple.com"); // 默认值 claims.put("sub", client_id); PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(readKey()); KeyFactory keyFactory = KeyFactory.getInstance("EC"); PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); String client_secret = Jwts.builder().setHeader(header).setClaims(claims).signWith(SignatureAlgorithm.ES256, privateKey).compact();
如何验证?
String url = "https://appleid.apple.com/auth/token"; // POST 请求 HttpSynClient client = new HttpSynClient(5000, 5000, 5000, 20); Map<String, String> form = new HashMap<String, String>(); form.put("client_id", client_id); form.put("client_secret", client_secret); form.put("code", code);
form.put("grant_type","authorization_code"); form.put("redirect_uri", redirectUrl); HttpResponse result = client.excutePost(url, form); System.out.println(result);
返回值样例:
{ "access_token":"a0996b16cfb674c0eb0d29194c880455b.0.nsww.5fi5MVC-i3AVNhddrNg7Qw", "token_type":"Bearer", "expires_in":3600, "refresh_token":"r9ee922f1c8b048208037f78cd7dfc91a.0.nsww.KlV2TeFlTr7YDdZ0KtvEQQ", "id_token":"eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuYXBwbGVsb2dpbmRlbW8iLCJleHAiOjE1NjU2NjU1OTQsImlhdCI6MTU2NTY2NDk5NCwic3ViIjoiMDAwMjY2LmRiZTg2NWIwYWE3MjRlMWM4ODM5MDIwOWI5YzdkNjk1LjAyNTYiLCJhdF9oYXNoIjoiR0ZmODhlX1ptc0pqQ2VkZzJXem85ZyIsImF1dGhfdGltZSI6MTU2NTY2NDk2M30.J6XFWmbr0a1hkJszAKM2wevJF57yZt-MoyZNI9QF76dHfJvAmFO9_RP9-tz4pN4ua3BuSJpUbwzT2xFD_rBjsNWkU-ZhuSAONdAnCtK2Vbc2AYEH9n7lB2PnOE1mX5HwY-dI9dqS9AdU4S_CjzTGnvFqC9H5pt6LVoCF4N9dFfQnh2w7jQrjTic_JvbgJT5m7vLzRx-eRnlxQIifEsHDbudzi3yg7XC9OL9QBiTyHdCQvRdsyRLrewJT6QZmi6kEWrV9E21WPC6qJMsaIfGik44UgPOnNnjdxKPzxUAa-Lo1HAzvHcAX5i047T01ltqvHbtsJEZxAB6okmwco78JQA" }
其中id_token是一个JWT,其中claims中的sub就是授权的用户唯一标识,该token也可以使用上述的验证方法进行有效性验证,另外授权code是有时效性的,且使用一次即失效
2.2、基于JWT的算法验证
使用到的Apple公钥接口:https://appleid.apple.com/auth/keys
详细接口文档说明参见:https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature
前端提交identityToken,服务端通过https://appleid.apple.com/auth/keys获取publicKey(数组结构,上一个失败循环使用下一个),然后验证token(jwt验证),最后解析token获取用户信息(sub即openId)
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 AppleUtil { private static final Logger logger = LoggerFactory.getLogger(AppleUtil.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(!AppleUtil.verify(identityToken)){ return new ApiResult(ApiCode.VALIDATION_ERROR, "授权验证失败"); } //对identityToken解码 JSONObject json = AppleUtil.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, "系统错误"); } }
链接
https://www.jianshu.com/p/0c12dc2a214e
https://cloud.tencent.com/developer/ask/sof/691914
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
https://blog.csdn.net/w_monster/article/details/124171787
https://blog.csdn.net/wpf199402076118/article/details/99677412
https://www.cnblogs.com/javallh/p/14168996.html

浙公网安备 33010602011771号