Flutter + Supabase 集成 Apple Sign-In 完整指南

如果你的 Flutter 应用在 iOS 上提供任何第三方登录------Google、Facebook 或者其他------苹果的 App Review 准则 4.8 会强制你也必须提供 Sign in with Apple。跳过这一步,App Store 审核一定会拒绝。

这是没得商量的。痛苦的部分是配置流程:Apple Developer Console、Services ID、.p8 密钥、capabilities、entitlements、deep links------而且大部分内容分散在三四个不同的 Apple 和 Supabase 页面上,这些页面之间还互相不太一致。

这是我第一次做时希望拥有的指南。完整的 Flutter + Supabase Apple Sign-In 流程,每一步按顺序来,把真正会坑到你的地方标出来。


最终你会得到什么

  • iOS、macOS 和 Android 上都能工作的 Apple Sign-In
  • 一个干净的 Flutter 辅助函数,包装原生流程并返回 Supabase 的 AuthResponse
  • 清楚理解什么时候需要 Services ID(什么时候不需要)
  • 正确处理 "Apple 只发送一次姓名和邮箱" 这个坑

第一步 --- Apple Developer Console

打开 Apple Developer portal,进入 Certificates, Identifiers & Profiles → Identifiers

在你的 App ID 上启用 Sign In with Apple

  1. 选择你现有的 App ID(匹配你 Flutter 应用 bundle identifier 的那个)。
  2. 往下滚动,勾选 Sign In with Apple,然后保存。

这是 原生 iOS / macOS 流程 的最低要求。如果你只针对这两个平台,可以直接跳到 Supabase 配置。

创建 Services ID(仅用于 web + Android)

如果你还需要支持 Android 或 web,就必须添加 Services ID。这是 Supabase 在没有 Apple 原生 SDK 的平台上使用的 OAuth 重定向流程。

  1. Identifiers 中,点击 + 并选择 Services IDs
  2. 设置一个 identifier ------ 惯例是在 bundle ID 后面加 .service 后缀(例如 com.example.app.service)。
  3. 启用 Sign In with Apple → Configure
  4. 把你的 Supabase 域名添加到允许的域名:<PROJECT_REF>.supabase.co
  5. 添加 return URL:https://<PROJECT_REF>.supabase.co/auth/v1/callback

创建 Sign In with Apple 密钥

还在 Apple Developer portal 中,进入 Keys → + 并勾选 Sign In with Apple 。下载生成的 .p8 文件------这是唯一一次可以下载的机会,所以立即备份。

在第二步生成 Secret Key JWT 时,你需要三样东西:

  • Key ID(创建密钥后可见)
  • Team ID(Apple Developer portal 右上角)
  • .p8 文件的内容(用文本编辑器打开)

第二步 --- Supabase Dashboard

生成 Secret Key JWT

目前的 Supabase dashboard 需要 JWT 本身------你在本地签名,然后粘贴结果。你需要 .p8 文件、Team ID、Key ID 和 Services ID。

用以下 payload 签名一个 ES256 JWT(一个小的 Node、Python 或 Ruby 脚本就行------搜 "apple sign in client_secret generator" 找一次性脚本):

json 复制代码
{
  "iss": "<TEAM_ID>",
  "iat": <now>,
  "exp": <now + 15777000>,
  "aud": "https://appleid.apple.com",
  "sub": "<SERVICES_ID>"
}

设置 JWT header kid 为你的 Key ID,algES256。把生成的 token 粘贴到 Secret Key 字段。

Apple JWT 6 个月后过期。 设置日历提醒,因为 token 过期时不会有任何警告------你的 web 和 Android 登录会默默地开始失败。原生 iOS/macOS 流程不受影响,因为它不使用 JWT。
纯 iOS 应用:跳过这一半。 原生流程直接把 idToken 发给 Supabase,Supabase 用 Apple 的公钥验证。你不需要 Services ID、.p8 或 Secret Key------只需启用 Apple 作为 provider,并把 bundle ID 添加到 Client IDs
或者直接用 Supabase JWT Generator 帮你生成 JWT。只是记得设置提醒每 6 个月重新生成一次。

在 Supabase 中配置 provider

在你的 Supabase 项目中,进入 Authentication → Providers → Apple 并开启 Enable Sign in with Apple

Dashboard 暴露了四个字段加一个 callback URL:

  • Client IDs ------ 允许的 bundle ID(用于原生流程)和 Services ID(用于 web 流程)的逗号分隔列表。对于纯 iOS 应用,这就是你的 bundle ID(例如 com.example.app)。如果你也针对 web 或 Android,追加你的 Services ID:com.example.app, com.example.app.service
  • Secret Key (for OAuth) ------ 你上面生成的 JWT。仅用于 web/Android 重定向流程,原生 iOS/macOS 会忽略。
  • Allow users without an email ------ 如果你想接受通过 Apple 邮件中继隐藏邮箱地址的用户,开启这个。否则这些用户的登录会静默失败,你会花一个下午想不明白为什么。
  • Callback URL (for OAuth) ------ Supabase 自动生成,格式为 https://<PROJECT_REF>.supabase.co/auth/v1/callback。这是你在第一步 Services ID 中注册的 URL。如果你跳过了那一步,从这里复制 URL 并添加到你的 Services ID 配置中。

保存。


第三步 --- iOS 配置

在 Xcode 中,打开 Runner target → Signing & Capabilities → + Capability → Sign In with Apple

这就是整个 iOS 设置。不需要修改 Info.plist,不需要手动编辑 entitlements------Xcode 添加 capability 时会自动处理 entitlement 文件。


第四步 --- macOS 配置

同样的路径:target Runner → Signing & Capabilities → + Capability → Sign In with Apple

然后双击检查 entitlements 文件(macos/Runner/DebugProfile.entitlementsRelease.entitlements)包含:

xml 复制代码
<key>com.apple.developer.applesignin</key>
<array>
  <string>Default</string>
</array>

顺便确认 Outgoing Connections (Client) 已启用------你需要它来连接 Supabase。


第五步 --- Android 配置

Apple 在 Android 上没有原生 SDK,所以你要退回到 Supabase 处理的 OAuth 重定向流程:

dart 复制代码
await Supabase.instance.client.auth.signInWithOAuth(
  OAuthProvider.apple,
  redirectTo: 'your-scheme://callback',
);

为了让重定向能回到你的应用,在 android/app/src/main/AndroidManifest.xml 中注册 deep link scheme:

xml 复制代码
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="your-scheme" android:host="callback" />
</intent-filter>

用户会得到一个基于 webview 的登录,而不是系统弹窗。不如 iOS 精致,但这是 Apple 在 Android 上给我们的唯一选择。

💡 注意:我个人不建议在 Android 上提供 Apple Sign-In,因为用户体验比原生 iOS 弹窗差太多。如果你一定要提供,确保同时提供 Google Sign-In 作为替代------Android 用户更熟悉 Google,可能根本不知道他们有 Apple ID 可以登录。


第六步 --- Flutter 依赖

三个包搞定所有工作:

yaml 复制代码
dependencies:
  sign_in_with_apple: ^6.x.x
  supabase_flutter: ^2.x.x

sign_in_with_apple 在 iOS/macOS 上触发原生 Apple 弹窗。supabase_flutter 处理 token 交换。crypto 让我们能 hash nonce------接下来会讲到。


第七步 --- Flutter 代码(原生 iOS / macOS 流程)

这是我每个项目都会用的函数。它生成 nonce,触发 Apple 凭证请求,然后把得到的 idToken 转发给 Supabase:

dart 复制代码
Future<AuthResponse> signInWithApple() async {
  // 1. 生成 nonce
  final rawNonce = client.auth.generateRawNonce();
  final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();

  // 2. 触发原生 Apple 弹窗
  final credential = await SignInWithApple.getAppleIDCredential(
    scopes: [
      AppleIDAuthorizationScopes.email,
      AppleIDAuthorizationScopes.fullName,
    ],
    nonce: hashedNonce,
  );

  final idToken = credential.identityToken;
  if (idToken == null) {
    throw Exception('Apple Sign-In returned no idToken');
  }

  // 3. 用 idToken 与 Supabase 交换
  return Supabase.instance.client.auth.signInWithIdToken(
    provider: OAuthProvider.apple,
    idToken: idToken,
    nonce: rawNonce,
  );
}

这里按顺序做了三件事:

  1. 我们生成一个随机的 rawNonce,并把它的 SHA-256 hash 传给 Apple。
  2. Apple 把这个 hash 烘焙进返回的签名 idToken 中。
  3. 我们把 原始 nonce(未 hash)发给 Supabase,Supabase 重新 hash 并确认与 Apple 签名的一致。

这个往返过程保护你免受重放攻击。


第八步 - 关联匿名账户(可选)

如果你支持匿名登录,可以把 Apple 凭证关联到现有的匿名用户,而不是创建新账户:

dart 复制代码
@override
Future<void> signupFromAnonymousWithApple() async {
  final credential = await SignInWithApple.getAppleIDCredential(
    scopes: [ AppleIDAuthorizationScopes.email],
  );
  final response = await client.auth.linkIdentityWithIdToken(
    provider: OAuthProvider.apple,
    idToken: credential.identityToken!,
  );
}

用户从匿名会话触发 Apple Sign-In。2 - 我们像之前一样获取 Apple 凭证。3 - 不是调用 signInWithIdToken,而是调用 linkIdentityWithIdToken 把 Apple provider 关联到现有的匿名用户。


会坑到你的坑

Apple 只发送姓名和邮箱 仅在第一次登录时

这是每个团队在生产环境都会遇到的 bug。用户第一次授权你的应用时,AppleIDCredential 包含 givenNamefamilyNameemail。之后每次用同一个 Apple ID 登录,这些字段都是 null

解决办法:第一次收到姓名和邮箱时,立即持久化到你自己的数据库。之后从你的数据库读取,而不是从 Apple 的 payload。

dart 复制代码
final user = response.user;
if (credential.givenName != null) {
  // 第一次登录------把姓名保存到 profile
  await Supabase.instance.client.from('profiles').upsert({
    'id': user!.id,
    'first_name': credential.givenName,
    'last_name': credential.familyName,
    'email': credential.email,
  });
}

Nonce 是强制的,不是可选的

有些教程跳过 nonce。别这样。没有它,Supabase 无法验证 idToken 是为你的特定请求签发的,signInWithIdToken 会拒绝。crypto 包给你的应用增加 12 KB------这是你做过的最便宜的安全交易。

Services ID 和 .p8 原生 iOS 不需要

值得重复,因为很多人会困惑:如果你的应用是纯 iOS,可以完全忽略 Services ID 和 key。原生流程把 idToken 直接发给 Supabase,Supabase 用 Apple 的公钥验证。.p8 只用于 Android 和 web 上的 OAuth 重定向流程。

authorizationCode vs idToken

AppleIDCredential 同时暴露 identityTokenauthorizationCodeauthorizationCode 用于服务端交换(当你有自己的后端需要与 Apple 通信时)。对于 Supabase,你需要 identityToken------signInWithIdToken 会处理剩下的。


总结

Apple Sign-In 的配置是移动开发中最吃力不讨好的任务之一------配置分散在三个平台上,文档碎片化,而且 Apple 主动把某些部分做得比必要的更难(说的就是你,name-and-email-only-once)。

但一旦配置正确,它就能工作。原生弹窗很快,用户信任它,而且 Apple 审核员不会再拒绝你的构建。

相关推荐
小村儿1 小时前
连载
前端·后端·ai编程
dinl_vin2 小时前
LangChain 系列·(九):Agent——让 AI 自己做决策
前端·人工智能·langchain
孟祥_成都2 小时前
前端唯一的护城河?结合 AI 将字节组件库 Headless 化后的感想~
前端·人工智能·react.js
尽欢i2 小时前
前端大坑!文件切片上传后端总报错找不到文件名?
前端·javascript
Sylvia33.2 小时前
世界杯数据链路解析:从球场传感器到终端推送的毫秒级架构
java·前端·python·架构
镜宇秋霖丶2 小时前
2026.5.10@霖宇博客制作中遇见的问题
前端·vue.js·elementui
ai超级个体2 小时前
前端唯一的护城河?结合 AI 将字节组件库 Headless 化后的感想~
前端·react·ai编程·ant design·组件库·vibe coding
冴羽yayujs2 小时前
前端周报:Remix 3、Node 26 与 Chrome 148
前端
问心无愧05132 小时前
ctf show web 入门39
android·前端·笔记