跳转至正文

如何使用 Capacitor 实现 Sign in with Apple(2026)

在 iOS、Android 和 Web 上的 Capacitor 8 应用中添加“通过 Apple 登录”:Apple 开发者配置、插件 code、后端 token 校验及常见错误。

在 Capacitor 应用中运行“Sign in with Apple”按钮,使用 Capgo Social Login 插件

要在 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 上启用该功能

  1. 打开 Certificates, Identifiers & Profiles 并选择 标识符.
  2. 打开与您的 Capacitor 匹配的 App ID appId (例如 com.example.app)。如果不存在,请创建。
  3. 在 功能特性,勾选 通过 Apple 登录 并保存。除非你要将多个 App 分组,否则请将其保留为主 App ID。

如果你更改了现有 App ID 的 Capabilities,请重新生成使用该 App ID 的 provisioning profile。旧 profile 不包含新的 entitlement。

创建 Services ID(仅限 Android 和 Web)

  1. 在 标识符,点击 + 并选择 服务 ID.
  2. 使用类似“示例 Web 登录”的描述和类似 com.example.app.signin的标识符。此值将成为 clientId 在 Android 和 Web 上。
  3. 打开新的 Services ID,勾选 通过 Apple 登录,然后点击 配置.
  4. 选择你的 App ID 作为主 App ID。
  5. 在以下位置添加你的域名 域名和子域名 (不含 scheme,例如 auth.example.com).
  6. 在以下位置添加返回 URL 返回 URL 需包含完整的 https:// 路径,例如 https://auth.example.com/apple/callback。对于 Web 登录,添加调用登录的页面 URL,带末尾斜杠和不带末尾斜杠各一个。

创建 Sign in with Apple 密钥(用于 Android 和服务端 token 交换)

  1. 前往 密钥,点击 +,勾选 通过 Apple 登录,然后点击 配置 以选择主 App ID。
  2. 注册密钥并下载 .p8 文件。Apple 只允许下载一次。
  3. 请记下 密钥 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 设置

  1. 运行 bunx cap open ios.
  2. 选择 应用 目标,打开 签名与能力,点击 + 功能,并添加 通过 Apple 登录.
  3. 请确保 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 重新打开。

在 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 属于原生部分,需要新的商店构建。

下一步该怎么做

为 Capacitor 应用提供 Live Updates

当 Web 层出现线上 Bug 时,通过 Capgo 发布修复,而不用等 App Store 审核好几天。用户会在后台收到更新,原生变更则仍走正常审核流程。

博客最新动态

Capgo 为你提供打造真正专业的移动应用所需的最有价值的洞察。