Sign|Sign in with Apple(苹果授权登陆)服务端验证

此文章来源自网络,目的在于方便广大开发者,转载请注明原文出处,感谢。 文章来源
苹果授权登陆方式 【Sign|Sign in with Apple(苹果授权登陆)服务端验证】1、PC/M端授权登陆,采用协议类似于oauth2协议
2、 App端授权登陆,提供两种后端验证方式
开发者后台配置 详细配置参考该文档,手把手教学
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
1、 PC/M接入方式 后台配置请参考下方文档,其中client_id对应的是Services ID,redirect_uri就是后台配置的接收code码的地址
https://appleid.apple.com/auth/authorize?response_type=code&client_id=&redirect_uri=&state=1234
2、APP端客户端授权登陆功能开发 可以参考如下文档,重点讲解苹果授权登陆后端如何验证
https://www.jianshu.com/p/23b46dea2076
针对后端验证苹果提供了两种验证方式,一种是基于JWT的算法验证,另外一种是基于授权码的验证
1、基于JWT的算法验证

  • 使用到的Apple公钥接口:https://appleid.apple.com/auth/keys
  • 详细接口文档说明参见:https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature
  • 接口返回值:
//接口返回值 { "keys": [ { "kty": "RSA", "kid": "AIDOPK1", "use": "sig", "alg": "RS256", "n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w", "e": "AQAB" } ] }

kid,为密钥id标识,签名算法采用的是RS256(RSA 256 + SHA 256),kty常量标识使用RSA签名算法,其公钥参数为n和e,其值采用了BASE64编码,使用时需要先解码
  • 使用方式:APP内苹果授权登陆会提供如下几个参数:userID、email、fullName、authorizationCode、identityToken
    • userID:授权的用户唯一标识
    • email、fullName:授权的用户资料
    • authorizationCode:授权code
    • identityToken:授权用户的JWT凭证
下面针对identityToken后端验证做简要说明: identityToken参考样例:
// jwt 格式 该token的有效期是10分钟 eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNreW1pbmcuZGV2aWNlbW9uaXRvciIsImV4cCI6MTU2NTY2ODA4NiwiaWF0IjoxNTY1NjY3NDg2LCJzdWIiOiIwMDEyNDcuOTNiM2E3OTlhN2M4NGMwY2I0NmNkMDhmMTAwNzk3ZjIuMDcwNCIsImNfaGFzaCI6Ik9oMmFtOWVNTldWWTNkcTVKbUNsYmciLCJhdXRoX3RpbWUiOjE1NjU2Njc0ODZ9.e-pdwK4iKWErr_Gcpkzo8JNi_MWh7OMnA15FvyOXQxTx0GsXzFT3qE3DmXqAar96nx3EqsHI1Qgquqt2ogyj-lLijK_46ifckdqPjncTEGzVWkNTX8uhY7M867B6aUnmR7u-cf2HsmhXrvgsJLGp2TzCI3oTp-kskBOeCPMyTxzNURuYe8zabBlUy6FDNIPeZwZXZqU0Fr3riv2k1NkGx5MqFdUq3z5mNfmWbIAuU64Z3yKhaqwGd2tey1Xxs4hHa786OeYFF3n7G5h-4kQ4lf163G6I5BU0etCRSYVKqjq-OL-8z8dHNqvTJtAYanB3OHNWCHevJFHJ2nWOTT3sbw // header 解码 {"kid":"AIDOPK1","alg":"RS256"} 其中kid对应上文说的密钥id // claims 解码 { "iss":"https://appleid.apple.com",// 苹果签发的标识 "aud":"com.skyming.devicemonitor", // 接收者的APP ID "exp":1565668086,"iat":1565667486, "sub":"001247.93b3a799a7c84c0cb46cd08f100797f2.0704", //用户的唯一标识 "c_hash":"Oh2am9eMNWVY3dq5JmClbg", "auth_time":1565667486 }

其中 iss标识是苹果签发的,aud是接收者的APP ID,该token的有效期是10分钟,sub就是用户的唯一标识
如何验证?
#首先通过identityToken中的header中的kid,然后结合苹果获取公钥的接口,拿到相应的n和e的值,然后通过下面这个方法构建RSA公钥 public RSAPublicKeySpec build(String n, String e) { BigInteger modulus = new BigInteger(1, Base64.decodeBase64(n)); BigInteger publicExponent = new BigInteger(1, Base64.decodeBase64(e)); return new RSAPublicKeySpec(modulus, publicExponent); } #获取验证所需的PublicKey public PublicKey getPublicKey(String n,String e)throws NoSuchAlgorithmException, InvalidKeySpecException { BigInteger bigIntModulus = new BigInteger(1,Base64.decodeBase64(n)); BigInteger bigIntPrivateExponent = new BigInteger(1,Base64.decodeBase64(e)); RSAPublicKeySpec keySpec = new RSAPublicKeySpec(bigIntModulus, bigIntPrivateExponent); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey publicKey = keyFactory.generatePublic(keySpec); return publicKey; }#通过下面这个方法验证JWT的有效性 # jwt 就是 identityToken:授权用户的JWT凭证 # audience就是APPID # subject 就是 就是userId public int verify(PublicKey key, String jwt, String audience, String subject) { JwtParser jwtParser = Jwts.parser().setSigningKey(key); jwtParser.requireIssuer("https://appleid.apple.com"); jwtParser.requireAudience(audience); jwtParser.requireSubject(subject); try { Jws claim = jwtParser.parseClaimsJws(jwt); if (claim != null && claim.getBody().containsKey("auth_time")) { return GlobalCode.SUCCESS; } return GlobalCode.THIRD_AUTH_CODE_INVALID; } catch (ExpiredJwtException e) { log.error("apple identityToken expired", e); return GlobalCode.THIRD_AUTH_CODE_INVALID; } catch (Exception e) { log.error("apple identityToken illegal", e); return GlobalCode.FAIL_ILLEGAL_REQ; } } #使用的JWT工具库为: io.jsonwebtoken jjwt 0.9.1

2、基于授权码的后端验证 首先需要了解如何构建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文件,然后下载密钥文件,此文件只能下载一次,请妥善保存,格式样例:
#密钥KEY格式样例 -----BEGIN PRIVATE KEY----- BASE64编码后的密钥 -----END PRIVATE KEY-----#秘钥读取 publicbyte[] readKey() throws Exception { String temp = "密钥文件中间的编码字符串"; return Base64.decodeBase64(temp); }#构建client_secret关键代码: String client_id = "..."; // 被授权的APP ID Map header = new HashMap(); header.put("kid", "密钥id"); // 参考后台配置 Map claims = new HashMap(); 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 form = new HashMap(); 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是有时效性的,且使用一次即失效
扩展资料
  • JWT:https://www.cnblogs.com/softidea/p/7041532.html
  • ECDSA 椭圆曲线签名,JDK 1.7 的第四个版本提供了对ECDSA的支持:https://blog.csdn.net/qq_35612816/article/details/78904225

    推荐阅读