Sign in with Apple 怎么接入?通过 Apple 登录的配置与 Swift 代码
最后更新
Sign in with Apple(通过 Apple 登录)接入 iOS App 分四步:给 App ID 开启 Sign in with Apple 能力并重新生成描述文件,在 entitlements 里加上 com.apple.developer.applesignin,用 SwiftUI 的 SignInWithAppleButton 拿到凭证并把 identityToken 交给服务端验证,最后处理授权撤销和账号删除时的 revoke。
下文从开发者后台配置一直讲到在自己的 iPhone 上登录成功,Apple 规则截至 2026 年 10 月,都附了官方链接。开始前准备好这几样东西。
- 已加入 Apple Developer Program 的账号,角色是账户持有人或管理员,开启能力需要这两种角色之一。
- 一个能用 KXBuild 装到 iPhone 上运行的 SwiftUI 工程,部署目标 iOS 15 或更高。还没有的按 SwiftUI 从新建工程到装上 iPhone做一个。
- 一部登录了 Apple 账户、开启了双重认证的 iPhone,并打开了开发者模式,开关位置见开发者模式怎么打开。
- 一个你自己的后端接口,用来验证登录凭证、建立账号。文中用
https://api.example.com/auth/apple代替。
什么时候必须提供「通过 Apple 登录」?
App Store 审核指南 4.8 登录服务规定,App 用第三方或社交登录(例如 Facebook、Google、微信登录)来创建或验证用户的主账号时,必须同时提供另一种等效的登录方式。这种登录方式要满足三条:只收集用户的姓名和邮箱;允许用户在建账号时隐藏邮箱;未经同意不收集用户在 App 里的行为用于广告。「通过 Apple 登录」满足这三条,是最常见的选择。
下面这些情况不需要另外提供。
| 情况 | 例子 |
|---|---|
| 只用你公司自己的账号体系 | 手机号加验证码、自家用户名密码 |
| 教育、企业或商务 App,要求用已有的学校或企业账号登录 | 企业内部办公 App |
| 用政府或行业背书的公民身份或电子身份认证 | 电子身份证登录 |
| App 是某个第三方服务的客户端,用户要直接登录那个服务才能看到内容 | 邮件客户端、某个社交平台的专用客户端 |
| 替代应用市场,或从替代应用市场分发、使用该市场的专用登录 | — |
另外两条和登录相关的规定也要一起满足。审核指南 5.1.1(v) 要求支持创建账号的 App 必须在 App 内提供删除账号的入口。
Apple 的账号删除说明写明,使用「通过 Apple 登录」的 App 在用户删除账号时,要调用 Sign in with Apple REST API 撤销用户的令牌,做法见下文 revoke 一节。审核由 Apple 决定,常见的被拒原因见 App 审核被拒的常见原因。
接入 Sign in with Apple 要做哪几步?
| 步骤 | 在哪里做 | 产出 |
|---|---|---|
| 1. App ID 开启 Sign in with Apple | developer.apple.com 的 Identifiers | App ID 带上这项能力 |
| 2. 重新生成描述文件 | developer.apple.com 的 Profiles | 含 com.apple.developer.applesignin 的描述文件 |
| 3. 加 entitlements | VS Code | MyApp.entitlements |
| 4. 写登录代码 | VS Code | 登录按钮、凭证处理、状态检查 |
| 5. 创建 Sign in with Apple 私钥 | developer.apple.com 的 Keys | 服务端用来生成 client_secret 的 .p8 |
| 6. 服务端验证和 revoke | 你的后端 | 验证 identityToken、换取和撤销令牌 |
| 7. 真机测试 | iPhone | 首次登录、再次登录、撤销后重新登录 |
第 1 步:App ID 怎么开启 Sign in with Apple?
- 登录 developer.apple.com,进入 Certificates, Identifiers & Profiles,在 Identifiers 页点开 App 的 App ID。App ID 要是精确的 Bundle ID,例如
com.example.myapp。 - 在 Capabilities 列表勾选 Sign in with Apple,点旁边的 Edit 或 Configure。
- 选 Enable as a primary App ID(作为主 App ID 启用),保存。第一次启用、或者这是一个新 App 时都选这一项。
- 点 Save 保存 App ID。
同一个产品还有 Mac 版、网页版要共用账号的,那些 App ID 选「与已有的主 App ID 分组」,用户只需授权一次。规则见 Apple 的关于通过 Apple 登录。
第 2 步:为什么要重新生成描述文件?
描述文件里记录着 App ID 当时开通的能力,开启 Sign in with Apple 之前生成的描述文件不含这项能力,必须重新生成。
- 在 Profiles 页逐个点开这个 App ID 的描述文件(Development、Ad Hoc、App Store),点 Edit,直接 Save,再 Download。
- 用新文件替换工程
keychain目录里的旧文件,旧的删掉。目录里同时留着新旧两份时,构建可能匹配到不含这项能力的旧文件。
在 VS Code 里双击新的 .mobileprovision 用描述文件查看器打开,权限里出现 com.apple.developer.applesignin 就说明已经包含这项能力。证书目录的放法和挑选规则见证书与签名,App ID 和描述文件的关系见 Bundle ID、App ID、证书和描述文件的关系。
第 3 步:给工程加 entitlements
在 App 源码文件夹里新建 MyApp.entitlements,内容如下。工程原本已有 entitlements 文件的(例如开了推送或 App Groups),把这个键加进原文件,不用新建。
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3<plist version="1.0">
4<dict>
5 <key>com.apple.developer.applesignin</key>
6 <array>
7 <string>Default</string>
8 </array>
9</dict>
10</plist>
键名和取值以 Apple 的 applesignin entitlement 文档为准,值是只含一项 Default 的数组。
然后让 target 用这个文件。以下两种做法任选其一。
- VS Code 插件。右键 target 选「KXBuild: 编辑目标设置」,在 General 页 Signing 分组的「Entitlements 文件」里填
MyApp/MyApp.entitlements,Ctrl+S 保存。 - 命令行。在工程目录执行下面这条,路径相对于 .xcodeproj 所在目录。
1kxbuild project add-setting -k CODE_SIGN_ENTITLEMENTS -v MyApp/MyApp.entitlements -t MyApp -P .\MyApp.xcodeproj
以后要加其他能力,双击 .entitlements 用属性列表编辑器编辑即可。设置项的位置见工程设置编辑器。
第 4 步:用 Swift 写登录代码
下面三个文件适合部署目标 iOS 15 及以上,在 Swift 6 严格并发模式下也能编译。SignInWithAppleButton 来自 SwiftUI,凭证类型来自 AuthenticationServices 框架。
用钥匙串保存用户标识
用户标识要存在钥匙串里,不要放 UserDefaults。新建 KeychainStore.swift。
1import Foundation
2import Security
3
4/// 把少量字符串存进钥匙串,删 App 重装后通常还在
5enum KeychainStore {
6 private static let service = "com.example.myapp.auth"
7
8 static func save(_ value: String, for key: String) {
9 delete(key)
10 let query: [String: Any] = [
11 kSecClass as String: kSecClassGenericPassword,
12 kSecAttrService as String: service,
13 kSecAttrAccount as String: key,
14 kSecValueData as String: Data(value.utf8),
15 kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
16 ]
17 SecItemAdd(query as CFDictionary, nil)
18 }
19
20 static func read(_ key: String) -> String? {
21 let query: [String: Any] = [
22 kSecClass as String: kSecClassGenericPassword,
23 kSecAttrService as String: service,
24 kSecAttrAccount as String: key,
25 kSecReturnData as String: true,
26 kSecMatchLimit as String: kSecMatchLimitOne,
27 ]
28 var item: CFTypeRef?
29 guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
30 let data = item as? Data else { return nil }
31 return String(data: data, encoding: .utf8)
32 }
33
34 static func delete(_ key: String) {
35 let query: [String: Any] = [
36 kSecClass as String: kSecClassGenericPassword,
37 kSecAttrService as String: service,
38 kSecAttrAccount as String: key,
39 ]
40 SecItemDelete(query as CFDictionary)
41 }
42}
登录管理器
新建 AppleSignInManager.swift,负责准备请求、处理凭证、把令牌发给服务端、检查凭证状态。把 loginURL 换成你自己的接口。
1import Foundation
2import AuthenticationServices
3import CryptoKit
4
5/// 发给自己服务器的登录请求体
6struct AppleLoginRequest: Codable {
7 let identityToken: String
8 let authorizationCode: String
9 let rawNonce: String
10 let user: String
11 let email: String?
12 let givenName: String?
13 let familyName: String?
14}
15
16@MainActor
17final class AppleSignInManager: ObservableObject {
18 // 换成你自己服务器的登录接口
19 static let loginURL = URL(string: "https://api.example.com/auth/apple")!
20
21 @Published private(set) var userID: String? = KeychainStore.read("appleUserID")
22 @Published var message: String?
23
24 private var currentNonce: String?
25 private var revokeObserver: NSObjectProtocol?
26
27 var isSignedIn: Bool { userID != nil }
28
29 init() {
30 // 用户在系统设置里停用了「通过 Apple 登录」时,系统发这个通知
31 revokeObserver = NotificationCenter.default.addObserver(
32 forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
33 object: nil,
34 queue: .main
35 ) { [weak self] _ in
36 Task { @MainActor in
37 self?.signOut(message: "Apple 账户授权已撤销,请重新登录")
38 }
39 }
40 }
41
42 /// 点按钮时调用:要姓名和邮箱,并带上 nonce
43 func prepare(_ request: ASAuthorizationAppleIDRequest) {
44 let nonce = Self.randomNonce()
45 currentNonce = nonce
46 request.requestedScopes = [.fullName, .email]
47 request.nonce = Self.sha256(nonce)
48 }
49
50 /// 处理按钮的回调结果
51 func handle(_ result: Result<ASAuthorization, Error>) async {
52 switch result {
53 case .success(let authorization):
54 guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
55 message = "收到的不是 Apple ID 凭证"
56 return
57 }
58 await signIn(with: credential)
59 case .failure(let error):
60 if let authError = error as? ASAuthorizationError, authError.code == .canceled {
61 return // 用户自己点了取消,不提示
62 }
63 message = "登录失败:\(error.localizedDescription)"
64 }
65 }
66
67 private func signIn(with credential: ASAuthorizationAppleIDCredential) async {
68 // user 是这个用户在你的开发者团队下固定不变的标识,用它当账号主键
69 let user = credential.user
70
71 // 邮箱和姓名只在第一次授权时返回,拿到就先存下来
72 if let email = credential.email {
73 KeychainStore.save(email, for: "appleEmail")
74 }
75 if let name = credential.fullName {
76 let formatted = PersonNameComponentsFormatter().string(from: name)
77 if !formatted.isEmpty {
78 KeychainStore.save(formatted, for: "appleFullName")
79 }
80 }
81
82 guard let tokenData = credential.identityToken,
83 let identityToken = String(data: tokenData, encoding: .utf8),
84 let codeData = credential.authorizationCode,
85 let authorizationCode = String(data: codeData, encoding: .utf8),
86 let rawNonce = currentNonce else {
87 message = "没有拿到 identityToken,请重试"
88 return
89 }
90
91 let body = AppleLoginRequest(
92 identityToken: identityToken,
93 authorizationCode: authorizationCode,
94 rawNonce: rawNonce,
95 user: user,
96 email: credential.email ?? KeychainStore.read("appleEmail"),
97 givenName: credential.fullName?.givenName,
98 familyName: credential.fullName?.familyName
99 )
100
101 do {
102 var request = URLRequest(url: Self.loginURL)
103 request.httpMethod = "POST"
104 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
105 request.httpBody = try JSONEncoder().encode(body)
106 let (_, response) = try await URLSession.shared.data(for: request)
107 guard (response as? HTTPURLResponse)?.statusCode == 200 else {
108 message = "服务器验证失败"
109 return
110 }
111 KeychainStore.save(user, for: "appleUserID")
112 userID = user
113 message = nil
114 } catch {
115 message = "网络错误:\(error.localizedDescription)"
116 }
117 }
118
119 /// 启动时和回到前台时调用,确认授权还有效
120 func checkCredentialState() async {
121 guard let userID else { return }
122 do {
123 let state = try await ASAuthorizationAppleIDProvider().credentialState(forUserID: userID)
124 switch state {
125 case .authorized:
126 break
127 case .revoked:
128 signOut(message: "Apple 账户授权已撤销,请重新登录")
129 case .notFound:
130 signOut(message: "请重新登录")
131 case .transferred:
132 message = "App 已转移到新的开发者团队,需要迁移账号"
133 @unknown default:
134 break
135 }
136 } catch {
137 // 查询失败(例如没登录 Apple 账户)时保持现状,下次再查
138 print("getCredentialState 失败: \(error)")
139 }
140 }
141
142 func signOut(message: String? = nil) {
143 KeychainStore.delete("appleUserID")
144 userID = nil
145 self.message = message
146 }
147
148 // MARK: - nonce
149
150 static func randomNonce(length: Int = 32) -> String {
151 let charset = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._")
152 var generator = SystemRandomNumberGenerator()
153 return String((0..<length).map { _ in charset.randomElement(using: &generator)! })
154 }
155
156 static func sha256(_ input: String) -> String {
157 SHA256.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined()
158 }
159}
几个要点。
user当账号主键。Apple 的文档写明,用户标识在你的开发者团队下唯一且不变,要用它而不是邮箱来识别用户。用户隐藏了邮箱时,你拿到的是一个privaterelay.appleid.com的转发地址。- 姓名和邮箱只给一次。按 Apple 的用户认证说明,姓名和邮箱只在用户第一次用「通过 Apple 登录」授权你的 App 时返回,之后在别的设备上登录也不会再给。用户停用后重新授权才会再次返回。
- 拿到就保存。拿到后要立刻保存,并尽快传给服务端,网络失败时下次还能补发。之后的
identityToken里仍带邮箱,但不带姓名。 - nonce 防重放。请求里放的是随机串的 SHA-256,
identityToken里的nonce字段就是这个哈希值。原始随机串发给服务端,服务端算一次 SHA-256 再比对。 .canceled不当错误。用户在登录框点了取消会走到这里,不用弹提示。
登录界面和 App 入口
新建 LoginView.swift,Swift 工程的基本配置见 Windows 开发 Swift iOS App。按钮的样式跟随深色、浅色模式切换,深色背景上用白色按钮。
1import SwiftUI
2import AuthenticationServices
3
4struct LoginView: View {
5 @EnvironmentObject private var auth: AppleSignInManager
6 @Environment(\.colorScheme) private var colorScheme
7
8 var body: some View {
9 VStack(spacing: 20) {
10 if auth.isSignedIn {
11 Text("已登录")
12 .font(.title2.bold())
13 Text(KeychainStore.read("appleFullName") ?? "Apple 用户")
14 Text(KeychainStore.read("appleEmail") ?? "未提供邮箱")
15 .foregroundStyle(.secondary)
16 Button("退出登录") {
17 auth.signOut()
18 }
19 } else {
20 SignInWithAppleButton(.signIn) { request in
21 auth.prepare(request)
22 } onCompletion: { result in
23 Task { await auth.handle(result) }
24 }
25 .signInWithAppleButtonStyle(colorScheme == .dark ? .white : .black)
26 .frame(height: 50)
27 }
28
29 if let message = auth.message {
30 Text(message)
31 .font(.footnote)
32 .foregroundStyle(.red)
33 }
34 }
35 .padding(24)
36 }
37}
把 App 入口文件(例如 MyAppApp.swift)改成下面这样。启动时和每次回到前台时都检查一次凭证状态。
1import SwiftUI
2
3@main
4struct MyAppApp: App {
5 @StateObject private var auth = AppleSignInManager()
6 @Environment(\.scenePhase) private var scenePhase
7
8 var body: some Scene {
9 WindowGroup {
10 LoginView()
11 .environmentObject(auth)
12 .task { await auth.checkCredentialState() }
13 }
14 .onChange(of: scenePhase) { phase in
15 if phase == .active {
16 Task { await auth.checkCredentialState() }
17 }
18 }
19 }
20}
部署目标是 iOS 17 及以上的,onChange 可以改用两个参数的新写法 { _, phase in ... },旧写法会有弃用警告但不影响运行。UIKit 工程用 ASAuthorizationAppleIDButton 加 ASAuthorizationController,凭证处理部分和上面相同,写法见 Apple 的示例项目。
凭证里有什么
| 字段 | 内容 | 什么时候有 |
|---|---|---|
user |
用户标识,团队内唯一且不变 | 每次都有 |
email |
真实邮箱或转发邮箱 | 只在首次授权时有 |
fullName |
姓名,用户可以在授权框里改 | 只在首次授权时有 |
identityToken |
JWT,交给服务端验证 | 每次都有 |
authorizationCode |
一次性授权码,服务端拿它换 refresh token | 每次都有 |
realUserStatus |
是否像真人,likelyReal、unknown、unsupported |
只在首次授权时返回 |
用户停用授权后 App 怎么知道?
用户可以随时在手机设置里停用你的 App 的「通过 Apple 登录」,所以 App 要主动确认授权还在。getCredentialState(forUserID:completion:) 在 Swift 里可以写成 try await credentialState(forUserID:),这是一个本地调用,不走网络,可以放心在每次回到前台时调用。Apple 的验证用户一文建议把 App 的登录会话和这个调用的结果绑在一起。
| 状态 | 含义 | 处理 |
|---|---|---|
.authorized |
授权有效 | 继续使用 |
.revoked |
用户已撤销授权 | 退出登录,回到登录页 |
.notFound |
这个用户和你的 App 没有授权关系 | 退出登录,回到登录页 |
.transferred |
App 已转移到其他开发者团队 | 按 Apple 的团队转移流程迁移用户标识 |
App 在运行时,用户撤销授权,系统会发出 credentialRevokedNotification,代码里已经监听它并退出登录。用户永久删除 Apple 账户时系统不发这个通知,Apple 文档要求用 getCredentialState 来发现,所以前台检查不能省。
服务端怎么验证 identityToken?
App 拿到的凭证必须在服务端验证后才能建立登录会话。服务端要做三件事:验证 identityToken、用 authorizationCode 换 refresh token、在用户删除账号时撤销令牌。
验证 identityToken 的五项检查
| 检查 | 怎么做 |
|---|---|
| 签名 | 从 https://appleid.apple.com/auth/keys 取 Apple 的公钥集合,用 JWT 头里 kid 对应的那把公钥、按头里的 alg 验签 |
iss |
必须是 https://appleid.apple.com |
aud |
必须是你的 client_id,iOS App 就是它的 Bundle ID |
exp |
当前时间要早于过期时间 |
nonce |
等于 App 发来的原始随机串的 SHA-256 |
通过后再核对 sub 字段等于 App 发来的 user,以 sub 作为账号主键。检查项出自 Apple 的验证用户,公钥接口见获取 Apple 公钥。公钥集合里可能有多把,数量也会变,不要写死,按 kid 挑选并缓存一段时间。
创建 Sign in with Apple 私钥
服务端调用 Apple 的令牌接口要带 client_secret,它是一个用 Sign in with Apple 私钥签名的 JWT。
- 在 Certificates, Identifiers & Profiles 的 Keys 页点「+」,填名字,勾选 Sign in with Apple,点旁边的 Configure。
- 选择关联的主 App ID,保存后点 Continue、Register。
- 下载
.p8文件,记下 Key ID。这个文件同样只能下载一次。
每个主 App ID 最多关联两把私钥,见 Apple 的创建 Sign in with Apple 私钥。这把私钥和推送用的 APNs 密钥是两回事,推送密钥的区别见 推送用 .p8 还是 .p12。
client_secret 的 JWT 内容如下,用 ES256 签名。
1{
2 "alg": "ES256",
3 "kid": "你的 Key ID"
4}
5{
6 "iss": "你的 Team ID",
7 "iat": 1792000000,
8 "exp": 1807000000,
9 "aud": "https://appleid.apple.com",
10 "sub": "com.example.myapp"
11}
sub 填 client_id,也就是 Bundle ID,区分大小写。exp 最多比当前时间晚 15777000 秒(六个月),超过会被拒,规则见 Apple 的创建 client secret。可以每几个月在服务端重新生成一次。
用授权码换 refresh token
1curl -X POST "https://appleid.apple.com/auth/token" \
2 -H 'content-type: application/x-www-form-urlencoded' \
3 -d 'client_id=com.example.myapp' \
4 -d 'client_secret=上一步生成的 JWT' \
5 -d 'code=App 发来的 authorizationCode' \
6 -d 'grant_type=authorization_code'
成功时返回 access_token、refresh_token 和 id_token。把 refresh_token 和账号一起存在服务端,删除账号时要用它。授权码只能用一次,App 每次登录拿到的都是新的。接口说明见 Apple 的令牌验证。
Apple 允许你最多每天用 refresh token 验证一次用户的 Apple 账户是否仍然正常,调用太频繁会被限流。
用户删除账号时怎么调用 revoke?
用户在 App 里删除账号时,服务端删除数据后再调用撤销接口,让这个用户对你的 App 的授权失效。
1curl -X POST "https://appleid.apple.com/auth/revoke" \
2 -H 'content-type: application/x-www-form-urlencoded' \
3 -d 'client_id=com.example.myapp' \
4 -d 'client_secret=同样的 client_secret JWT' \
5 -d 'token=保存的 refresh_token' \
6 -d 'token_type_hint=refresh_token'
成功时返回 200,没有响应体,令牌之前已经失效的也返回 200。没存 refresh token 的,可以用 access token 撤销,token_type_hint 改成 access_token。接口见 Apple 的令牌撤销。撤销后,用户下次再用「通过 Apple 登录」进你的 App,会像第一次一样重新授权姓名和邮箱。
用户也可能在 App 外面停用授权或删除 Apple 账户。在 App ID 的 Sign in with Apple 配置里登记一个服务器通知地址,Apple 会在这些事件发生时推送通知,事件类型包括 consent-revoked(停用授权)、account-deleted(删除 Apple 账户)、email-disabled 和 email-enabled(转发邮箱开关)。格式见 Apple 的处理账户变更。
用户选了隐藏邮箱、你又要给他发邮件的,要先在开发者后台登记发件域名或发件地址,并配好 SPF 或 DKIM,否则转发服务不会投递。
怎么在 iPhone 上测试通过 Apple 登录?
第一次登录
- 在 KXBuild 视图的「构建配置」节点选 Debug,运行
KXBuild: 构建并运行,装到手机上。调试方法见 Windows 上真机调试 iOS App。 - 点「通过 Apple 登录」按钮,系统弹出授权框,显示姓名和邮箱选项。可以改名字,邮箱可以选「共享我的电子邮件」或「隐藏邮件地址」。
- 用面容 ID、触控 ID 或密码确认。App 显示「已登录」和姓名、邮箱。
在代码里 signIn(with:) 的开头打一个断点,能看到 email、fullName 有值。服务端还没写好时,可以先把 loginURL 的请求注释掉,直接保存 user,把 App 端跑通。
再次登录
点「退出登录」后再点按钮,这次授权框只有一个「继续」,断点里 email 和 fullName 都是 nil。这是正常行为,说明第一次就要把它们存好。
撤销授权后重新测试首次登录
想再看一次首次授权的完整流程,按 Apple 的管理使用「通过 Apple 登录」的 App操作。
- 打开手机「设置」,轻点最上方你的姓名。
- 轻点「通过 Apple 登录」。
- 在列表里选中你的 App,轻点「删除」,按提示确认。
- 回到 App,App 回到前台时
checkCredentialState()拿到.revoked,自动退出登录。 - 再点登录按钮,授权框又会出现姓名和邮箱选项。
只是删掉 App 重装,不会重置授权,用户标识和「只给一次」的规则都不变。
Sign in with Apple 常见报错怎么处理?
| 现象 | 原因 | 处理 |
|---|---|---|
点按钮后报错 ASAuthorizationError 1000(unknown) |
签名里没有 com.apple.developer.applesignin,多见于 App ID 没开能力、描述文件没重新生成,或 target 没用上 entitlements 文件 |
双击 .mobileprovision 确认有这项能力,在工程设置编辑器里搜索 CODE_SIGN_ENTITLEMENTS 确认路径,重新构建 |
| 系统提示去「设置」里登录 Apple 账户 | 手机没登录 Apple 账户,或账户没开双重认证 | 在「设置」里登录 Apple 账户并开启双重认证后再试 |
| 报 1001(canceled) | 用户点了取消,或授权框被关掉 | 不用提示,让用户再点一次 |
| 报 1004(failed)、1002(invalidResponse) | 授权过程失败或响应异常 | 稍后重试,网络受限时换网络 |
构建报描述文件不含 com.apple.developer.applesignin |
描述文件是开启能力前生成的 | 编辑描述文件后保存,重新下载替换旧文件 |
安装报 signed with invalid entitlements |
entitlements 里有这个键,描述文件里没有 | 同上,见签名报错 |
email、fullName 是 nil |
不是首次授权 | 按上文撤销授权后重测;正式 App 里从自己保存的数据读取 |
服务端验证 aud 不通过 |
client_id 写成了 Services ID 或别的 Bundle ID |
iOS App 登录时 aud 是 App 的 Bundle ID |
/auth/token 返回 invalid_client |
client_secret 过期、签名错、sub 和 client_id 不一致,或 Key ID、Team ID 填错 |
重新生成 client_secret,核对 kid、iss、sub |
/auth/token 返回 invalid_grant |
授权码已用过或已过期 | 每次登录都用 App 新拿到的授权码,收到后立即换取 |
错误码数值以 SDK 里的 ASAuthorizationError.Code 为准,接口错误的含义见 Apple 的 ErrorResponse。Sign in with Apple 之外的签名和描述文件报错见 iOS 签名报错与 ipa 安装失败。
常见问题
App 有微信登录,必须加「通过 Apple 登录」吗?
按审核指南 4.8,用第三方或社交登录建立主账号的 App,要同时提供一种只收集姓名和邮箱、允许隐藏邮箱、不做广告追踪的登录方式。「通过 Apple 登录」满足这些条件,是最常用的做法;只用自家账号体系、企业或教育账号等情况可以不加。
为什么第二次登录拿不到邮箱和姓名?
Apple 只在用户第一次授权你的 App 时返回姓名和邮箱,之后的登录只返回用户标识和令牌。第一次拿到就要保存并传给服务端,identityToken 里之后仍带邮箱,但不带姓名。
测试时怎么重置「通过 Apple 登录」的授权?
在 iPhone 的「设置 → 你的姓名 → 通过 Apple 登录」里选中 App,轻点「删除」。之后再登录,系统会重新询问姓名和邮箱。
用户删除账号时要做什么?
在 App 内提供删除账号入口,服务端删除数据后,用保存的 refresh token 调用 https://appleid.apple.com/auth/revoke 撤销授权。Apple 要求使用「通过 Apple 登录」的 App 这样做。
ASAuthorizationError 1000 是什么原因?
1000 是 unknown 错误,最常见的原因是签名里缺少 com.apple.developer.applesignin:App ID 没开能力、描述文件没重新生成,或 target 没设置 entitlements 文件。签名都正确仍然报错时,删掉 App 后用新描述文件重新构建安装再试。