要在 Capacitor 应用中添加 Sign in with Apple,请安装 @capgo/capacitor-social-login,在 App ID 和 Xcode 中启用 Sign in with Apple 功能,然后调用 SocialLogin.initialize({ apple: {} })模式,然后调用 SocialLogin.login({ provider: 'apple' })。在 iOS 上,这会打开原生 Apple 登录界面并返回身份令牌。Android 和 Web 使用 Apple 的 Web OAuth 流程,需要 Services ID 和 HTTPS 重定向 URL。
本指南涵盖 Apple Developer 设置、iOS、Android 和 Web 的客户端 code、在后端验证令牌,以及您可能遇到的常见错误。目标为 Capacitor 8 和 Xcode 26,自 2026 年 4 月起,App Store Connect 已要求上传时使用此版本。
开始前的准备
| 要求 | 原因 |
|---|---|
| 付费 Apple Developer Program 会员资格 | App ID、Service ID 和 Sign in with Apple 密钥仅对会员开放 |
| 配备 Xcode 26 的 Mac | 2026 年构建和上传 iOS 应用所需 |
| Capacitor 8 项目 | 插件 v8 支持 Capacitor 8 |
| 您控制的 HTTPS 域名 | 仅适用于 Android 和 Web。Apple 拒绝 localhost 纯 HTTP 返回 URL |
| 后端端点 | 始终用于验证 token。在 Android 上,它还负责接收 Apple 的回调。 |
如果你还没有签名证书, iOS 证书生成器 可以自动创建,无需在 Mac 钥匙串中手动操作。
Sign in with Apple 在各平台的工作原理
Apple 只在自家平台提供原生 SDK,所以流程不同:
| 平台 | 机制 | 使用的 Client ID | 登录所需的后端 |
|---|---|---|---|
| iOS | 原生 ASAuthorizationAppleIDProvider sheet |
你的应用 bundle ID | 否 |
| Android | 在 Chrome Custom Tab 中通过 Apple Web OAuth 登录,Apple 会向你的服务器发起 POST 请求,服务器再重定向回应用 | Services ID | 是 |
| Web 端 | Apple JS 弹窗 | Services ID | 否(但需验证 token) |
audience(aud)字段与 client ID 一致。来自 iOS 的 token 携带你的 bundle ID,来自 Android 和 Web 的 token 携带 Services ID。你的后端必须同时接受这两种。
第 1 步:配置 Apple Developer 账号
在 App ID 上启用该功能
- 打开 Certificates, Identifiers & Profiles 并选择 标识符.
- 打开与您的 Capacitor 匹配的 App ID
appId(例如com.example.app)。如果不存在,请创建。 - 在 功能特性,勾选 通过 Apple 登录 并保存。除非你要将多个 App 分组,否则请将其保留为主 App ID。
如果你更改了现有 App ID 的 Capabilities,请重新生成使用该 App ID 的 provisioning profile。旧 profile 不包含新的 entitlement。
创建 Services ID(仅限 Android 和 Web)
- 在 标识符,点击 + 并选择 服务 ID.
- 使用类似“示例 Web 登录”的描述和类似
com.example.app.signin的标识符。此值将成为clientId在 Android 和 Web 上。 - 打开新的 Services ID,勾选 通过 Apple 登录,然后点击 配置.
- 选择你的 App ID 作为主 App ID。
- 在以下位置添加你的域名 域名和子域名 (不含 scheme,例如
auth.example.com). - 在以下位置添加返回 URL 返回 URL 需包含完整的
https://路径,例如https://auth.example.com/apple/callback。对于 Web 登录,添加调用登录的页面 URL,带末尾斜杠和不带末尾斜杠各一个。
创建 Sign in with Apple 密钥(用于 Android 和服务端 token 交换)
- 前往 密钥,点击 +,勾选 通过 Apple 登录,然后点击 配置 以选择主 App ID。
- 注册密钥并下载
.p8文件。Apple 只允许下载一次。 - 请记下 密钥 ID 以及你的 团队 ID (显示在开发者门户右上角及 Membership 下方)。
你的服务器使用该密钥对 client_secret 发送给 Apple token 端点的 JWT 进行签名。
如果需要发送邮件,请注册邮件来源
用户可以选择 隐藏我的电子邮件,它会为你生成一个 @privaterelay.appleid.com 地址。Apple 只会转发来自你在以下位置注册的域名和地址的邮件: 服务 > 使用 Sign in with Apple 进行邮件通信。跳过这一步,你的密码重置和收据邮件就会悄悄消失。
第 2 步:安装插件
bun add @capgo/capacitor-social-login
bunx cap sync
该 plugin 内置了多个 provider。如果你只用 Apple(可能还有 Google),请在 capacitor.config.ts 中关闭其他 provider,这样就不会打包它们的 SDK:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'Example',
webDir: 'dist',
plugins: {
SocialLogin: {
providers: {
apple: true,
google: false,
facebook: false,
twitter: false,
},
},
},
};
export default config;
运行 bunx cap sync 修改此代码块后再次运行。禁用的 provider 在运行时不可用,所以不要禁用你正在调用的 provider。移除 Facebook 也会同时移除 AD_ID 其 SDK 在 Google Play Console 中触发的权限警告。
第 3 步:iOS 设置
- 运行
bunx cap open ios. - 选择 应用 目标,打开 签名与能力,点击 + 功能,并添加 通过 Apple 登录.
- 请确保 bundle identifier 与您配置的 App ID 一致。
Xcode 会写入 com.apple.developer.applesignin 为 App.entitlements。如果你在 CI 中 build,或使用 Capgo 构建,请提交该 entitlements 文件,否则云端 build 生成的应用会因 ASAuthorizationError code 1000 而报错。
否 Info.plist URL scheme 或 AppDelegate 更改是 iOS 上 Apple 登录所必需的。
步骤 4:Android 配置
Apple 在 Android 上没有 SDK。该插件会使用你的 Services ID 在 Custom Tab 中打开 Apple 的授权页面。随后 Apple 会将结果以 form POST 形式发送到你的 HTTPS 返回 URL。你的服务器用 code 换取 token,然后重定向到 deep link,让应用携带 token 重新打开。
添加 deep link
在 android/app/src/main/AndroidManifest.xml,在 MainActivity <activity> 元素中:
<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="com.example.app" android:host="apple-login" />
</intent-filter>
将 intent 转发给插件
编辑 MainActivity.java 以便深度链接能到达插件:
package com.example.app;
import android.content.Intent;
import android.net.Uri;
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginHandle;
import ee.forgr.capacitor.social.login.SocialLoginPlugin;
public class MainActivity extends BridgeActivity {
@Override
protected void onNewIntent(Intent intent) {
Uri data = intent.getData();
if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null
&& "apple-login".equals(data.getHost())) {
PluginHandle handle = getBridge().getPlugin("SocialLogin");
if (handle != null) {
Plugin plugin = handle.getInstance();
if (plugin instanceof SocialLoginPlugin) {
((SocialLoginPlugin) plugin).handleAppleLoginIntent(intent);
return;
}
}
}
super.onNewIntent(intent);
}
}
按 host 过滤可确保其他深度链接(如密码重置、营销链接)正常工作。
回调端点
Apple 会向你的返回 URL 发送 code, id_token, state,首次登录时还会发送一个 user JSON 字符串。该端点需交换 code 并重定向到应用,携带 success=true 及令牌,这是插件解析的约定。以下是使用 jose:
import { Hono } from 'hono';
import { SignJWT, importPKCS8, createRemoteJWKSet, jwtVerify } from 'jose';
import { saveAppleRefreshToken } from './db'; // your own persistence layer
const app = new Hono();
const TEAM_ID = process.env.APPLE_TEAM_ID!;
const KEY_ID = process.env.APPLE_KEY_ID!;
const SERVICE_ID = process.env.APPLE_SERVICE_ID!; // com.example.app.signin
const PRIVATE_KEY = process.env.APPLE_PRIVATE_KEY!; // contents of the .p8 file
const APP_REDIRECT = 'com.example.app://apple-login';
const APPLE_JWKS = createRemoteJWKSet(new URL('https://appleid.apple.com/auth/keys'));
async function appleClientSecret() {
const key = await importPKCS8(PRIVATE_KEY, 'ES256');
return new SignJWT({})
.setProtectedHeader({ alg: 'ES256', kid: KEY_ID })
.setIssuer(TEAM_ID)
.setSubject(SERVICE_ID)
.setAudience('https://appleid.apple.com')
.setIssuedAt()
.setExpirationTime('5m')
.sign(key);
}
app.post('/apple/callback', async (c) => {
const form = await c.req.parseBody();
const code = String(form.code ?? '');
if (!code) {
return c.redirect(`${APP_REDIRECT}?success=false`);
}
// `form.user` is only present on the first authorization. Store it now.
const res = await fetch('https://appleid.apple.com/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: SERVICE_ID,
client_secret: await appleClientSecret(),
code,
grant_type: 'authorization_code',
redirect_uri: 'https://auth.example.com/apple/callback',
}),
});
if (!res.ok) {
return c.redirect(`${APP_REDIRECT}?success=false`);
}
const tokens = await res.json();
// Verify the identity token, then keep the long-lived refresh token on the server.
// Only the short-lived tokens go back to the app in the deep link.
const { payload } = await jwtVerify(tokens.id_token, APPLE_JWKS, {
issuer: 'https://appleid.apple.com',
audience: SERVICE_ID,
});
if (tokens.refresh_token) {
await saveAppleRefreshToken(String(payload.sub), tokens.refresh_token);
}
const params = new URLSearchParams({
success: 'true',
access_token: tokens.access_token,
id_token: tokens.id_token,
});
return c.redirect(`${APP_REDIRECT}?${params}`);
});
export default app;
的最小 Hono 示例。插件从深度链接的查询字符串中读取令牌,因此你放入其中的任何内容都可能出现在日志中,或被注册了相同自定义 scheme 的其他应用读取。只发送短时效的 access_token 以及 id_token,切勿发送 refresh token,并验证 id_token 签名以及 nonce 在您的服务器上验证后再信任它,并使用您应用独有的 scheme(您的反向 bundle ID)。
本指南中介绍的 client_secret JWT 有效期最长可达六个月,但为每个请求生成短时效 JWT 可避免过期故障。请保留 .p8 文件在您的密钥管理器中,切勿放入应用 bundle。
第 5 步:Web 配置
Web 构建使用 Apple JS 和 Services ID。请将触发登录的确切页面 URL 添加到 Services ID 的返回 URL 中。Apple 不接受 localhost,因此请在已部署的预览域名上,或通过带真实主机名的隧道来测试 Web 登录。
步骤 6:初始化并登录
在应用启动时初始化一次。传入 redirectUrl 仅在 Android 和 Web 上。在 iOS 上,非空的 redirectUrl 会让插件把结果 POST 到该 URL,而原生流程中你并不希望这样。
import { Capacitor } from '@capacitor/core';
import { SocialLogin } from '@capgo/capacitor-social-login';
const platform = Capacitor.getPlatform();
export async function initAuth() {
await SocialLogin.initialize({
apple: {
clientId: platform === 'ios' ? 'com.example.app' : 'com.example.app.signin',
redirectUrl:
platform === 'android'
? 'https://auth.example.com/apple/callback'
: platform === 'web'
? 'https://app.example.com/login'
: '',
},
});
}
然后通过按钮触发登录。使用随机 nonce,让服务器能拒绝重放的 token:
import { SocialLogin } from '@capgo/capacitor-social-login';
export async function signInWithApple() {
const nonce = crypto.randomUUID();
try {
const { result } = await SocialLogin.login({
provider: 'apple',
options: {
scopes: ['email', 'name'],
nonce,
},
});
if (!result.idToken) {
throw new Error('Apple did not return an identity token');
}
// Send to your backend. Name and email are only present on the first login.
const response = await fetch('https://api.example.com/auth/apple', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
idToken: result.idToken,
nonce,
givenName: result.profile.givenName,
familyName: result.profile.familyName,
}),
});
return response.json();
} catch (error: any) {
if (error?.code === 'USER_CANCELLED') {
return null; // user closed the sheet, not a failure
}
throw error;
}
}
响应会返回:
| 字段 | 内容 |
|---|---|
result.idToken |
Apple 签名的 JWT,也就是你的服务器要验证的内容 |
result.profile.user |
Apple 的稳定用户 ID(与 sub claim 相同) |
result.profile.email |
邮箱或私密中继地址(授权后提供) |
result.profile.givenName / familyName |
仅在首次授权时提供 |
result.accessToken |
在默认设置下,这里保存的是授权 code,而不是真正的 access token |
result.authorizationCode |
使用以下内容初始化时会出现 useProperTokenExchange: true |
如果你的服务器需要 Apple refresh token(例如在用户删除账号时调用 revoke 端点),请设置 useProperTokenExchange: true ,详见 apple 配置并交换 authorizationCode 在服务器上。
第 7 步:在后端验证 identity token
切勿信任客户端发送的用户资料字段。请使用 Apple 的公钥验证 JWT 签名并检查 claims:
import { createRemoteJWKSet, jwtVerify } from 'jose';
const appleKeys = createRemoteJWKSet(new URL('https://appleid.apple.com/auth/keys'));
export async function verifyAppleToken(idToken: string, expectedNonce: string) {
const { payload } = await jwtVerify(idToken, appleKeys, {
issuer: 'https://appleid.apple.com',
audience: ['com.example.app', 'com.example.app.signin'], // bundle ID + Services ID
});
if (payload.nonce !== expectedNonce) {
throw new Error('Nonce mismatch');
}
return {
appleUserId: payload.sub as string,
email: payload.email as string | undefined,
emailVerified: payload.email_verified === true || payload.email_verified === 'true',
};
}
用户记录的主键应使用 sub,而不是 email。email 可能是中继地址,用户也可能停止共享。验证后,签发你自己的 session token。如果你使用 Supabase 或 Firebase,它们会替你验证 token,详见 使用 Capacitor Social Login 的 Supabase.
需要留意的 App Store 规则
- 准则 4.8:如果提供 Google、Facebook 或其他第三方登录作为主要账号系统,还必须提供同等的隐私优先选项。Sign in with Apple 符合要求。
- 准则 5.1.1(v):允许用户创建账号的应用,必须允许用户在应用内删除账号。对于使用 Sign in with Apple 的用户,删除时请调用 Apple 的
https://appleid.apple.com/auth/revoke接口。请参阅我们的 账号删除指南. - 按钮设计:请使用 Apple 认可的按钮样式,并让按钮至少与其他登录按钮一样醒目。
故障排除
| 现象 | 可能原因 | 修复 |
|---|---|---|
ASAuthorizationError code 1000 在 iOS 上 |
构建目标中缺少 Capability 或 entitlement | 在 Xcode 中添加 Sign in with Apple,提交 App.entitlements,重新生成 profiles |
| Code 1001 在 iOS 上 | 用户已取消 | 该插件会以以下错误拒绝: USER_CANCELLED,请将其视为非错误 |
invalid_client 在 Android 或 Web 上 |
错误 clientId (使用了 bundle ID 而非 Services ID)或配置有误 client_secret |
使用 Services ID,检查 Team ID、Key ID 和密钥内容 |
invalid_request: Invalid redirect_uri |
Return URL 未按原样注册 | 添加完整的 https:// URL 到 Services ID,并匹配末尾斜杠 |
| Android 登录后 App 未能返回 | Intent filter 的 scheme 或 host 与服务器重定向不符 | 对比 com.example.app://apple-login 逐字符 |
apple.android.redirectUrl is null or empty |
否 redirectUrl 在 Android 上 |
在 Android 上初始化时传入您的 HTTPS 回调 URL |
姓名和邮箱是 null |
非首次授权 | 从您的数据库中读取。在设备设置中撤销应用以重新测试 |
后端拒绝了 Token: aud 不匹配 |
服务器仅接受一个受众 | 同时支持 Bundle ID 和 Services ID |
发布后推送修复
Most auth bugs live in TypeScript: a wrong client ID per platform, a missing nonce, bad error handling. Those can be shipped to users the same day with Capgo 实时更新 而无需等待 App 审核。对 entitlements、Android manifest 或 MainActivity 属于原生部分,需要新的商店构建。
下一步该怎么做
- 在 Apple 旁边添加 Google: 如何使用 Capacitor 通过 Google 登录.
- 为你的应用选择合适的认证方式组合: 如何在 Capacitor 应用中添加身份验证.
- 插件参考和平台页面: Social Login 文档 以及 Apple iOS 设置页面.