Highlight
Apple 认证体验团队的 Garrett 全面介绍了 Passkey——一种基于 FIDO Alliance 标准的下一代认证技术。Passkey 在 macOS Ventura 和 iOS 16 中正式面向所有用户推出(去年是开发者预览)。
核心内容
密码登录有两个长期问题。用户很难为每个账号创建强密码,服务端又必须保存密码派生值。即使 App 和网站把流程做对,钓鱼、密码复用和凭证泄露仍然会让账号失守(00:33)。
Passkey 把登录凭据改成每个账号一对密钥。设备生成密钥对,公钥保存在服务端,私钥留在用户设备上。登录时,服务端发出一次性 challenge,设备用私钥在本地签名,服务端用公钥验证签名(26:06)。服务端没有私钥可泄露,用户也没有字符串可以被钓鱼网站骗走。
这场 Session 的重点是落地。Apple 建议把 passkey 放进现有用户名输入框和 AutoFill(自动填充)流程。用户看到的仍然是熟悉的登录表单;如果账号已有 passkey,QuickType bar 会直接给出建议,点一下并通过 Face ID 或 Touch ID 即可登录(09:15)。
详细内容
先把 App 和网站关联起来
(11:30)App 使用 passkey 前,需要配置 Associated Domains(关联域名),并在站点的 Apple App Site Association 文件里声明 webcredentials 服务。
{
"webcredentials": {
"apps": [ "A1B2C3D4E5.com.example.Shiny" ]
}
}
关键点:
webcredentials表示这个域名允许 App 使用网站凭据,包括密码和 passkey。apps数组写入 Team ID 加 Bundle ID,示例中的A1B2C3D4E5.com.example.Shiny对应 Shiny App。- 这一步让系统知道 App 与
example.com属于同一个登录域。
接着,在用户名输入框上标注 textContentType。系统用这个标记决定在哪里展示 passkey 建议(11:47)。
override func viewDidLoad() {
super.viewDidLoad()
//Additional setup…
userNameField.textContentType = .username
}
关键点:
viewDidLoad()是视图初始化的位置,适合设置输入框属性。super.viewDidLoad()保留 UIKit 的默认初始化流程。userNameField.textContentType = .username告诉系统这里是用户名字段,passkey 和密码建议会出现在这个输入位置。
用 AutoFill 启动 passkey 登录
(11:59)原生 App 通过 AuthenticationServices 里的 ASAuthorizationPlatformPublicKeyCredentialProvider 发起 passkey 请求。Apple 建议尽早启动 AutoFill-assisted request,让用户聚焦用户名字段时就能看到建议(13:05)。
// AutoFill-assisted passkey request
func signIn() {
let challenge: Data = … // Fetched from server
let provider =
ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com")
let request =
provider.createCredentialAssertionRequest(
challenge: challenge)
let controller =
ASAuthorizationController(
authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
// Start the request
controller.performAutoFillAssistedRequests()
}
关键点:
challenge必须从服务端获取,对应 WebAuthn 登录请求里的单次挑战。ASAuthorizationPlatformPublicKeyCredentialProvider是处理 passkey 请求的 provider。relyingPartyIdentifier: "example.com"指向 passkey 所属的登录域。createCredentialAssertionRequest(challenge:)创建登录用的 assertion request。ASAuthorizationController负责执行授权请求。delegate接收成功或失败回调。presentationContextProvider告诉系统授权界面应该挂在哪个窗口上。performAutoFillAssistedRequests()启动 AutoFill 辅助请求;用户选择 QuickType bar 中的 passkey 后,系统触发 Face ID 或 Touch ID。
成功后,不会把任何内容填进用户名输入框。App 会收到统一的 ASAuthorizationControllerDelegate 回调,并拿到 passkey assertion(13:29)。
// Completing a passkey sign in
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
guard let passkeyAssertion = authorization.credential as?
ASAuthorizationPlatformPublicKeyCredentialAssertion
else { … }
let signature = passkeyAssertion.signature
let clientDataJSON = passkeyAssertion.rawClientDataJSON
// Pass these values to your server, and complete the sign in
…
}
关键点:
didCompleteWithAuthorization是所有ASAuthorization成功后的统一入口。authorization.credential需要先转成ASAuthorizationPlatformPublicKeyCredentialAssertion。signature是设备用私钥对 challenge 生成的签名。rawClientDataJSON是 WebAuthn 验证需要的客户端数据。- App 不在本地判断登录成功,而是把这些值传给服务端完成 WebAuthn 验证。
用户输入用户名后,改用模态登录
(15:30)如果用户没有点 AutoFill 建议,而是手动输入了用户名,Apple 建议取消 AutoFill 请求,再弹出模态 passkey 登录表。代码变化很小,只需要把最后一步换成 performRequests()。
// Modal passkey request
func signIn() {
let challenge: Data = … // Fetched from server
let provider =
ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com")
let request =
provider.createCredentialAssertionRequest(
challenge: challenge)
let controller =
ASAuthorizationController(
authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
// Start the request
controller.performRequests()
}
关键点:
- 前半段仍然是同一个 challenge、provider 和 assertion request。
performRequests()会展示模态表单,列出可用 passkey。- 这个表单也会提供使用附近设备 passkey 的入口。
- 成功后仍然走同一个 delegate 回调,因此后端验证逻辑可以复用。
当设备上有多个 passkey 时,可以根据已输入用户名从服务端查出对应 credential ID,再设置 allow list(20:55)。
// Modal request with allow list
func signIn(userName: String) {
let challenge: Data = … // Fetched from server
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier:"example.com")
let request = provider.createCredentialAssertionRequest(
challenge: challenge)
let credentialIDs: [Data] = … // Fetched from server for provided userName
request.allowedCredentials = credentialIDs.map(
ASAuthorizationPlatformPublicKeyCredentialDescriptor.init(credentialID:))
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
// Start the request
controller.performRequests()
}
关键点:
userName是用户已经输入的账号名。credentialIDs来自服务端;WebAuthn 服务端应能按用户名查到 credential ID。allowedCredentials限制模态表单只展示匹配的 passkey。ASAuthorizationPlatformPublicKeyCredentialDescriptor.init(credentialID:)把原始 ID 转成请求需要的 descriptor。- 这个模式适合用户已经提供账号上下文的场景。
没有本机 passkey 时,决定是否静默回退
(22:56)默认的模态请求在没有本机 passkey 时,会显示二维码,让用户用附近设备登录。若你只是想先试探本机是否有可用凭据,可以使用 .preferImmediatelyAvailableCredentials。没有匹配凭据时,系统通过错误回调静默回退。
// Modal passkey request, silent fallback
func signIn() {
let challenge: Data = … // Fetched from server
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier:"example.com")
let request = provider.createCredentialAssertionRequest(
challenge: challenge)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
// Start the request
controller.performRequests(options: .preferImmediatelyAvailableCredentials)
}
关键点:
performRequests(options:)使用同一个 passkey 请求,只改变展示策略。.preferImmediatelyAvailableCredentials要求系统优先使用当前设备上立即可用的凭据。- 如果没有可用凭据,系统不会直接展示跨设备二维码,而是进入错误回调。
- Apple 提醒:App 仍应在其他地方提供 AutoFill 请求或默认模态请求,让附近设备登录入口可达(23:48)。
// Handling a silent fallback
func authorizationController(controller: ASAuthorizationController,
didCompleteWithError error: Error) {
guard let error = error as? ASAuthorizationError else { … }
if error.code == .canceled {
// Either the user canceled the sheet, or there were no credentials available.
showSignInForm()
}
}
关键点:
didCompleteWithError是授权失败或取消时的 delegate 回调。ASAuthorizationError提供结构化错误码。.canceled有两种来源:用户关闭了表单,或.preferImmediatelyAvailableCredentials没有找到本机凭据。showSignInForm()是回退路径,用来展示传统登录表单。
Web 端用条件 mediation 接入 AutoFill
(16:53)Web 端使用标准 WebAuthn API。第一步是在用户名输入框上同时声明 username 和 webauthn 自动填充 token。
<input type="text" id="username-field" autocomplete="username webauthn" >
关键点:
autocomplete="username webauthn"让浏览器在用户名字段上展示密码和 passkey 建议。- 这个标记对应 App 里的
.usernametext content type。 - 这是 AutoFill-assisted WebAuthn 请求显示在正确位置的前提。
接着,在 JavaScript 中检测条件 mediation 是否可用,并用 navigator.credentials.get 发起请求(17:09)。
// AutoFill-assisted WebAuthn request (JavaScript)
function signIn() {
if (!PublicKeyCredential.isConditionalMediationAvailable ||
!PublicKeyCredential.isConditionalMediationAvailable()) {
// Browser doesn't support AutoFill-assisted requests.
return;
}
const options = {
"publicKey": {
challenge: … // Fetched from server
},
mediation: "conditional"
};
navigator.credentials.get(options)
.then(assertion => {
// Pass the assertion to your server.
});
}
关键点:
PublicKeyCredential.isConditionalMediationAvailable是标准特性检测入口。- 检测失败时直接返回,避免在不支持的浏览器上发起错误流程。
challenge同样来自服务端。mediation: "conditional"把 WebAuthn 请求变成 AutoFill 辅助模式。navigator.credentials.get(options)返回 Promise。assertion需要传回服务端验证,浏览器端不保存登录结论。
如果要展示模态 WebAuthn 表单,移除 mediation: "conditional" 即可(18:14)。
// Modal WebAuthn request (JavaScript)
function signIn() {
var options = {
"publicKey": {
challenge: … // Fetched from server
}
};
navigator.credentials.get(options)
.then(function (assertion) {
// Pass the assertion to your server.
});
}
关键点:
options.publicKey.challenge仍然是服务端生成的 WebAuthn challenge。- 没有
mediation参数时,浏览器展示模态请求。 - Apple 建议模态 WebAuthn 请求由用户手势触发,例如点击按钮(19:25)。
- 返回的
assertion与 AutoFill 流程一样,传回服务端验证。
多种凭据可以放在同一个授权请求里
(24:40)现实迁移不会一步完成。用户可能有 passkey、密码、Sign in with Apple 中的一种或多种凭据。ASAuthorizationController 可以在同一个模态表单里请求多种凭据。
// Combined credential modal request
func signIn() {
let challenge: Data = … // Fetched from server
let passkeyProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier:"example.com")
let passkeyRequest = passkeyProvider.createCredentialAssertionRequest(
challenge: challenge)
let passwordRequest = ASAuthorizationPasswordProvider().createRequest()
let signInWithAppleRequest = ASAuthorizationAppleIDProvider().createRequest()
let controller = ASAuthorizationController(
authorizationRequests: [passkeyRequest, passwordRequest, signInWithAppleRequest])
controller.delegate = self
controller.presentationContextProvider = self
// Start the request
controller.performRequests()
}
关键点:
passkeyRequest处理已有 passkey 登录。ASAuthorizationPasswordProvider().createRequest()请求保存的密码凭据。ASAuthorizationAppleIDProvider().createRequest()请求 Sign in with Apple 凭据。authorizationRequests数组把三种请求交给同一个 controller。- 系统表单只展示当前设备上可用的凭据。
回调里需要按凭据类型分发到不同处理逻辑(25:02)。
// Completing a combined credential request
func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
switch authorization.credential {
case let passkeyAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
finishSignIn(with: passkeyAssertion)
case let signInWithAppleCredential as ASAuthorizationAppleIDCredential:
finishSignIn(with: signInWithAppleCredential)
case let passwordCredential as ASPasswordCredential:
finishSignIn(with: passwordCredential)
default:
// Handle other credential types
break
}
}
关键点:
switch authorization.credential用运行时类型判断用户选择了哪种凭据。ASAuthorizationPlatformPublicKeyCredentialAssertion走 passkey 验证。ASAuthorizationAppleIDCredential走 Sign in with Apple 登录。ASPasswordCredential走密码登录。default保留未来或其他凭据类型的处理入口。
跨设备登录由系统完成
(28:33)Passkey 支持在没有本机凭据的设备上登录。客户端显示二维码,保存 passkey 的设备扫描二维码,随后两台设备完成本地密钥协商,并通过蓝牙证明物理邻近。
这个流程有几个安全边界:二维码里编码了一对一次性加密密钥;手机广播包含 relay server 路由信息;本地交换完成后,网络通信端到端加密,relay server 读不到内容;网站不参与跨设备通信(29:49)。因此,把二维码远程发给受害者或在假网站生成二维码,无法完成蓝牙邻近证明。
核心启发
-
做什么:在现有登录页加入 passkey AutoFill。 为什么值得做:Session 明确建议把 passkey 放进用户名字段,而不是新做一套登录界面。 怎么开始:配置
webcredentials关联域名,为用户名输入框设置.username,启动performAutoFillAssistedRequests()。 -
做什么:用户输入用户名后只展示对应账号的 passkey。 为什么值得做:设备上可能保存多个账号;allow list 可以减少选错账号的机会。 怎么开始:服务端按用户名返回 credential ID,客户端把它们映射为
ASAuthorizationPlatformPublicKeyCredentialDescriptor并设置到request.allowedCredentials。 -
做什么:App 启动时先尝试本机凭据,再展示传统登录表单。 为什么值得做:有本机凭据的用户可以直接进入系统表单;没有凭据的用户不会先看到二维码。 怎么开始:调用
performRequests(options: .preferImmediatelyAvailableCredentials),在.canceled回调里展示登录表单。 -
做什么:把 passkey、密码和 Sign in with Apple 放进同一个模态表单。 为什么值得做:迁移期用户的凭据类型不一致,统一请求可以减少入口分裂。 怎么开始:创建
passkeyRequest、passwordRequest、signInWithAppleRequest,一起传给ASAuthorizationController。 -
做什么:为网站登录加入条件 mediation。 为什么值得做:Web 端也能把 passkey 建议显示在用户名字段,用户不用先选择复杂的登录方式。 怎么开始:给输入框设置
autocomplete="username webauthn",检测PublicKeyCredential.isConditionalMediationAvailable(),再调用带mediation: "conditional"的navigator.credentials.get()。
关联 Session
- Enhance your Sign in with Apple experience — 同样基于
ASAuthorization,讲账号去重、凭据查询和 Sign in with Apple 字段处理。 - Replace CAPTCHAs with Private Access Tokens — 讨论用设备和账号信任能力替代传统 Web 安全挑战。
- Improve DNS security for apps and servers — 从网络层减少钓鱼和劫持风险,适合与 passkey 登录一起考虑。
- Discover Managed Device Attestation — 讲服务端如何验证受管理设备身份,与凭据登录同属访问控制基础设施。
评论
GitHub Issues · utterances