身份认证
邮箱密码登录、OTP 验证、社交登录与会话管理。
身份认证基于 supabase-swift 的 Auth 模块,已端到端 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 / verifyLoginOtp
(signInWithOTP(shouldCreateUser: false) → verifyOTP(type: .magiclink))。
重试策略
仓库对每个方法分别编码了重试策略——这是刻意的正确性选择,而非一刀切的封装:
- 幂等调用最多重试 2 次(间隔 300ms):
resendEmailOtp、resetPassword、changeEmail、signOutOtherSessions、deleteAccount。 - 一次性 / 凭据提交绝不重试:
signIn、signUp、所有verify*Otp、completePasswordReset、changePassword以及社交登录流程。重试会烧掉一次性 OTP,或触发 验证码 / 限流阈值。
输入校验
Features/Auth/Validation/AuthValidation.swift 内含纯 Swift、无依赖的校验器(email、
password、otp、required),返回类型化的 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 审核的显著性
要求。
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_ID 和 GOOGLE_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 能。该函数依次:
- 从 JWT 识别调用者(仅 POST,无请求体)。
- 删除用户的
todos、device_tokens、profiles行(缺失的表会跳过;subscriptions/payments从auth.users级联)。 - 移除
sample-uploads/<user-id>/下的用户文件。 - 调用
auth.admin.deleteUser,吊销所有设备上的每个会话。
行与文件在删除 auth 用户之前清除,这样中途失败时账户仍完好、调用可安全重试。Apple 要求 应用内提供账户注销才能通过 App Store 审核——参见 App Store 发布。
