Skip to main content

Passkeys

原理图

客户端

服务端

passkey-demo 源码

  1. 注册用户,会写入数据库
  2. 登录成功后,可添加 passkey
  3. 添加 passkey 后,对应上图添加的 passkey 到服务器
    1. 获取服务端随机生成 challenge
    2. 客户端指纹识别,生成公钥
    3. 客户端将公钥发送给服务端
    4. 服务端将公钥和 credentials 的 id 存储到数据库
  4. 使用 passkey 登录
    1. 获取服务端随机生成 challenge
    2. 客户端指纹识别,生成公钥
    3. 客户端将公钥发送给服务端
    4. 服务端验签
    5. 验证通过后查询数据库,返回对应的 credentials id 的用户信息

启动项目

注:按照 readme.md 就可以启动

模拟 firebase 截图

数据库(本地运行)

  1. 本地模拟 firebase,项目需要 java11 环境
https://www.oracle.com/java/technologies/downloads/#jdk21-mac

本地前端

  1. http://localhost:8080/
  2. http://127.0.0.1:4000/firestore/data

端口占用

alt text

这个好像不行,啥端口 alt text

$  ~/Desktop/webAuth/passkeys-demo git:(main)lsof -i :8081
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 71377 haotian.chen 168u IPv6 0x2d7829fa0ea81cb2 0t0 TCP localhost:sunproxyadmin (LISTEN)
$ ~/Desktop/webAuth/passkeys-demo git:(main)kill -9 71377

alt text

alt text

效果图

前端

钥匙串添加的数据

浏览器添加的数据

alt text alt text

代码仓库

  1. chrome passkey 管理
  2. 源码阅读记录 passkeys-demo

参考文档

  1. 创建通行密钥以实现无密码登录
  2. 通过表单自动填充功能使用通行密钥登录
  3. passwordless-id/webauthn 是对 webauth 的封装

webauthn 的缺点

  1. Mac 电脑的钥匙串需要手动删除
  2. 浏览器 passkey 需要手动删除
  3. credentials id 需要用用户的不变信息来生成,随机生成导致钥匙串有很多相同的 name,无法辨别,一个用户只需要一个钥匙 credentials id / 浏览器 passkey

技术点

  1. 如何调用起指纹识别
    1. 页面挂载的时候,对 credentials 进行判断,如果有 credentials,用 challenge 对 credentials 发起挑战
    2. 用户聚焦到 input 上,选择一个钥匙串/passkey 会触发挑战,如果挑战成功,会返回公钥
  2. 坑:webauth 不能够存储任何信息
  3. 坑:webauthn 的 response 是一个内容不断变化的对象(即使是相同的挑战,得到的结果也是不一样的,除了时间戳外,还有个计数器,前端叫 count,链端叫 nounce)
  4. 坑:注册页面和登录页面的 input 的 name 和 autocomplete 需要一致
    1. name: 用来标识用户的唯一信息,用来生成 credentials id
    2. autocomplete: 用来标识用户的唯一信息,用来生成 credentials id
    3. 两者必须一致,否则会导致注册的时候生成的 credentials id 和登录的时候生成的 credentials id 不一致,导致登录失败
    4. 两者必须一致,否则会导致注册的时候生成的 credentials id 和登录的时候生成的 credentials id 不一致,导致登录失败
    5. 两者必须一致,否则会导致注册的时候生成的 credentials id 和登录的时候生成的 credentials id 不一致,导致登录失败
    6. 重要的事情说三遍
  5. 坑:注册页面和登录页面不能同时出发 credentials,浏览器会检测只允许一个

核心代码-登录

// 以passkey - demo代码说明;

<input
type="text"
id="username"
class="mdc-text-field__input"
aria-labelledby="username-label"
name="username" // 注意这里
autocomplete="username webauthn" // 注意这里
autofocus
/>

// https://github.com/841660202/passkeys-demo/blob/e873e30bfa3ab0c621e3cbe740ee70fca460578d/views/index.html#L9
export const webauthn ={
// 页面挂在时候调用这个api,这样上面的输入框就可以选择钥匙串了
getBuffer: async function (login: (id: Buffer) => void) {
if (window.PublicKeyCredential && PublicKeyCredential.isConditionalMediationAvailable) {
const cma = await PublicKeyCredential.isConditionalMediationAvailable()
if (cma) {
let challenge = getChallenge() // 这里是一个模拟的随机数
const cred: any = await navigator.credentials.get({
publicKey: {
challenge: parseBase64url(challenge),
allowCredentials: [],
timeout: 60000,
userVerification: 'preferred',
rpId: 'localhost',
},
mediation: 'conditional',
})
console.log('cred', cred)
console.log('cred.response.signature', new Uint8Array(cred.response.signature).toString())

if (cred.id) {
login(Buffer.from(base64url.decode(cred.id!)))
}
}
}
}
}

export const getChallenge = () => {
let challenge = storage.get('challenge')
if (challenge === null) {
challenge = randomChallenge()
storage.set('challenge', challenge)
}

return challenge
}