使用 Supabase 增强 Sign in with Apple 认证服务

了解如何使用 Supabase 增强“通过 Apple 登陆”服务,包括但不限于持久化用户数据、自动验证用户身份和维持会话。

使用 Supabase 增强 Sign in with Apple 认证服务

使用 Sign in with Apple 的难点

在「使用 Sign in with Apple 进行身份认证」这篇文章中讲过,使用 Sign in with Apple 服务,开发者需要处理的工作不仅仅是身份验证,一般包含以下几个步骤:

  1. 使用 Apple 提供的 Sign in with Apple 完成身份验证
  2. 开发者需要在服务端验证 identityToken 有效性(通过使用 Apple 提供的 API)
  3. 开发者需要实现后续会话的维护(例如令牌刷新)
  4. 开发者需要自行存储用户的 Emial 和 Username(因为只提供一次)

因此,开发者仍然需要至少一台服务器,来处理以上工作。

Supabase 的核心就是简化后续几个步骤的工作,并且无需格外的服务器,直接从 Apple → Supabase。

使用 Supabase 简化工作量

理解 Supabase 与 Sign in with Apple 服务集成原理

整个 Sign in with Apple 与 Supabase 集成的核心流程,分为两个关键步骤:

  1. Apple 负责用户身份验证
  • 仍然使用 Sign in with Apple 提供的 SignInWithAppleButton 组件,以及相关的认证服务和流程 —— 因为这是一套成熟的认证系统。
  • 完成认证之后,将获取到的 identity token(身份令牌)提供给 Supabase 提供的 signInWithIdToken() 方法即可。
  1. Supabase 负责后续身份验证和令牌刷新
  • 自动验证令牌 - Supabase 的 signInWithIdToken() 方法会自动负责验证 identityToken 的有效性。
  • 自动提取用户信息 - 自动从 identityToken 中解析 userIdentifier、email、username 等。
  • 自动存储用户信息。
  • 自动维护会话、刷新令牌。

Apple 提供的 identity token 仅用于 Supabase 的初始化验证,验证通过之后,后续的身份认证令牌,都是用 Supabase 自己的令牌。

开始使用 Supabase

安装 Supabase-swift 库

Supabase-swift 在 24 年正式推出了 v2.0 版本,现在可以更加方便、简单的将 Supabase 认证集成到 SwiftUI 应用中。

在 SPM 中安装依赖:

https://github.com/supabase/supabase-swift.git

获取 Project URL 和 Project Key

需要通过传递Project URLProject Key,来初始化SupabaseClient 实例。

可以在Project SettingsDataAPI 中找到:

在 Sapubase 网页端启用 Apple 登陆

在 Project -> Authentication -> Sign In/Up 中,启用 Apple 选项:

这里的 Client ID 就是 App 的 Identifier,可以在这里找到:https://developer.apple.com/account/resources/identifiers/list

根据官方文档显示,如果只构建 iOS 端应用,无需填写 Secret Key

初始化 Client 实例

基本的实例化方式,参考自:https://supabase.com/docs/reference/swift/auth-api

import Supabase

let client = SupabaseClient(
    supabaseURL: URL(string: "https://xyzcompany.supabase.co")!,
    supabaseKey: "public-anon-key")

SupabaseClient 提供了不同的参数用于实现自定义的初始化。详情可以查询:https://supabase.com/docs/reference/swift/initializing

进行认证

进行认证实际上非常简单,我们只需要正常调用 SignInWithAppleButton 组件进行认证,并在回调中将 identityToken 传递给 Supabase 的 signInWithIdToken() 方法即可。

signInWithIdToken 会自动验证 identityToken,并将用户信息存储到 user 表中(这个表在项目初始化时即默认创建)。

官方示例代码:

import SwiftUI
import AuthenticationServices
import Supabase

struct SignInView: View {
    let client = SupabaseClient(supabaseURL: URL(string: "your url")!, supabaseKey: "your anon key")

    var body: some View {
      SignInWithAppleButton { request in
        request.requestedScopes = [.email, .fullName]
      } onCompletion: { result in
        Task {
          do {
            guard let credential = try result.get().credential as? ASAuthorizationAppleIDCredential
            else {
              return
            }

            guard let idToken = credential.identityToken
              .flatMap({ String(data: $0, encoding: .utf8) })
            else {
              return
            }
              try await client.auth.signInWithIdToken(
              credentials: .init(
                provider: .apple,
                idToken: idToken
              )
            )
          } catch {
            dump(error)
          }
        }
      }
      .fixedSize()
    }
}

上述代码直接在 View 组件中创建 SupabaseClient 实例,并调用 signInWithIdToken 方法。在实际项目中,更推荐创建一个 SupabaseManager 或者 AuthService 类来统一处理,我们会在下面讨论这个主题。

重启 App 后是否需要再次登陆?

以上代码并没有显式的将用户的 Session 持久化。因此,如果用户关闭了 App,如何保证重新打开 App 时,无需再次进行认证?

Supabase Swift SDK 默认会将会话信息(包括访问令牌和刷新令牌)自动存储在 iOS 的 Keychain 中,这些数据即使在 App 完全关闭后也会保留。

当 App 重新启动时,可以通过调用supabase.auth.session()方法恢复会话。该方法尝试获取现有会话并恢复用户状态,如果找到有效会话,就不需要重新登录。

获取并存储 Apple ID 的用户名

Supabase 会默认创建一个 User 表。

在使用 signInWithIdToken() 方法完成 Sign in with Apple 认证之后,在 User 表中可以正确获取并存储 Email,但是无法自动获取并存储用户名。

相同的问题,其他人也有遇到:

Full Name not stored in Database when Using Apple Sign-In in React Native · Issue #899 · supabase/supabase-js
Description When using Apple’s native sign-in with React Native and the signInWithIdToken method, the raw_user_meta_data in the auth.users table doesn’t include the newly created user’s full name.…
Missing User Info (Email & Display Name) in Supabase Auth with Apple Sign-In via signInWithIdToken · Issue #1032 · supabase/auth-js
Bug report Apple Authentication via supabase.auth.signInWithIdToken creates a record without the Email or Display name fields in Supabase Auth (despite the user providing these during the Auth proc…

解决方法是:

手动使用 updateUser() 方法来补充这些信息
——updateUser()是 JS SDK 提供的方法。

在 Swift SDK 中,

  • 应当使用 update() 方法,而不是 updateUser()
  • update() 方法并未提供专门的 userName 字段,只能通过 data 自定义字段
try? await supabase.auth.update(
    user: UserAttributes(data: ["full_name": .string(fullName)])
)

更详细的可以参考:https://supabase.com/docs/reference/javascript/v1/auth-update

自定义的 data 数据,存储在 user 表的 raw_user_meta_data 列中:

认证后数据处理

如何恢复会话?

Supabase 提供了两个用于刷新会话的方法:

  • supabase.auth.session

直接调用supabase.auth.session()方法,会自动返回会话。如果有必要,会自动刷新它。

try await supabase.auth.session
  • supabase.auth.refreshSession()

无论当前会话是否已过期,此方法都会刷新会话。

let session = try await supabase.auth.refreshSession()
let session = try await supabase.auth.refreshSession(refreshToken: "custom-refresh-token")

如何注销登陆?

直接调用 supabase.auth 提供的signOut()方法即可:

Swift: Link an identity to a user | Supabase Docs
Supabase API reference for Swift: Link an identity to a user
try await supabase.auth.signOut()

如何删除用户账号?

删除账号功能,必须调用 supabase.auth.admin 下的方法。并且,必须使用 serviceRoleKey 创建的 SupabaseClient 实例,

import Supabase

let supabase = SupabaseClient(
  supabaseURL: supabaseURL,
  supabaseKey: serviceRoleKey
)

// Access auth admin api
let adminAuthClient = supabase.auth.admin

普通用户无权调用删除方法,否则会遇到服务器报错:

根据 Supabase 文档的建议,不应当在客户端直接调用 deleteUser() 方法,应当创建一个中间服务器,并在该服务器上保存 serviceRoleKey 以及调用 deleteUser 方法。

deleteUser()方法需要用户的 ID,该 ID 映射到auth.users.id列。

try await supabase.auth.admin.deleteUser(
  id: "715ed5db-f090-4b8c-a067-640ecee36aa0"
)
Swift: Delete a user | Supabase Docs
Supabase API reference for Swift: Delete a user

如何查询用户信息?

如何增删改查用户信息?

官方说明:

Swift: Insert data | Supabase Docs
Supabase API reference for Swift: Insert data

使用 Supabase 进行权限管理

Supabase 提供的 supabase.auth.userIdentities() 方法,可以返回用户的身份信息:

Swift: Link an identity to a user | Supabase Docs
Supabase API reference for Swift: Link an identity to a user

Supabase 在中国的支持情况

这个部分需要添加后,经过一段时间的实际测试才知道。