Web 下一代免密登录技术 Passkeys 介绍

7 min read

用户登录一般都是使用账号密码登录,但是使用密码登录有一些问题一直无法解决,比如安全问题和不同设备密码同步以及需要借助密码管理器帮助管理不同密码。于是目前业界提出了 Passkeys 的标准,可以利用设备生物认证方便的注册和重复登录,不需要单纯依靠记忆力或者密码管理器管理不同密码,支持在不同设备间同步,并且得到了 Apple、Google、Microsoft 等大厂的支持.

示例

以下示例使用 https://webauthn.io/ (opens in a new tab)

注册时生成 Passkeys

passkeys-register

使用 Passkeys 登录

passkeys-login

Web 页面中如何实现

创建

参考 web.dev 上的文档 Create a passkey for passwordless logins (opens in a new tab)

passkeys-create

为用户创建 Passkey 的流程大致如下:

  1. 判断当前设备是否支持
  2. 用户点击创建 Passkey 按钮,调用 navigator.credentials.create() API
  3. 调用 navigator.credentials.create() API之后会调用用户设备的生物识别,如果成功会返回公钥
  4. 将返回的公钥和用户信息传给后端进行存储

对应的示例代码如下:

判断当前设备是否支持

// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `​​isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.​​isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.​​isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

用户点击创建 Passkey 按钮,调用 navigator.credentials.create() API

const publicKeyCredentialCreationOptions = {  
  challenge: *****,  
  rp: {  
    name: "Example",  
    id: "example.com",  
  },  
  user: {  
    id: *****,  
    name: "john78",  
    displayName: "John",  
  },  
  pubKeyCredParams: [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}],  
  excludeCredentials: [{  
    id: *****,  
    type: 'public-key',  
    transports: ['internal'],  
  }],  
  authenticatorSelection: {  
    authenticatorAttachment: "platform",  
    requireResidentKey: true,  
  }  
};
 
const credential = await navigator.credentials.create({  
  publicKey: publicKeyCredentialCreationOptions  
});
 
// Encode and send the credential to the server for verification.  

将返回的公钥和用户信息传给后端进行存储。

登录

参考 web.dev 上的文档 Sign in with a passkey through form autofill (opens in a new tab)

passkeys-get

用户使用已有 Passkey 登录的流程大致如下:

  1. 调用 navigator.credentials.get() API 获取对应用户的 Passkey
  2. 用户设备的生物识别成功后会返回对应的公钥
  3. 将返回的公钥传给后端验证

示例代码如下:

输入框中的 autocomplete 属性中加入 webauth 可以方便用户选择已有的 Passkey

<input type="text" name="username" autocomplete="username webauthn" ...>

调用 navigator.credentials.get() API 获取对应用户的 Passkey

// To abort a WebAuthn call, instantiate an `AbortController`.  
const abortController = new AbortController();
 
const publicKeyCredentialRequestOptions = {  
  // Server generated challenge  
  challenge: ****,  
  // The same RP ID as used during registration  
  rpId: 'example.com',  
};
 
const credential = await navigator.credentials.get({  
  publicKey: publicKeyCredentialRequestOptions,  
  signal: abortController.signal,  
  // Specify 'conditional' to activate conditional UI  
  mediation: 'conditional'  
});  

将返回的公钥传给后端验证,这一步在后端服务中处理。校验成功后,用户即可登录成功。

借助 SimpleWebAuthn 或者 Hanko 第三方库方便快速的实现

由于使用 Passkeys 涉及 API 较多,同时涉及前后端,我们可以使用社区封装的 SimpleWebAuthn 或者 Hanko 来简化接入的过程。他们的示例代码可以在以下链接找到:

SimpleWebAuthn 为例,我们前端主要关注的是页面上的接入,即调用 startRegistration 注册用户及生成 Passkeys, 调用 startAuthentication 为已有用户登录

  1. 引入前端页面依赖 @simplewebauthn/browser
import SimpleWebAuthnBrowser from '@simplewebauthn/browser';
  1. 调用 startRegistration 注册用户及生成 Passkeys
  const { startRegistration } = SimpleWebAuthnBrowser;
 
  // <button>
  const elemBegin = document.getElementById('btnBegin');
  // <span>/<p>/etc...
  const elemSuccess = document.getElementById('success');
  // <span>/<p>/etc...
  const elemError = document.getElementById('error');
 
  // Start registration when the user clicks a button
  elemBegin.addEventListener('click', async () => {
    // Reset success/error messages
    elemSuccess.innerHTML = '';
    elemError.innerHTML = '';
 
    // GET registration options from the endpoint that calls
    // @simplewebauthn/server -> generateRegistrationOptions()
    const resp = await fetch('/generate-registration-options');
 
    let attResp;
    try {
      // Pass the options to the authenticator and wait for a response
      attResp = await startRegistration(await resp.json());
    } catch (error) {
      // Some basic error handling
      if (error.name === 'InvalidStateError') {
        elemError.innerText = 'Error: Authenticator was probably already registered by user';
      } else {
        elemError.innerText = error;
      }
 
      throw error;
    }
 
    // POST the response to the endpoint that calls
    // @simplewebauthn/server -> verifyRegistrationResponse()
    const verificationResp = await fetch('/verify-registration', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(attResp),
    });
 
    // Wait for the results of verification
    const verificationJSON = await verificationResp.json();
 
    // Show UI appropriate for the `verified` status
    if (verificationJSON && verificationJSON.verified) {
      elemSuccess.innerHTML = 'Success!';
    } else {
      elemError.innerHTML = `Oh no, something went wrong! Response: <pre>${JSON.stringify(
        verificationJSON,
      )}</pre>`;
    }
  });
  1. 调用 startAuthentication 为已有用户登录
  const { startAuthentication } = SimpleWebAuthnBrowser;
 
  // <button>
  const elemBegin = document.getElementById('btnBegin');
  // <span>/<p>/etc...
  const elemSuccess = document.getElementById('success');
  // <span>/<p>/etc...
  const elemError = document.getElementById('error');
 
  // Start authentication when the user clicks a button
  elemBegin.addEventListener('click', async () => {
    // Reset success/error messages
    elemSuccess.innerHTML = '';
    elemError.innerHTML = '';
 
    // GET authentication options from the endpoint that calls
    // @simplewebauthn/server -> generateAuthenticationOptions()
    const resp = await fetch('/generate-authentication-options');
 
    let asseResp;
    try {
      // Pass the options to the authenticator and wait for a response
      asseResp = await startAuthentication(await resp.json());
    } catch (error) {
      // Some basic error handling
      elemError.innerText = error;
      throw error;
    }
 
    // POST the response to the endpoint that calls
    // @simplewebauthn/server -> verifyAuthenticationResponse()
    const verificationResp = await fetch('/verify-authentication', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(asseResp),
    });
 
    // Wait for the results of verification
    const verificationJSON = await verificationResp.json();
 
    // Show UI appropriate for the `verified` status
    if (verificationJSON && verificationJSON.verified) {
      elemSuccess.innerHTML = 'Success!';
    } else {
      elemError.innerHTML = `Oh no, something went wrong! Response: <pre>${JSON.stringify(
        verificationJSON,
      )}</pre>`;
    }
  });

可以看到借助第三方依赖封装之后接入会更加简单,需要使用的 API 也更少更直观。其他更多 API 可以参考 SimpleWebAuthn 文档 (opens in a new tab)

!!!注意:目前 Passkeys 功能还非完善阶段,如果需要使用请注意设备支持,可以参考 Device Support (opens in a new tab)

参考链接

2023 © OXXD.RSS