关于 axiom-passkeys
面向 iOS 开发者的 Passkeys 认证指南,涵盖注册、登录、AutoFill 和密码用户迁移。它也说明关联域配置及结合通行密钥、密码和 Sign in with Apple 的登录流程。
主要功能
- 01实现通行密钥注册与断言
- 02配置 AutoFill 辅助登录
- 03在 iOS 18+ 请求自动升级
- 04组合通行密钥、密码和 Apple 登录
- 05设置关联域与 AASA 文件
- 06处理跨设备通行密钥登录
适用场景
- 01为 iOS 应用添加通行密钥登录
- 02排查 QuickType 栏未显示通行密钥
- 03逐步将密码用户迁移到通行密钥
- 04同时支持多种凭据的登录流程
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-passkeys安装后在 Claude Code 中输入/axiom-passkeys确认已加载。
详细介绍
密钥通行证
适用于 iOS 应用的密钥通行证身份验证——注册、断言、借助自动填充的请求、自动升级、组合凭据流程,以及从基于密码的身份验证迁移。
何时使用此技能
在以下情况下使用:
- ☑ 为 iOS 应用添加密钥通行证登录
- ☑ 将基于密码的身份验证替换为密钥通行证
- ☑ 为密钥通行证注册或断言配置 ASAuthorizationController
- ☑ 设置借助自动填充的密钥通行证请求(QuickType 栏)
- ☑ 为现有密码用户添加自动密钥通行证升级(iOS 18+)
- ☑ 支持组合凭据请求(密钥通行证 + 密码 + 使用 Apple 登录)
- ☑ 为 webauthn/webcredentials 配置关联域
- ☑ 调试密钥通行证断言失败或 QuickType 建议缺失
示例提示词
“如何为我的应用添加密钥通行证登录?” “我的密钥通行证没有显示在 QuickType 栏中” “如何将现有密码用户迁移到密钥通行证?” “ASAuthorizationError.canceled——出了什么问题?” “迁移期间如何同时支持密钥通行证和密码?” “如何为密钥通行证设置关联域?” “performRequests 和 performAutoFillAssistedRequests 有什么区别?” “如何在 iOS 18 上添加自动密钥通行证升级?”
风险信号
以下迹象表明你可能走错了方向:
- ❌ 在密钥通行证可用时仍将密码用作主要身份验证方式——密钥通行证不是“额外的安全措施”,而是替代方案。每一次仅使用密码的登录,都是你留下的一个钓鱼机会。
- ❌ 未将用户名字段的 textContentType 标注为
.username——没有此标注,系统就无法将该字段与密钥通行证凭据关联。对于未标注的字段,自动填充不会建议密钥通行证。 - ❌ 在主要登录流程中使用
performRequests()——这会显示模态弹窗,而不是将密钥通行证放入 QuickType 栏。主要流程应使用performAutoFillAssistedRequests()。将performRequests()留给注册和用户明确点按“登录”按钮的场景。 - ❌ 在服务器上将
userVerification设为"required"——这会导致没有生物识别功能的设备无法登录。平台会根据设备情况妥善处理验证。请使用"preferred"(默认值)。 - ❌ 在扩展或非主应用环境中创建密钥通行证——创建密钥通行证需要主应用目标。扩展可以执行断言,但不能注册。
- ❌ 不支持组合凭据请求——迁移期间,用户可能拥有密钥通行证、密码,或两者都没有。单个 ASAuthorizationController 可以处理这三种情况。只提供密钥通行证会让尚未升级的用户无法登录。
为什么选择密钥通行证
密钥通行证不是对密码的渐进式改进,而是替代密码的一种架构。
防钓鱼:每个密钥通行证都以加密方式绑定到特定域名。secure-myapp.com 上的虚假登录页面无法触发为 myapp.com 创建的密钥通行证。用户没有需要输入到错误网站的凭据。
无需担心凭据数据库泄露:服务器只存储公钥。发生泄露也不会暴露可利用的信息——没有可破解的密码哈希,也没有可重放的共享密钥。
轻点一次即可登录:Face ID 或 Touch ID 取代手动输入。注册和断言都只需轻点一次。
FIDO Alliance 标准:采用 WebAuthn/CTAP2 协议。适用于 Apple、Google 和 Microsoft 平台。在 iPhone 上创建的密钥通行证会通过 iCloud Keychain 同步,并可在 Mac、iPad 和 Web 上使用。
普及情况:Apple 将密钥通行证作为系统的一等功能提供。iCloud Keychain 会同步密钥通行证。Passwords 应用负责管理它们。自 iOS 17 起,第三方凭据管理器(1Password、Dashlane)也已原生支持密钥通行证。
关联域设置
密钥通行证需要关联域,以将你的应用与服务器关联起来。没有关联域,系统就不会为你的应用提供密钥通行证。
1. 添加权利
在 Xcode 中:Target > Signing & Capabilities > + Associated Domains。
添加:
webcredentials:example.com
2. 托管 AASA 文件
通过 HTTPS 从你的域名提供 /.well-known/apple-app-site-association,并设置 Content-Type: application/json:
{
"webcredentials": {
"apps": [
"TEAMID.com.example.myapp"
]
}
}
要求:
- 使用有效证书通过 HTTPS 提供(不能是自签名证书)
- 不得重定向——文件必须直接在该路径提供
TEAMID是你的 Apple Developer Team ID,而不是 bundle ID 前缀- 文件必须位于根域名下,不能放在子目录中
3. 验证
curl -s "https://example.com/.well-known/apple-app-site-association" | python3 -m json.tool
Apple 的 CDN 会缓存 AASA 文件。更改最多可能需要 24 小时才能生效。开发期间,请在设备的 Developer Settings 中启用 Associated Domains Development,并使用 ?mode=developer 查询参数。
注册流程
注册会创建一个新的通行密钥,并将其存储在用户的凭证管理器中。
import AuthenticationServices
func registerPasskey(challenge: Data, userName: String, userID: Data) {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialRegistrationRequest(
challenge: challenge,
name: userName,
userID: userID
)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
关键参数:
relyingPartyIdentifier— 必须与你的关联域名匹配(不带https://前缀)challenge— 由服务器生成的加密质询(至少使用 16 个随机字节)。切勿重复使用质询。name— 在通行密钥提示词和 Passwords app 中向用户显示的名称userID— 用户账户的不透明标识符。不要使用电子邮件或用户名——请使用随机 UUID 或服务器端用户 ID
处理注册响应
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credential = authorization.credential
as? ASAuthorizationPlatformPublicKeyCredentialRegistration else { return }
let attestationObject = credential.rawAttestationObject
let clientDataJSON = credential.rawClientDataJSON
let credentialID = credential.credentialID
// Send attestationObject, clientDataJSON, credentialID to your server
// Server validates and stores the public key
}
注册使用 performRequests()(模态),因为用户明确选择创建通行密钥。这是适合使用模态展示的唯一场景。
断言流程(登录)
登录有两种路径,分别适用于不同的 UX 场景。
AutoFill 辅助(首选路径)
推荐的登录流程。当用户轻点内容类型为 .username 的文本字段时,通行密钥会显示在 QuickType 栏中。轻点一次即可登录,不会被模态界面打断。
func signInWithAutoFill() {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialAssertionRequest(
challenge: serverChallenge
)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performAutoFillAssistedRequests()
}
关键细节:
- 尽早调用
performAutoFillAssistedRequests()——在用户聚焦用户名字段之前。请在viewDidAppear中或登录视图出现时调用。 - 用户名
UITextField必须设置.textContentType = .username。否则,QuickType 栏不会显示通行密钥建议。 - 不要在请求上设置
allowedCredentials。AutoFill 需要显示该域名下的所有可用通行密钥。 - 请求会一直保持有效,直到用户选择凭证、离开当前页面或取消请求。
模态(回退路径)
当用户明确轻点“登录”按钮,或你已知用户名并希望请求特定凭证时使用。
func signInWithModal(allowedCredentials: [ASAuthorizationPlatformPublicKeyCredentialDescriptor]? = nil) {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialAssertionRequest(
challenge: serverChallenge
)
if let allowedCredentials {
request.allowedCredentials = allowedCredentials
}
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
当你知道用户的凭证 ID 时使用 allowedCredentials(例如,用户输入了用户名,且你的服务器返回了其已注册的凭证 ID)。这会将通行密钥选择范围缩小到该账户。
处理断言响应
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credential = authorization.credential
as? ASAuthorizationPlatformPublicKeyCredentialAssertion else { return }
let signature = credential.signature
let clientDataJSON = credential.rawClientDataJSON
let authenticatorData = credential.rawAuthenticatorData
let credentialID = credential.credentialID
let userID = credential.userID
// Send to server for verification
}
自动升级通行密钥(iOS 18+)
在不中断用户流程的情况下,静默将使用密码的用户升级为通行密钥。系统会显示简短通知,确认升级已完成——无需模态界面,也无需额外轻点。
工作原理
当用户使用密码登录时,系统可以为同一账户自动创建通行密钥。以下条件必须同时满足:
- 凭证管理器支持自动升级
- 用户刚刚使用密码成功验证了同一账户
- 你的应用请求条件式注册
实现
func requestAutomaticUpgrade(challenge: Data, userName: String, userID: Data) {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialRegistrationRequest(
challenge: challenge,
name: userName,
userID: userID
)
request.requestStyle = .conditional
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performAutoFillAssistedRequests()
}
关键细节:.requestStyle = .conditional 会使注册以机会式方式进行。条件满足时会静默成功,不满足时会静默失败。不要将失败回调视为错误——这表示此次未满足条件。
调用时机:用户使用密码成功验证身份后。先检查用户是否已为此账户设置通行密钥——如果已设置,就不要请求升级。
服务器要求
你的服务器必须能够处理在密码登录后不久到达的异步注册。userID 和 challenge 必须有效,并且与该会话相关联。
组合凭证请求
迁移期间,用户可能使用通行密钥、密码或“通过 Apple 登录”凭证。一个 ASAuthorizationController 即可处理这三种凭证。
func signInWithCombinedRequest() {
var requests: [ASAuthorizationRequest] = []
// Passkey assertion
let passkeyProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
requests.append(
passkeyProvider.createCredentialAssertionRequest(challenge: serverChallenge)
)
// Password
let passwordProvider = ASAuthorizationPasswordProvider()
requests.append(passwordProvider.createRequest())
// Sign in with Apple
let appleIDProvider = ASAuthorizationAppleIDProvider()
requests.append(appleIDProvider.createRequest())
let controller = ASAuthorizationController(authorizationRequests: requests)
controller.delegate = self
controller.presentationContextProvider = self
controller.performAutoFillAssistedRequests()
}
处理多种凭证类型
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
switch authorization.credential {
case let credential as ASAuthorizationPlatformPublicKeyCredentialAssertion:
// Passkey sign-in — verify with server
handlePasskeyAssertion(credential)
case let credential as ASPasswordCredential:
// Password sign-in — verify, then offer passkey upgrade
handlePasswordSignIn(credential)
case let credential as ASAuthorizationAppleIDCredential:
// Apple ID sign-in
handleAppleIDSignIn(credential)
default:
break
}
}
密码登录成功后,调用自动升级流程,逐步将用户迁移到通行密钥。
跨设备登录
用户可以使用手机作为身份验证器,在没有通行密钥的设备上登录。
工作原理:
- App 发起通行密钥断言请求
- 系统在发起登录请求的设备上显示二维码
- 用户用手机扫描二维码(手机上有通行密钥)
- 通过蓝牙近距离验证,确认手机就在附近
- 用户在手机上通过 Face ID/Touch ID 完成身份验证
- 断言在原设备上完成
无需更改 App。这是系统级功能。任何支持通行密钥的设备都可以作为跨设备身份验证器。通信通过 Apple 中继服务进行端到端加密。
需要蓝牙:两台设备都必须启用蓝牙。近距离检查可防止远程网络钓鱼——身份验证设备必须在发起请求的设备附近。
迁移策略
阶段 1 — 在保留密码的同时添加通行密钥支持
保留现有的密码身份验证。添加通行密钥注册和断言。使用组合凭证请求,让两种方式都能正常使用。
服务端更改:添加用于注册和断言的 WebAuthn 端点。将公钥与密码哈希一起存储。两种身份验证方式都验证到同一个用户会话。
App 更改:实现注册流程(在密码登录后提供)、断言流程(借助自动填充)和组合请求。
阶段 2 — 自动升级(iOS 18+)
在密码登录后添加条件注册请求。用户会逐步、无感地迁移到通行密钥。跟踪升级指标以衡量采用情况。
无需用户操作。系统会透明地处理升级。
阶段 3 — 减少易遭网络钓鱼的验证因素
对于使用通行密钥的账户,可考虑:
- 移除密码重置流程(通行密钥不需要重置)
- 停用短信双重验证(通行密钥本身就是双重验证:设备持有权 + 生物识别)
- 提供通过另一台设备上的通行密钥恢复账户的方式,而不是通过电子邮件/短信恢复
不要强制移除密码。让用户自行选择是否改用无密码登录。有些用户需要在不支持通行密钥的设备上使用密码登录。
密码 App 集成(iOS 18+)
密码 App 会使用关联域名中的 OpenGraph 元数据来显示你 App 的名称和图标。在网站的 <head> 中添加:
<meta property="og:title" content="MyApp" />
<meta property="og:image" content="https://example.com/icon.png" />
用户的凭证管理器会以这种方式显示你的 App。如果没有这些元数据,密码 App 只会显示域名。
反驳借口
| 借口 | 事实 | 时间成本 |
|---|---|---|
| “目前用密码就够了” | 每次密码登录都存在网络钓鱼风险。撞库攻击会造成实际经济损失——数据泄露的平均成本为 450 万美元。通行密钥可以彻底消除整个攻击面。 | 持续承担风险,而实施只需 2-3 天 |
| “以后再添加通行密钥” | 借助自动填充功能发起通行密钥请求,所需的集成工作量与自定义密码文本字段加自动填充相同。推迟实施并不能节省时间。 | 无论何时实施,工作量都相同 |
| “用户不会理解通行密钥” | 用户无需理解公钥加密。他们看到的是“使用 Face ID 登录”——点一下就行。Apple、Google 和 Microsoft 正在所有平台上将通行密钥设为默认方式。 | 无需额外进行用户教育 |
| “我们的服务器不支持 WebAuthn” | 所有主流后端(Python、Node、Go、Ruby、Java、.NET)都有服务端 WebAuthn 库。多数库都经过充分测试,并且在积极维护。 | 服务端集成需 1-2 天 |
| “没有生物识别功能的用户怎么办?” | 设备密码也是有效的用户验证方式。所有受支持的设备至少都有设备密码。将 userVerification 设为 "preferred",平台就会妥善处理。 | 无需额外工作——平台会处理 |
| “我们必须永久保留密码作为备用方式” | 组合凭据请求可以同时支持密码和通行密钥。使用自动升级逐步迁移。你可以无限期保留密码,同时让通行密钥成为主要方式。 | 无需强制二选一——两种方式并行运行 |
应对质疑的场景
场景 1:“我们的用户还没准备好使用通行密钥”
背景:产品经理以用户可能感到困惑为由,反对采用通行密钥。
质疑:“我们的用户不懂技术。他们不会理解什么是通行密钥。我们先继续用密码,明年再添加通行密钥。”
事实:Apple 将通行密钥作为内置系统功能,提供给所有平台——iPhone、iPad、Mac、Apple Watch,以及通过跨设备认证使用的 Windows。用户会在 QuickType 栏中看到“使用 Face ID 登录”。他们不会看到“WebAuthn CTAP2 公钥凭据”。“密码”应用会透明地将通行密钥与密码一并管理。Apple 自家的账户系统、Google 账户和 Microsoft 账户都使用通行密钥。你的用户已经在其他地方使用它们了。
正确做法:实现组合凭据请求。现有密码用户继续使用密码登录。对于凭据管理器支持通行密钥的用户,系统会自动显示通行密钥。添加自动升级(iOS 18+),无需用户操作即可逐步迁移。
回应模板:“用户无需理解通行密钥。他们看到的是‘使用 Face ID 登录’——点一下就完成。Apple、Google 和 Amazon 已经在自家的登录流程中使用通行密钥。我们会在密码之外添加通行密钥,因此不会改变任何人的使用流程。自动获得通行密钥的用户会享受到更好的体验;其他用户则可以照常使用。”
场景 2:“先上线密码认证,之后再添加通行密钥”
背景:新应用面临截止日期压力。开发者想把通行密钥支持推迟到上线后的更新中。
压力:“我们得在周五前上线。密码认证已经能用了。下个迭代再添加通行密钥。”
实际情况:实现由 AutoFill 辅助的通行密钥请求,其工作量与构建一个完善的密码文本字段(支持 AutoFill、安全存储和“忘记密码”流程)相当。无论如何,你都要构建 ASAuthorizationController 集成——问题在于你要接入一个提供方(密码),还是三个(通行密钥 + 密码 + Apple ID)。组合请求只需在代理中增加约 30 行代码。
正确做法:从一开始就实现组合凭据请求。服务器需要 WebAuthn 端点,但客户端的工作几乎完全相同。从第一天起就支持通行密钥上线,意味着你永远不必事后补上它,也可以避免“下个迭代”变成“下个季度”。
反驳模板:“由 AutoFill 辅助的通行密钥使用的 ASAuthorizationController,与我们用于密码 AutoFill 的相同。添加通行密钥支持只需在代理中增加约 30 行代码,不需要一个迭代的工作量。不支持它就意味着我们现在构建密码流程,以后还要重建认证流程来添加通行密钥。我们一次性做好吧。”
检查清单
上线通行密钥认证前:
关联域:
- 已将
webcredentials:yourdomain.com添加到 Associated Domains 功能中 - AASA 文件通过 HTTPS 在
/.well-known/apple-app-site-association提供 - AASA 文件包含正确的 Team ID 和 bundle identifier
- AASA 文件路径没有重定向
- AASA 更改已传播(或已为测试启用开发者模式)
注册:
- 服务器为每次注册尝试生成唯一的质询
-
userID不包含可识别信息(不是电子邮件或用户名) -
relyingPartyIdentifier与关联域完全匹配 - 注册使用
performRequests()(模态方式——适用于显式创建) - 注册成功后,服务器会存储 credentialID 和公钥
断言(登录):
- 用户名文本字段设置了
.textContentType = .username - 由 AutoFill 辅助的请求尽早调用(在字段获得焦点前)
- AutoFill 路径使用
performAutoFillAssistedRequests() - 可通过
performRequests()提供模态备用方案,用于显式登录按钮 - 由 AutoFill 辅助的请求未设置
allowedCredentials - 服务器会验证签名、authenticatorData 和 clientDataJSON
组合请求(如果支持多种认证方式):
- 包含通行密钥、密码和 Apple ID 提供方
- 代理通过 switch 语句处理所有凭据类型
- 密码登录会触发自动通行密钥升级流程
自动升级(iOS 18+):
- 注册请求使用
.requestStyle = .conditional - 升级请求使用
performAutoFillAssistedRequests() - 将失败回调视为“未满足条件”,而不是错误
- 请求升级前检查是否已有通行密钥
错误处理:
- 妥善处理
ASAuthorizationError.canceled(用户已关闭——不是错误) - 记录带有上下文的
ASAuthorizationError.failed,以便调试 - 服务器验证期间发生网络故障时,不会导致认证状态不一致
资源
WWDC:2022-10092、2024-10125
文档:/authenticationservices、/authenticationservices/public-private-key-authentication/supporting-passkeys
技能:axiom-keychain