Sign in with Apple - IOS應用服務端的處理

本週新做一個需求,爲IOS APP接入蘋果第三方登錄。查看官方文檔,發現其在IOS端操作描述是非常細緻的,但在用戶服務端的講解實在是不知所云,讓人頭大。只能藉助廣大網友的智慧。本文並非完全原創,因爲無論概念解讀,還是操作方式,都是從各個文章處抄來的。本文最大的作用,在於針對自己和團隊內部的開發記錄,防止多次踩坑。

Sign in with Apple,對IOS和其它平臺的處理方式是有很大差別的。本文只針對IOS平臺,其它平臺可以參考這篇文章,說得非常詳細

說明

本文主要以官方文檔爲主線,輔以自己的理解。依賴於Sign in with Apple REST API頁面,其分爲兩部分

  • 用戶授權和驗證 - Authentication and Verification of Users

  • 獲取公鑰和生成&驗證token的API介紹

IOS上的蘋果登錄與一般的第三方登錄最大的區別,在於IOS在客戶端已經獲取了必要的用戶信息,以加密的形式發送給服務端,服務端需要做的驗證並應用即可;而一般的第三方登錄流程是需要在服務端請求用戶信息的。秉持這一基礎認識很重要,否則會雲裏霧裏。

客戶端

該部分對應 Authenticating Users with Sign in with Apple

在這裏插入圖片描述
客戶端授權流程如上主要有如下幾步

  1. 調用API發起授權請求
  2. 設備彈出授權框,請求用戶授權
  3. 用戶授權成功,API調用Apple ID 服務,請求用戶信息
  4. 請求成功,Apple ID服務以返回三個字段:identity token、authorization code、user identifier

identity token

identity token是一個JWT,使用解析工具解開後如下,包含了基本的用戶信息。

{
     kid: "86D88Kf",
     alg: "RS256"
}.
{
     iss: "https://appleid.apple.com",
     aud: "com.mampod.enlighten",
     exp: 1585110701,
     iat: 1585110101,
     sub: "001230.15f855de99ef4b788a18d18b7b45b053.0400",
     nonce: "123",
     c_hash: "lGYaArOB6z6IFuCOx2Z64A",
     email: "[email protected]",
     email_verified: "true",
     is_private_email: "true",
     auth_time: 1585110101,
     nonce_supported: true
}.
[signature]

每個字段解讀如下

頭部:

kid: key id,在token驗證時用於選取公鑰的ID

alg: algorithm,算法

載荷部分:

  • iss: isser的縮寫,即JWT發佈人
  • aud: audience的縮寫,客戶,對應APP的開發者。對應Apple開發者賬戶中的client_id
  • exp: expire的縮寫,即過期時間
  • iat: issue at的縮寫,即該token的發佈時間
  • sub: subject,即加密對象,這是關鍵,它是用戶的唯一標識符,很重要
  • nonce: 即隨機字符串。用於綁定客戶端會話和token的字符串值,用於防止重複攻擊。
  • nonce_supported: 指示對待nonce的方式
    • true: 如果授權請求時有給nonce,但返回的token不含nonce,則說明此次請求失敗
    • false: 不支持nonce,忽略nonce
  • c_hash: authorizationCode的hash值,用於驗證authorizationCode
  • email: 用戶郵箱
  • email_verified: 郵箱是否經過驗證,總是爲true
  • is_private_email: 是否是加密郵箱,即上面說的[email protected]
  • auth_time: 請求授權的時間

authorization code

authorization code用於和Apple ID服務交互,這裏暫時用不到,忽略。

user identifier

JWT中的sub字段,對應了用戶唯一標識符,即identifier,它具有如下特性

  • 唯一且穩定
  • 同一個蘋果開發賬戶的所有APP對應的同一個用戶的identifier是唯一的
  • 不同的開發賬戶的APP對應一個用戶的不同identifier
  • 對於用戶從APP註銷,再登錄,identifier是不會變的
  • 可以用於唯一標示用戶,即應該用它而不是郵箱嵌入我們的業務數據庫

Private email

授權時,用戶可以選擇隱藏真是郵箱,於是我們會獲取到一個[email protected]格式的用戶郵箱,該郵箱具有一定限制

  • 郵箱具有全局唯一性
  • 發往該郵箱的信息將會被轉發到真實的用戶郵箱
  • 對於同一個開發者的所有APP,一個用戶對應一個郵箱;對於不同開發這的不同APP,一個用戶對應多個郵箱
  • 該郵箱一旦生成,會一直生效,無論用戶是否有登錄你的APP,或已經刪除APP
  • 要想向該郵箱發送信息,需要在Apple註冊發送郵箱的郵箱域名,否則不會發送成功。

客戶端最後一步

爲了將用戶與我方APP服務端用戶系統綁定,需要將上面獲取的的identity token、authorization code、user identifier等信息發送APP服務端。

服務端

這部分對應 Verifying a User,也是最令人頭大的一部分。相同地,他也提供了一個流程圖,描述如何使用authorization code換取用戶信息和refresh token。對IOS登錄的後臺驗證毫無幫助,相反會起到混淆視聽的作用。請直接忽略。

對於IOS登錄,由於IOS客戶端已經通過客戶端API獲取了必要的用戶信息:唯一標識符、name、email等,我們已經沒有必要再次獲取這些信息,只需要驗證他們的真實性即可。

而對於其它平臺的登錄,如果你覺得看不懂這裏的文檔,強烈建議你按照這篇文章的方式操作——Sign in with Apple Tutorial, Part 4: Web and Other Platforms

驗證identity token

identity token的簽名是Apple ID服務使用私鑰加密的,需要從這裏獲取公鑰解密驗證。取得的公鑰以JWKS的形式呈現,如下。

{
    "keys": [
    {
        "kty": "RSA",
        "kid": "86D88Kf",
        "use": "sig",
        "alg": "RS256",
        "n": "iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ",
        "e": "AQAB"
    },
    {
        "kty": "RSA",
        "kid": "eXaunmL",
        "use": "sig",
        "alg": "RS256",
        "n": "4dGQ7bQK8LgILOdLsYzfZjkEAoQeVC_aqyc8GC6RX7dq_KvRAQAWPvkam8VQv4GK5T4ogklEKEvj5ISBamdDNq1n52TpxQwI2EqxSk7I9fKPKhRt4F8-2yETlYvye-2s6NeWJim0KBtOVrk0gWvEDgd6WOqJl_yt5WBISvILNyVg1qAAM8JeX6dRPosahRVDjA52G2X-Tip84wqwyRpUlq2ybzcLh3zyhCitBOebiRWDQfG26EH9lTlJhll-p_Dg8vAXxJLIJ4SNLcqgFeZe4OfHLgdzMvxXZJnPp_VgmkcpUdRotazKZumj6dBPcXI_XID4Z4Z3OM1KrZPJNdUhxw",
        "e": "AQAB"
    }
    ]
}

公鑰不止一個,需要取kid字段與JWT頭部的kid字段匹配的那個。

這裏我們使用了使用人數較多的庫java-jwtjwks-rsa-java

val jwt = JWT.decode(/*客戶端上傳的JWT*/)
val jwk = new UrlJwkProvider("https://appleid.apple.com/auth/keys").get(jwt.getKeyId)
val algorithm = Algorithm.RSA256(jwk.getPublicKey.asInstanceOf[RSAPublicKey], null)
Try {
    algorithm.verify()
} match {
    case Success(_) => // 驗證通過
    case Failure(e) => // 驗證不通過
}

驗證其它內容

根據官方手冊,總計需要驗證如下內容

  • 使用公鑰驗證JWT簽名,上一步已驗證
  • 驗證nonce,可選,這裏不驗證
  • 驗證iss,必須爲apple簽發,即必須包含https://appleid.apple.com字符串
  • 驗證aud,必須爲開發者賬戶的client_id
  • 驗證exp,即過期時間

使用現成庫的好處之一是可以直接獲取JWT的標準字段,如下

Try {
    assert(jwt.getAudience.get(0) == clientId, "aud incorrect")
    assert(jwt.getIssuer.contains("https://appleid.apple.com"), "iss must contains https://appleid.apple.com")
    assert(jwt.getSubject == request.identifier, "identifier invalid")
    assert(jwt.getExpiresAt.getTime > System.currentTimeMillis(), "Identity token expired")
} match {
    case Success(_) =>  // 驗證通過
    case Failure(e) => // 驗證不通過
}

優化 - 緩存Algorithm實例

上述驗證步驟中,在獲取公鑰和構建Algorithm實例時耗費較長時間——超過1秒

val jwk = new UrlJwkProvider("https://appleid.apple.com/auth/keys").get(jwt.getKeyId)
val algorithm = Algorithm.RSA256(jwk.getPublicKey.asInstanceOf[RSAPublicKey], null)

爲了加快響應速度,可以緩存Algorithm實例,但由於apple提供的公鑰可能變化,因此需要使用一定的策略兼顧效率和正確性。我們使用如下策略。
在這裏插入圖片描述
實際操作如下

def verifySignature(force: Boolean = false) = {
    if (force) jwkCache.remove(jwt.getKeyId)
    jwkCache.get(jwt.getKeyId, () => {
        val jwk = new UrlJwkProvider("https://appleid.apple.com/auth/keys").get(jwt.getKeyId)
        Algorithm.RSA256(jwk.getPublicKey.asInstanceOf[RSAPublicKey], null)
    }).map(algorithm => Try {
        algorithm.verify(jwt)
    } match {
        case Success(_) => Unit
        case Failure(e) => Future.failed(e)
    })
}

verifySignature()
.recoverWith[Any] { case _: Throwable => verifySignature(force = true) }

總結

相對於傳統第三方登錄,IOS的登錄流程略有不同

一般流程

客戶端

  • 用戶被導入登錄服務提供商,如微信、支付寶等
  • 用戶掃碼或輸入賬號密碼授權
  • 服務提供商將用戶重定向回客戶端,並附帶token
  • 客戶端憑藉該token進行登錄

服務端

  • 應用服務端憑藉該token向服務提供商驗證登錄真實性並獲取用戶信息
  • 使用獲取到的用戶信息綁定自建的用戶系統

IOS登錄流程

客戶端

  • IOS客戶端上彈出授權框
  • 用戶刷臉授權
  • Apple同樣將用戶重定向迴應用,附帶token,但同時使用客戶端API想Apple Server獲取用戶信息,以JWT形式提供
  • 客戶端將JWT發送給應用服務端

服務端

  • 驗證JWT
  • 使用JWT附帶的用戶信息綁定自建的用戶系統

可以看到,其最主要的不同還是在於IOS客戶端已經獲取了用戶信息,在服務端僅需要驗證JWT即可,與一般流程相比,IOS登錄的應用後端少了一步請求用戶信息的步驟。

沒涉及的部分

文章到這裏也只介紹了在IOS客戶端接入Sign in with apple的後端操作步驟,並沒有設計到IOS之外平臺的處理方式,對此,可以參考這篇文章,它有很好的講解。

更多

關於更多授權相關知識,這裏列舉了一些學習資源

參考文檔

  1. Authenticating Users with Sign in with Apple

  2. Verifying a User

  3. Fetch Apple’s public key for verifying token signature

  4. iOS 13 蘋果賬號登陸與後臺驗證相關

  5. Sign in with Apple Tutorial, Part 3: Backend – Token verification

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章