全网最佳,第三方登录系列——苹果登录
梳理一下苹果登录的逻辑, 这一篇是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,取出与客户端传入的进行比对即可。
全网最佳,第三方登录系列——苹果登录相关推荐
- uniapp—微信登录,苹果登录
目录 微信登录代码实现 在微信小程序和app登录,获取的unionid不一致 ios13+苹果登录 在苹果登录的开发过程中,遇到的问题及解决方案. 需求说明: 我的项目需求是已有微信小程序项目,新增a ...
- Uniapp苹果登录sign in Apple
Uniapp苹果登录sign in Apple 前提:软件内,如果已经实现第三方登录,必须也有sign in Apple功能,否则审核会不通过 准备工作:uniapp自带sign in Apple必须 ...
- ios 登录 java 后台,IOS苹果登录sign in with apple后端校验
IOS苹果登录sign in with apple后端校验 最近新开发的app在IOS平台app store connent提审的时候,被拒了,原因是app上如果有接第三方登陆(比如微信,微博,fac ...
- vue 前端显示图片加token_手摸手,带你用vue撸后台 系列二(登录权限篇)
完整项目地址:vue-element-admin https://github.com/PanJiaChen/vue-element-admin 前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自 ...
- 【iOS】苹果登录Sign in with Apple
在iOS13中,如果苹果开发者提供任何其他第三方登录,就必须提供"苹果登录"选项.也就是说,如果软件要求"微信登录"或是"QQ登录"时,必须 ...
- IONIC4 苹果登录-Sign In With Apple Id
最近上架APP被苹果拒绝,理由是使用第三方登录需加上苹果登录,否则不给上架,所以在这分享一下ionic4的苹果登录 首先安装cordova插件,获取系统版本插件 ionic cordova plugi ...
- swift 苹果登录
苹果登录项目中继承第三方登录时,需增加上苹果登录即可上架苹果登录需要iOS系统 13以上支持详细的内容阅读苹果官方的网址url:https://developer.apple.com/document ...
- iOS13苹果登录的后台验证token(JAVA)
最近随着iOS的更新,苹果要求含有第三方登录的app必须实现苹果登录功能,在查询相关资料后整合进自己的项目中,再次记录下,也供大家借鉴. 以下是大致流程,挺简单的: 首先引入解析jwt的包: < ...
- apicloud——微信第三方登录、apple登录
apicloud中微信第三方登录的问题 混合开发移动应用很火啊现在,多数公司都要求是必备技能,或者加分技能,所以学习来了 如下是作为前端工作者应该了解的内容,更加详细的前往官方示例 *** 结合api ...
最新文章
- Android界面性能调优手册
- python os模块system_Python如何使用OS模块调用cmd
- 深度学习学习7步骤_如何通过4个简单步骤为深度学习标记音频
- C# 用IrisSkin4.dll美化你的WinForm
- ai可以滚轮缩放吗_AI侵入艺术天堂!艺术也可以“量产”了吗?
- Maven学习总结(9)——使用Nexus搭建Maven私服
- android 获取wifi的ip地址吗,Android获取有线和无线(wifi)的IP地址
- phpstrom 安装
- web页面的回流,认识与避免
- 立于山巅!他,凭什么抗住万亿级流量冲击!
- 推荐系统(3)——个性化推荐系统架构
- RHCE考试第一天之学习安排计划
- 伴随矩阵例题_考研数学一真题详解:伴随矩阵有关问题
- 批处理为win7桌面添加计算机图标,WIN7桌面显示IE图标批处理
- 使用selenium爬取百合网
- xmind linux,xmind linux免费版下载
- android恢复 模式,Android Doze模式使用命令启用和恢复
- MySQL-Parser
- 不完整拼音模糊匹配算法
- nginx降权及匹配php
热门文章
- python游戏csgo开挂_V社:用深度学习检测CSGO中的开挂行为
- AtCoder Beginner Contest 171 D - Replacing
- testmeshpro合批_Unity合批原理及失败的原因
- Spamhaus -影响最大的RBL(实时黑名单列表)服务提供商,非营利性组织
- matlab拟合多自变量函数,多个自变量的函数拟合问题
- 计算机科学第三讲——布尔逻辑和逻辑门
- python 语料_用python将语料转化为可计算的形式
- GCD Expectation ZOJ - 3868 (容斥)
- yii mysql gii_Yii 框架使用Gii生成代码操作示例
- Linux基础命令之:top命令详解以及VIRT,RES,SHR,DATA