梳理一下苹果登录的逻辑, 这一篇是Kotlin版本的,Kotlin的代码语义比较明确,和Java兼容, 同样的方法都可以在Java中找到。
之后我会整理一篇Java版本和Go版本的

apple登录有两种校验方式,分别是id_token 和 code校验。

方式一: id_token校验

方式二 code校验:

第一种方式是由客户端直接发起登录拿到id_token和userInfo,服务端只进行一个简单的token校验,不与apple服务器交互。

第二种方式是由客户端拿到一个code,服务端拿到app相关信息和code对apple服务器发起校验。之后解析拿到用户信息。

网上也有很多是第二种的简化,就是在第二种的基础上去掉了第一种方式中的id_token校验,但这样是不对的,如果以客户端传过来的userInfo为准,那么用户信息是可以被顶替和造假的。因为第二种方式涵盖了第一种方式的校验,所以本文对第二种方式的登录进行实现。

如果想要第一种方式实现看这里:

一、前置条件 第一种方式不需要申请苹果的privateKey和KeyId,其他全部完成。

二、开始流程,前3步不用做,直接拿第四步的校验方法去用。

一、 前置条件

前置条件:申请apple app、 获取publicKey

1. 申请苹果app会获得四个参数: clientId、privateKey、teamId、keyId

2. 获取publicKey ⬇️

private val appleAuthUrl = "https://appleid.apple.com/auth/keys"private var publicKey: MutableMap<String, AppleKeys.Keys> = mutableMapOf()@PostConstruct
fun init() {val appleKeys: ResponseEntity<AppleKeys> = restTemplate.getForEntity(appleAuthUrl, AppleKeys::class.java)if (appleKeys.body!!.keys!!.isNotEmpty()) {// 将appleKey缓存起来publicKey.putAll(appleKeys.body!!.keys!!.map {it.kid to it}.toMap())}
}

获取到的每一个Key结构是这样的,一个Keys对象的数组⬇️

class AppleKeys {val keys: List<Keys>? = nullclass Keys(val kty: String,val kid: String,val use: String,val alg: String,val n: String,val e: String)
}

准备好publicKey是为了接下来对token的解密。 使用map处理,

一个kid对应一个Keys。由kid找到对应的N和E,再生成对应的PublicKey。

生成PublicKey的方法:

fun createPublicKey(stringN: String?, stringE: String?): PublicKey? {try {val modulus = BigInteger(1, Base64.decodeBase64(stringN))val publicExponent = BigInteger(1, Base64.decodeBase64(stringE))val spec = RSAPublicKeySpec(modulus, publicExponent)val kf = KeyFactory.getInstance("RSA")return kf.generatePublic(spec)} catch (e: Exception) {e.printStackTrace()}return null
}

完整的步骤:之后从token中获取header,从header信息中找到kid,从Map中找到kid对应的Keys对象,取出对应的n和e,传入对应的n和e的值即可。


二、 开始流程

1. 客户端调起苹果登录,获取到code、userId。(客户端略)

2. 服务端生成client_secret

首先获取到申请苹果appId后的那些参数,我将这些参数存储在了配表中,使用自用的appId进行获取

val clientId = thirdPartyConfig.getAppleClientId(appId)
val privateKey = thirdPartyConfig.getApplePrivateKey(appId)
val teamId = thirdPartyConfig.getAppleTeamId(appId)
val keyId = thirdPartyConfig.getAppleKId(appId)

然后调用generateClientSecret() 传入这四个值,获取到client_secret (这个是有有效期限的,最长设置半年,可以缓存使用,也可随用随生成)

class GenerateAppleClientSecret {companion object {val APPLE_JWT_AUD_URL = "https://appleid.apple.com"/*** 生成clientSecret** @param kid* @param teamId* @param clientId* @param privateKeyStr* @return*/fun generateClientSecret(kid: String, teamId: String?,clientId: String?, privateKeyStr: String): String {val header: MutableMap<String, Any> = HashMap()header["kid"] = kidval second = System.currentTimeMillis() / 1000//将private key字符串转换成PrivateKey 对象var privateKey: PrivateKey? = nulltry {val pkcs8EncodedKeySpec = PKCS8EncodedKeySpec(readPrivateKey(privateKeyStr))val keyFactory: KeyFactory = KeyFactory.getInstance("EC")privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec)} catch (e: Exception) {e.printStackTrace()}// 此处只需PrimaryKeyval algorithm: Algorithm = Algorithm.ECDSA256(null,privateKey as ECPrivateKey?)// 生成JWT格式的client_secretreturn JWT.create().withHeader(header).withClaim("iss", teamId).withClaim("iat", second).withClaim("exp", 86400 * 180 + second).withClaim("aud", APPLE_JWT_AUD_URL).withClaim("sub", clientId).sign(algorithm)}private fun readPrivateKey(primaryKey: String): ByteArray? {val pkcs8Lines = StringBuilder()val rdr = BufferedReader(StringReader(primaryKey))var line: String? = ""try {while (rdr.readLine().also { line = it } != null) {pkcs8Lines.append(line)}} catch (e: IOException) {e.printStackTrace()}// 需要注意删除 "BEGIN" and "END" 行, 以及空格var pkcs8Pem = pkcs8Lines.toString()pkcs8Pem = pkcs8Pem.replace("-----BEGIN PRIVATE KEY-----", "")pkcs8Pem = pkcs8Pem.replace("-----END PRIVATE KEY-----", "")pkcs8Pem = pkcs8Pem.replace("\\s+".toRegex(), "")// Base64 转码return Base64.decodeBase64(pkcs8Pem)}}
}

3. 服务端拿到客户端传过来的code,拿到申请苹果appId时获取的client_id、上一步生成的client_secret,发起调用。

// 请求头设置,x-www-form-urlencoded格式的数据
val headers = HttpHeaders()
headers.add("Content-Type", "application/x-www-form-urlencoded")// 提交参数设置
val postParameters: MultiValueMap<String, Any> = LinkedMultiValueMap()
postParameters.add("client_id", client_id)
postParameters.add("client_secret", client_secret)
postParameters.add("code", code)
postParameters.add("grant_type", "authorization_code")val r: HttpEntity<MultiValueMap<String, Any>> = HttpEntity(postParameters, headers)

然后苹果会返回一个这样的结构体

data class AppleTokens(val access_token: String,val token_type: String,val expires_in: Int,val refresh_token: String,val id_token: String
)

4. 接下来需要获取到这个结构体中的id_token进行校验(目的是匹配解析出的userId和客户端传过来的是否一致)

/**** key: 从苹果那里拿来的公钥, 直接注入了* jwt: id_token* audience: 包名,从apple申请的apple_client_id* subject: apple用户唯一ID(客户端传过来的)*/
fun verify(jwt: String, audience: String, subject: String): Jws<Claims> {// 拿到header部分val headerJ = String(Base64.decodeBase64(jwt.split(".")[0]))val gson = Gson()val header = gson.fromJson(headerJ, Header::class.java)val kid = header.kidval n = publicKey[kid]!!.nval e = publicKey[kid]!!.e// 前置部分创建好的方法val pubKey = createPublicKey(n, e)val jwtParser = Jwts.parser().setSigningKey(pubKey)jwtParser.requireIssuer(appleIssuerUrl)jwtParser.requireAudience(audience)jwtParser.requireSubject(subject)try {return jwtParser.parseClaimsJws(jwt)} catch (e: IncorrectClaimException) {throw badRequestError(999, "error apple userId")} catch (e: Exception) {e.printStackTrace()throw notFoundError(999, "未知异常!")}
}

上面Header的结构是这样的

class Header(val kid: String,val alg: String
)

这样可以从token中解析拿到Claims对象。其中SUBJECT的值存储的就是apple的userId,取出与客户端传入的进行比对即可。

全网最佳,第三方登录系列——苹果登录相关推荐

  1. uniapp—微信登录,苹果登录

    目录 微信登录代码实现 在微信小程序和app登录,获取的unionid不一致 ios13+苹果登录 在苹果登录的开发过程中,遇到的问题及解决方案. 需求说明: 我的项目需求是已有微信小程序项目,新增a ...

  2. Uniapp苹果登录sign in Apple

    Uniapp苹果登录sign in Apple 前提:软件内,如果已经实现第三方登录,必须也有sign in Apple功能,否则审核会不通过 准备工作:uniapp自带sign in Apple必须 ...

  3. ios 登录 java 后台,IOS苹果登录sign in with apple后端校验

    IOS苹果登录sign in with apple后端校验 最近新开发的app在IOS平台app store connent提审的时候,被拒了,原因是app上如果有接第三方登陆(比如微信,微博,fac ...

  4. vue 前端显示图片加token_手摸手,带你用vue撸后台 系列二(登录权限篇)

    完整项目地址:vue-element-admin https://github.com/PanJiaChen/vue-element-admin 前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自 ...

  5. 【iOS】苹果登录Sign in with Apple

    在iOS13中,如果苹果开发者提供任何其他第三方登录,就必须提供"苹果登录"选项.也就是说,如果软件要求"微信登录"或是"QQ登录"时,必须 ...

  6. IONIC4 苹果登录-Sign In With Apple Id

    最近上架APP被苹果拒绝,理由是使用第三方登录需加上苹果登录,否则不给上架,所以在这分享一下ionic4的苹果登录 首先安装cordova插件,获取系统版本插件 ionic cordova plugi ...

  7. swift 苹果登录

    苹果登录项目中继承第三方登录时,需增加上苹果登录即可上架苹果登录需要iOS系统 13以上支持详细的内容阅读苹果官方的网址url:https://developer.apple.com/document ...

  8. iOS13苹果登录的后台验证token(JAVA)

    最近随着iOS的更新,苹果要求含有第三方登录的app必须实现苹果登录功能,在查询相关资料后整合进自己的项目中,再次记录下,也供大家借鉴. 以下是大致流程,挺简单的: 首先引入解析jwt的包: < ...

  9. apicloud——微信第三方登录、apple登录

    apicloud中微信第三方登录的问题 混合开发移动应用很火啊现在,多数公司都要求是必备技能,或者加分技能,所以学习来了 如下是作为前端工作者应该了解的内容,更加详细的前往官方示例 *** 结合api ...

最新文章

  1. Android界面性能调优手册
  2. python os模块system_Python如何使用OS模块调用cmd
  3. 深度学习学习7步骤_如何通过4个简单步骤为深度学习标记音频
  4. C# 用IrisSkin4.dll美化你的WinForm
  5. ai可以滚轮缩放吗_AI侵入艺术天堂!艺术也可以“量产”了吗?
  6. Maven学习总结(9)——使用Nexus搭建Maven私服
  7. android 获取wifi的ip地址吗,Android获取有线和无线(wifi)的IP地址
  8. phpstrom 安装
  9. web页面的回流,认识与避免
  10. 立于山巅!他,凭什么抗住万亿级流量冲击!
  11. 推荐系统(3)——个性化推荐系统架构
  12. RHCE考试第一天之学习安排计划
  13. 伴随矩阵例题_考研数学一真题详解:伴随矩阵有关问题
  14. 批处理为win7桌面添加计算机图标,WIN7桌面显示IE图标批处理
  15. 使用selenium爬取百合网
  16. xmind linux,xmind linux免费版下载
  17. android恢复 模式,Android Doze模式使用命令启用和恢复
  18. MySQL-Parser
  19. 不完整拼音模糊匹配算法
  20. nginx降权及匹配php

热门文章

  1. python游戏csgo开挂_V社:用深度学习检测CSGO中的开挂行为
  2. AtCoder Beginner Contest 171 D - Replacing
  3. testmeshpro合批_Unity合批原理及失败的原因
  4. Spamhaus -影响最大的RBL(实时黑名单列表)服务提供商,非营利性组织
  5. matlab拟合多自变量函数,多个自变量的函数拟合问题
  6. 计算机科学第三讲——布尔逻辑和逻辑门
  7. python 语料_用python将语料转化为可计算的形式
  8. GCD Expectation ZOJ - 3868 (容斥)
  9. yii mysql gii_Yii 框架使用Gii生成代码操作示例
  10. Linux基础命令之:top命令详解以及VIRT,RES,SHR,DATA