WebAuthn
- 全称是 Web Authentication,是一个由万维网联盟(W3C)和 FIDO 联盟(Fast Identity Online)开发的 web 标准。它允许网站和应用程序使用公钥加密和生物识别技术(如指纹识别、面部识别)进行强身份验证,以替代传统的密码登录方式。WebAuthn 提供了更高的安全性和更好的用户体验。
- https://webauthn.io/
WebAuthn 工作原理
WebAuthn 是一个由万维网联盟(W3C)和 FIDO 联盟(Fast Identity Online)开发的 web 标准,它允许网站和应用程序使用公钥加密和生物识别技术(如指纹识别、面部识别)进行强身份验证,以替代传统的密码登录方式。以下是 WebAuthn 的工作原理,包括注册(Registration)和认证(Authentication)两个主要过程。
1. 注册(Registration)
在注册过程中,用户在其设备上创建一个新的公钥-私钥对,并将公钥发送给服务器保存。具体步骤如下:
- 用户发起注册请求:用户在网站上选择注册,并开始注册过程。
- 服务器生成挑战(challenge):服务器生成一个随机的挑战,并将其发送给用户的浏览器。
- 浏览器调用 WebAuthn API:浏览器使用 WebAuthn API 调用用户设备的可信平台模块(TPM)或安全元件,生成公钥-私钥对。然后,设备会用私钥签署服务器的挑战,并将签名和公钥一起发送回服务器。
- 服务器验证并存储公钥:服务器验证签名和挑战,以确保请求的真实性。如果验证成功,服务器存储公钥用于后续的认证。
2. 认证(Authentication)
在认证过程中,用户使用之前注册的设备进行登录。具体步骤如下:
- 用户发起登录请求:用户在网站上选择登录,并开始认证过程。
- 服务器生成挑战(challenge):服务器生成一个随机的挑战,并将其发送给用户的浏览器。
- 浏览器调用 WebAuthn API:浏览器使用 WebAuthn API 调用用户设备的 TPM 或安全元件,使用私钥签署服务器的挑战。然后,设备将签名和相关信息发送回服务器。
- 服务器验证签名:服务器使用存储的公钥验证签名和挑战,以确保请求的真实性。如果验证成功,用户成功登录。
WebAuthn 的优势
- 提高安全性:WebAuthn 使用公钥加密,减少了传统密码的攻击面,避免了密码泄露、弱密码等问题。
- 简化用户体验:利用设备的生物识别功能,如指纹、面部识别等,使得用户无需记住复杂的密码,登录过程更加便捷。
- 广泛支持:WebAuthn 标准得到了主流浏览器和操作系统的广泛支持,包括 Chrome、Firefox、Edge、Safari,以及 Android 和 iOS 设备。
navigator.credentials.create
Navigator
接口的只读属性credentials
返回与当前文档关联的CredentialsContainer
对象,该对象暴露用于请求凭据的方法。CredentialsContainer
接口还会在发生感兴趣的事件时通知用户代理,例如成功登录或注销。此接口可用于特性检测。
开始注册
Web Authentication API 的 options 对象
字段 | 类型 | 说明 | 可选值 |
---|---|---|---|
attestation | 字符串 | 指定所需的认证声明传递偏好。 | "none" (无需认证信息)、"indirect" (间接认证)、"direct" (直接认证) |
authenticatorSelection | 对象 | 定义选择认证器的标准。 | |
- authenticatorAttachment | 字符串 | 指定要使用的认证器类型。 | "platform" (使用集成到设备中的认证器)、"cross-platform" (使用外部认证器) |
- requireResidentKey | 布尔值 | 指示认证器是否必须创建存储在设备上的常驻密钥。 | true (是)、false (否) |
- residentKey | 字符串 | 指定常驻密钥的要求。 | "required" (必须)、"preferred" (优先)、"discouraged" (不建议) |
challenge | 字符串 | 用于认证过程中的挑战码,应该是从服务器端生成的。 | |
excludeCredentials | 数组 | 列出在注册新凭证时应排除的凭证的列表,用于确保不重复注册。 | |
extensions | 对象 | 用于扩展 Web Authentication API 的附加选项。 | |
- credProps | 布尔值 | 指示是否返回有关创建的凭证的属性信息。 | true (返回),false (不返回) |
pubKeyCredParams | 数组 | 定义公钥证书应使用的加密算法。 | |
- alg | 数字 | 算法标识符(如 -7 表示 ES256,-257 表示 RS256) | |
- type | 字符串 | 证书类型,通常为 "public-key" | |
rp | 对象 | 定义依赖方(即服务提供者)的信息。 | |
- id | 字符串 | 依赖方的域名。 | |
- name | 字符串 | 依赖方的描述名称,如 "Passkey Example" 本地测试可以用 window.location.hostname | |
timeout | 数字 | 指定操作的超时时间(毫秒)。 | |
user | 对象 | 定义正在注册或认证的用户的信息。 | |
- displayName | 字符串 | 用户的显示名称。 | |
- id | 字符串 | 用户的唯一标识符。 | |
- name | 字符串 | 用户的名称。 |
// options 参考
{
attestation: "none",
authenticatorSelection: {
authenticatorAttachment: "platform",
requireResidentKey: true,
residentKey: "required",
},
challenge: "F16NrxGy23Ps_73Pgy_H6fh0ihgEXBuycusFLahMvmU",
excludeCredentials: [],
extensions: {
credProps: true,
},
pubKeyCredParams: [
{
alg: -7, // 对应于ES256
type: "public-key",
},
{
alg: -257, // 对应于RS256
type: "public-key",
},
],
rp: {
id: window.location.hostname, // 本地测试用 可以使用localhost
name: "Passkey Example",
},
timeout: 60000,
user: {
displayName: "allen",
id: "20222027",
name: "allen",
},
};
需要将 challenge 和 user.id 转成ArrayBuffer
类型
options.challenge = base64url.decode(options.challenge);
options.user.id = base64url.decode(options.user.id);
const base64url = {
encode: function (buffer) {
const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
},
decode: function (base64url) {
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const binStr = window.atob(base64);
const bin = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) {
bin[i] = binStr.charCodeAt(i);
}
return bin.buffer;
},
};
唤起生物认证
- 未开启 windows hello
- 在系统设置里面添加
- 添加完成 唤起成功
完成注册后的步骤
在 Web Authentication API (navigator.credentials.create
) 成功创建公钥凭证后,你需要执行以下几个关键步骤以完成用户的注册过程:
1. 发送凭证信息到服务器
成功创建凭证后,需要将其详细信息发送到服务器端进行验证和存储。以下是应发送的凭证详情:
- ID (
credential.id
): 凭证的唯一标识符。 - 类型 (
credential.type
): 凭证的类型,通常是"public-key"
。 - 原始 ID (
credential.rawId
): 凭证的原始标识符,使用 Base64URL 编码格式。 - 响应 (
credential.response
): 包括认证对象 (attestationObject
) 和客户端数据 (clientDataJSON
) 的响应,均使用 Base64URL 编码。
2. 服务器端验证
服务器接收到客户端发送的凭证信息后,需要执行以下验证步骤:
- 验证挑战 (
challenge
): 确认返回的挑战码与之前发送给客户端的挑战码相匹配。 - 验证来源 (
origin
): 确认凭证的创建请求来自预期的域名。 - 验证公钥凭证参数 (
pubKeyCredParams
): 确认凭证使用了有效的加密算法。 - 存储凭证信息: 在服务器端安全地存储用户的公钥和其他相关凭证信息,以备将来进行身份验证使用。
示例代码:将凭证信息发送到服务器
fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: credential.id,
type: credential.type,
rawId: credential.rawId,
attestationObject: credential.response.attestationObject,
clientDataJSON: credential.response.clientDataJSON
})
})
.then(response => response.json())
.then(data => {
console.log('注册成功', data);
alert('注册成功!');
})
.catch(error => {
console.error('注册失败', error);
alert('注册失败,请重试!');
});
完整注册代码
async register() {
if (!("credentials" in navigator)) {
alert(
"此浏览器不支持 Web Authentication API。请尝试更新浏览器或使用其他支持的浏览器。"
);
return;
}
try {
// 从服务器获取创建凭证的选项
const {data: options} = await axios.get('/api/register-options');
// 转成 arrayBuffer
options.challenge = base64url.decode(options.challenge);
options.user.id = base64url.decode(options.user.id);
// 开始注册
const credential = await navigator.credentials.create({
publicKey: options,
});
await axios.post('/api/register', {
id: credential.id,
type: credential.type,
rawId: credential.rawId,
attestationObject: credential.response.attestationObject,
clientDataJSON: credential.response.clientDataJSON
});
alert("注册成功");
} catch (error) {
console.error("注册失败", error);
alert("注册失败");
}
},