身份认证

邮箱密码登录、OTP 验证、社交登录与会话管理。

身份认证基于 supabase-swiftAuth 模块,已端到端 Integrated(已集成)。它涵盖邮箱 密码、邮箱 OTP、密码重置、修改邮箱/密码、会话管理、账户注销,以及原生 Apple + Google 登录。

会话管理

SessionController 是应用中「谁已登录」的唯一可信来源。它是一个 @MainActor @Observable 对象,订阅认证状态流并对外发布当前用户:

  • AuthRepository.sessionStream 把 supabase-swift 的 authStateChanges 封装为 AsyncStream<Session?>
  • SessionController 消费它,设置 currentUser,并在首个事件后将 isLoading 置为 false
  • RootView 中的 Auth 闸门读取 currentUser,据此在认证栈与 主壳之间切换。

会话存放在钥匙串,而非 UserDefaults supabase-swift 默认的 KeychainLocalStorage 负责令牌持久化。切勿把认证令牌镜像到 AppPreferences / UserDefaults——钥匙串副本才是权威来源。

邮箱与密码流程

每个流程都是 AuthRepositoryProtocol 上的一个方法,返回 AppResult<Void>(见 数据层)。底层的 SupabaseAuthService 把每个流程映射到一次 supabase-swift 的 auth 调用:

流程supabase-swift 调用说明
登录auth.signIn(email:password:)
注册auth.signUp(...) + data: ["name": ...]姓名存入用户 metadata
验证注册auth.verifyOTP(type: .signup)6 位邮箱 OTP
重发验证码auth.resend(type: .signup)
忘记密码auth.resetPasswordForEmail(...)发送恢复码
完成重置verifyOTP(type: .recovery)update(password:)两步
修改密码auth.update(user: .init(password:))登录状态下
修改邮箱auth.update(user: .init(email:))触发确认 OTP
确认改邮箱verifyOTP(type: .emailChange)
登出auth.signOut()当前设备
登出其他会话auth.signOut(scope: .others)保留当前设备

还有一条免密路径—— sendLoginOtp / verifyLoginOtpsignInWithOTP(shouldCreateUser: false)verifyOTP(type: .magiclink))。

重试策略

仓库对每个方法分别编码了重试策略——这是刻意的正确性选择,而非一刀切的封装:

  • 幂等调用最多重试 2 次(间隔 300ms):resendEmailOtpresetPasswordchangeEmailsignOutOtherSessionsdeleteAccount
  • 一次性 / 凭据提交绝不重试signInsignUp、所有 verify*OtpcompletePasswordResetchangePassword 以及社交登录流程。重试会烧掉一次性 OTP,或触发 验证码 / 限流阈值。

输入校验

Features/Auth/Validation/AuthValidation.swift 内含纯 Swift、无依赖的校验器(emailpasswordotprequired),返回类型化的 AuthValidationError。规则:合法邮箱正则; 密码 ≥ 8 位且至少含一个字母和一个数字;OTP 恰好 6 位数字。因为是纯函数,无需网络或视图即可 单元测试。

社交登录

模板 README 的「Out of scope」一行已过时——Apple 与 Google 登录都在 Features/Auth/Social/ 中完整接好。已对照代码核实。

两个提供方都获取原生 ID token,再通过 auth.signInWithIdToken(...) 交给 Supabase——无需 网页重定向。

Apple

AppleSignInButton 直接驱动 ASAuthorizationController(以便渲染为纯图标磁贴),为每次请求 生成随机 nonce,把它的 SHA-256 哈希发给 Apple,同时把原始 nonce 转交给 Supabase (OpenIDConnectCredentials(provider: .apple, idToken:, nonce:))。首次登录时用户姓名会被 写入用户 metadata。无需 xcconfig 密钥——在 Xcode 中添加 Sign in with Apple 能力,并在 Supabase 启用 Apple 提供方即可。Apple 磁贴置于首位(前导),以满足 App Store 审核的显著性 要求。

Google

GoogleSignInButton 使用 GoogleSignIn SDK(GIDSignIn.signIn(withPresenting:))获取 Google ID token,再调用 OpenIDConnectCredentials(provider: .google, idToken:)。它需要 GOOGLE_IOS_CLIENT_ID——未设置时磁贴显示为暗色且不可点击,以便本地开发时 UI 仍能正常 排布。

配置(来自模板的 docs/google-sign-in-configuration.md):

创建两个 Google Cloud OAuth 客户端

一个 Web 客户端(供 Supabase 使用)和一个 iOS 客户端,其 Bundle ID 需匹配 com.soarstarter.SoarStarterSwift。把 https://<ref>.supabase.co/auth/v1/callback 加入 Web 客户端的授权重定向 URI。

配置 Supabase Google 提供方

Authentication → Providers → Google 下:把 Client IDs 设为逗号分隔的 <web-client-id>,<ios-client-id>(Web 在前,且包含 iOS 以便接受 token audience)、填入 Web 客户端密钥,并把 Skip nonce checks 打开(原生流程不传 nonce)。

Secrets.xcconfig 中设置密钥

GOOGLE_IOS_CLIENT_IDGOOGLE_REVERSED_CLIENT_ID(同一 ID,各段反转)。项目会把它们注入 Info.plist,作为 GIDClientID 和用于 OAuth 回调的 CFBundleURLTypes URL scheme。改动后需 重新构建。

常见的 Google 故障(audience 不匹配、nonce 错误、不返回应用)见 疑难排查

注销账户

Me → Account & Security → Delete account 会带上用户 JWT 调用 delete-account Edge Function——客户端无法删除自己的 auth.users 行,只有 service role 能。该函数依次:

  1. 从 JWT 识别调用者(仅 POST,无请求体)。
  2. 删除用户的 todosdevice_tokensprofiles 行(缺失的表会跳过; subscriptions / paymentsauth.users 级联)。
  3. 移除 sample-uploads/<user-id>/ 下的用户文件。
  4. 调用 auth.admin.deleteUser,吊销所有设备上的每个会话。

行与文件在删除 auth 用户之前清除,这样中途失败时账户仍完好、调用可安全重试。Apple 要求 应用内提供账户注销才能通过 App Store 审核——参见 App Store 发布

相关页面

On this page