WWDC Quick Look 💓 By SwiftGGTeam
Meet passkeys

Meet passkeys

观看原视频

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。第一步是在用户名输入框上同时声明 usernamewebauthn 自动填充 token。

<input type="text" id="username-field" autocomplete="username webauthn" >

关键点:

  • autocomplete="username webauthn" 让浏览器在用户名字段上展示密码和 passkey 建议。
  • 这个标记对应 App 里的 .username text 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 放进同一个模态表单。 为什么值得做:迁移期用户的凭据类型不一致,统一请求可以减少入口分裂。 怎么开始:创建 passkeyRequestpasswordRequestsignInWithAppleRequest,一起传给 ASAuthorizationController

  • 做什么:为网站登录加入条件 mediation。 为什么值得做:Web 端也能把 passkey 建议显示在用户名字段,用户不用先选择复杂的登录方式。 怎么开始:给输入框设置 autocomplete="username webauthn",检测 PublicKeyCredential.isConditionalMediationAvailable(),再调用带 mediation: "conditional"navigator.credentials.get()

关联 Session

评论

GitHub Issues · utterances