梳理一下苹果登录的逻辑, 这一篇是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>? = null
class 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"] = kid
val second = System.currentTimeMillis() / 1000
//将private key字符串转换成PrivateKey 对象
var privateKey: PrivateKey? = null
try {
val pkcs8EncodedKeySpec = PKCS8EncodedKeySpec(
readPrivateKey(privateKeyStr))
val keyFactory: KeyFactory = KeyFactory.getInstance("EC")
privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec)
} catch (e: Exception) {
e.printStackTrace()
}
// 此处只需PrimaryKey
val algorithm: Algorithm = Algorithm.ECDSA256(null,
privateKey as ECPrivateKey?)
// 生成JWT格式的client_secret
return 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.kid
val n = publicKey[kid]!!.n
val 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,取出与客户端传入的进行比对即可。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)