全网最佳,第三方登录系列——苹果登录

全网最佳,第三方登录系列——苹果登录,第1张

梳理一下苹果登录的逻辑, 这一篇是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,取出与客户端传入的进行比对即可。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/web/992662.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存