【苹果登录】使用Apple账号登录你的APP(服务端原理介绍+Java实现)

您所在的位置:网站首页 苹果账号授权 【苹果登录】使用Apple账号登录你的APP(服务端原理介绍+Java实现)

【苹果登录】使用Apple账号登录你的APP(服务端原理介绍+Java实现)

2024-07-08 06:20| 来源: 网络整理| 查看: 265

苹果登录的服务端验证原理和实现 前言铺垫知识(会的可以直接跳过)APPLE的登录原理实现(Java)

前言

为什么要用苹果登录?在国内大多数人应该都是使用的微信登录或者手机号一键登录多一些。之所以要开发苹果登录,可能大多是因为苹果的霸王条款:要求只要在苹果商店上架的应用,凡是接入了其他第三方登录,必须接入苹果登录。

虽然苹果的霸王条款很让人恼火,但是苹果登录本身还是值得我们学习和思考的。下面介绍一下苹果登录的原理。

铺垫知识(会的可以直接跳过)

在学习苹果登录之前,首先你需要了解以下几个知识点:

什么是非对称加密,什么是公钥和私钥?什么是JWT,这东西是用来干嘛的?什么是第三方登录?

下面依次给大家简要介绍一下上面三个知识点:

非对称加密 我们知道对称加密在加解密时使用的秘钥内容是相同的,而非对称加密在加密和解密时使用不同的秘钥。我们把这两个不同的秘钥一个称为私钥,一个称为公钥。一般私钥由我们自己保留不对外公开,而公钥则可以对外公开下载。这样有什么好处呢?举个例子:我们发布一则公告,使用私钥进行加密,然后把公告放到互联网上。想要看公告的人直接下载我们的公钥,然后用公钥解密公告即可。这样别人就无法仿造我们的公告,因为只有我们提供的公钥才能解密,从而达到对公告信任的目的。

JWT是什么 JWT是什么?直接回答这个问题比较难以理解,我们就拿苹果的JWT举个例子:

{"kid": "W6WcOKB","alg": "RS256"}.{"iss":"https://appleid.apple.com","aud":"com.test","exp":1687776521,"iat":1687690121,"sub":"001001.0bb42737edf44688850c5de5e666d9e.1024","c_hash":"FXhhsDY1Dpabo8_GTID-Lw","auth_time":1687690121,"nonce_supported":true,"real_user_status":2}.xxxxxxxxxxxxxxxxxxxxxxxxxx

我们看到jwt就是一个字符串,由三段字符串组成。

第一段:{"kid": "W6WcOKB","alg": "RS256"}我们称为头部,标记了这个JWT通过RS256进行加密,(这里的kid是对应的公钥id,这个是苹果特有的)。第二段:{"iss":"https://appleid.apple.com","aud":"com.test","exp":1687776521,"iat":1687690121,"sub":"001001.0bb42737edf44688850c5de5e666d9e.1024","c_hash":"FXhhsDY1Dpabo8_GTID-Lw","auth_time":1687690121,"nonce_supported":true,"real_user_status":2}。 这里我们只说关键的几个参数:iss:发布人,固定是苹果的域名;aud:应用的包名;sub:当前用户的唯一ID,类似微信等第三方登录的OpenID;第三段:就是校验和类似的东西,没有详细的研究苹果是怎么实现的,但是我们理解技术不必太过纠结,知道他是个校验和的东西就行。比如我可以这么实现,首先将前面的两个部分拼接成一个字符串,然后求一个摘要,再用私钥进行加密后作为第三部分。需要校验时,先求前两部分摘要,然后用公钥解密第三方部分,看解密的结果和我们求出的摘要是否相等即可。 什么是第三方登录? 其实问为什么需要第三方登录更好。随着我们使用的应用越来越多,如果我们每个都创建一个账户和密码,简直就是噩梦。而我们有微信账号,如果可以直接用微信账号登录多好,一个账号走天下,只要记住微信的密码,能登录微信就可以了,再也不怕忘记密码了。至于通常如何实现第三方登录,可以去了解下OAuth2,这里就不再多说了,不是本文的重点。 APPLE的登录原理 首先,当你在应用内点击苹果登陆按钮时,苹果会生成一系列的参数给你,其中最重要的一个参数是:identityToken。这个参数的值就是一个JWT。这个JWT是通过苹果的私钥加密的。我们将这个identityToken传给我们的后台服务,后台服务从苹果提供的公钥下载地址下载公钥,然后使用公钥验证这个JWT是否合法。在合法的前提下,我们看一下iss是否是苹果(显然是),再看aud是不是你自己的包名(别人的应用也可以生成一个token,也可以通过校验,但是包名肯定和你的不同)。都校验通过了,说明这是一个合法JWT。然后我们就取sub作为苹果的OpenId,记录到我们后台的用户表里即可(如果没有用户则创建,如果有则绑定)。等到再次登录的时候,则用sub查询有没有对应的用户,如果有,则直接让其登录即可。 实现(Java)

引入JWT工具包

io.jsonwebtoken jjwt 0.9.1 com.auth0 jwks-rsa 0.22.0

实现代码(以下代码复制就可以直接使用):

校验代码(核心逻辑) public class AppleAuthHelper { private static final String PUBLIC_KEY_URL = "/auth/keys"; private static final String APPLE_DOMAIN = "https://appleid.apple.com"; private static final String aud = "com.test"; public static boolean isValid(AppleAuthRequest appleAuthRequest) { try { IdentityToken idToken = new IdentityToken(appleAuthRequest.getIdentityToken()); String userID = appleAuthRequest.getUserID(); PublicKey publicKey = AppPublicKey.get(APPLE_DOMAIN + PUBLIC_KEY_URL, idToken.getKid()); if (null == publicKey) { return false; } JwtParser jwtParser = Jwts.parser().setSigningKey(publicKey); jwtParser.requireIssuer(APPLE_DOMAIN); jwtParser.requireAudience(aud); jwtParser.requireSubject(userID); Jws claim = jwtParser.parseClaimsJws(idToken.getToken()); if (claim != null && claim.getBody().containsKey("auth_time")) { return true; } } catch (Exception e) { log.error("校验apple登录信息失败", e); } return false; } } 获取公钥代码 public class AppPublicKey { public static volatile Map cache = new HashMap(); public static PublicKey get(String url, String kid) { try { if (cache.containsKey(kid)) { Map publicKeyConfig = cache.get(kid); Jwk jwa = Jwk.fromValues(publicKeyConfig); return jwa.getPublicKey(); } Map publicKeyConfig = getPublicKeyConfig(url, kid); if (null == publicKeyConfig) { return null; } Jwk jwa = Jwk.fromValues(publicKeyConfig); return jwa.getPublicKey(); } catch (final Exception e) { log.error("apple get publicKey error", e); } return null; } private static synchronized Map getPublicKeyConfig(String url, String kid) { try { String str = HttpUtil.get(url); JsonNode jsonNode = JsonHelper.asTree(str); JsonNode keys = jsonNode.get("keys"); if (null == keys) { return null; } Iterator elements = keys.elements(); while (elements.hasNext()) { JsonNode node = elements.next(); String nodeKid = node.get("kid").asText(); if (kid.equals(nodeKid)) { Map item = JsonHelper.toMap(node); cache.put(nodeKid, item); } } return cache.get(kid); } catch (final Exception e) { log.error("apple get publicKey error", e); } return null; } } 辅助类(请求参数等) public class AppleAuthRequest { private String authorizationCode; private String identityToken; private String userID; private AppleUserInfo fullName; } public class AppleUserInfo { private String givenName; private String familyName; private String middleName; private String namePrefix; private String nameSuffix; private String nickname; private String phoneticRepresentation; } public class IdentityToken { //原始token private String token; //公钥id private String kid; //应用包名,应该为:com.test private String aud; //用户openId private String sub; @SneakyThrows public IdentityToken(String token) { this.token = token; String[] identityTokens = this.token.split("\\."); Map firstDate = JSONObject .parseObject(new String(Base64.decodeBase64(identityTokens[0]), "UTF-8")); Map secondData = JSONObject .parseObject(new String(Base64.decodeBase64(identityTokens[1]), "UTF-8")); this.kid = String.valueOf(firstDate.get("kid")); this.aud = String.valueOf(secondData.get("aud")); this.sub = String.valueOf(secondData.get("sub")); } } public class JsonHelper { private final static ObjectMapper objectMapper = new ObjectMapper(); public static Map toMap(JsonNode jsonNode){ return objectMapper.convertValue(jsonNode, new TypeReference(){}); } public static JsonNode asTree(String strJson) { try { return objectMapper.readTree(strJson); } catch (IOException e) { log.error("Deserialize fail", e); } return null; } }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3