不用密码也能登陆?React中的通行密钥实现
你可能已经注意到了,近两年有些网站提供了一种名为 通行密钥/Passkey 的东西作为用户身份认证方式,或者辅助验证。今天我们来了解下什么是通行密钥。
什么是通行密钥?
2022 年 6 月,苹果公司宣布将在 iOS 和 macOS 中加入对通行密钥标准的支持,“通行密钥”这个术语首次被广泛提及。除了 Apple,目前 Google 和 Microsoft 也提供了对通行密钥的支持。
简单的说,通行密钥是一种用于网站或者 App 身份验证的方式,它使用非对称加密方式,使用存储在个人设备(手机或者电脑)中的私钥进行签名,由服务器端使用对应的公钥进行验证,确认用户身份完成登录过程。
要实现这个过程需要浏览器和操作系统的共同支持。这个技术在现代的设备上已经有了比较好的支持,如果你使用 iOS/MacOS 系统,那么更妙,iCloud 会自动同步存储的信息,方便你跨设备使用。
为什么使用通行密钥?
- 安全。传统的用户名/密码登陆方式早已不安全,容易受到撞库、密码泄漏影响。
- 良好的体验。两步验证虽然安全,但是使用上比较麻烦。而使用通行密钥用户无需记住复杂的密码,还可以使用生物识别等方式快速登录。
- 跨平台兼容。通行密钥可在不同设备和平台间无缝使用。
如何在 React 应用中实现通行密钥?
要在代码中实现通行密钥的支持,需要客户端和服务端的共同配合。我试着实现了一下,这个流程其实并不复杂,客户端的核心 API 只有navigator.credentials.create
和navigator.credentials.get
,但是因为涉及数据格式转换、签名验证等,还是有些繁琐的。所以我推荐使用simplewebauthn这个库来帮助实现,它提供了客户端和服务端实用函数的封装,十分方便。
流程
我用 Claude 生成了两个图方便理解,
两个流程很类似,都是先向服务器请求包含 challenge 的配置信息,然后和navigator.credentials
API 交互,最后把结果传给服务器用于注册或者登陆。
示例代码
因为我使用 Next.js 的 Server Action,所以避免了手动调用接口,直接调用后端方法即可。代码比较长,我省略了函数的实现,完整代码以文件形式提供。
完整代码:passkey.tsx passkey.ts
首先在数据库 User 表中添加了三个字段用于存储通行密钥信息,以下是 prisma 的 schema 文件的修改。
credentialId String? @unique @map("credential_id")
publicKey String? @map("public_key")
counter Int @default(0) @map("counter")
// passkey.ts 文件,用于服务器端
// 生成通行密钥创建选项
// 返回包含challenge等信息的选项对象,用于客户端调用navigator.credentials.create
export async function generateCreateOptions() {
...
}
// 验证通行密钥创建响应
// 参数attResponse: 客户端创建通行密钥后的响应数据
// 返回验证结果,成功则返回true
export async function verifyCreateResponse(
attResponse: RegistrationResponseJSON,
) {
...
}
// 生成通行密钥登录选项
// 返回包含challenge等信息的选项对象,用于客户端调用navigator.credentials.get
export async function generateLoginOptions() {
...
}
// 验证通行密钥登录响应
// 参数attResponse: 客户端使用通行密钥登录后的响应数据
// 返回验证结果,成功则返回true
export async function verifyLoginResponse(
attResponse: AuthenticationResponseJSON,
) {
...
}
// passkey.tsx React组件,用于客户端
export function PasskeyBindButton() {
const handleBindPasskey = async () => {
const options = await generateCreateOptions();
const credential = await startRegistration(options);
const verified = await verifyCreateResponse(credential as any);
if (verified) {
toast.success("绑定通行密钥成功");
} else {
toast.error("绑定通行密钥失败");
}
};
return (
<div>
<Button onClick={handleBindPasskey}>绑定通行密钥</Button>
</div>
);
}
export function PasskeyLoginButton() {
const router = useRouter();
const handlePasskeyLogin = async () => {
const options = await generateLoginOptions();
const credential = await startAuthentication(options);
try {
const verified = await verifyLoginResponse(credential as any);
if (verified) {
toast.success("登录成功");
router.push(siteConfig.adminUrl);
} else {
toast.error("登录失败");
}
} catch (error) {
console.error(error);
toast.error((error as Error).message);
return;
}
};
return (
<Button className="w-full" onClick={handlePasskeyLogin}>
使用通行密钥登录
</Button>
);
}
如果客户端和服务端都正确实现的话,点击页面上的绑定或者登陆按钮,就会唤起浏览器的注册/登陆界面。
一些注意事项
- 我这里是在用户登陆以后,提供了绑定通行密钥的按钮,所以"注册流程"走完以后会绑定到当前用户。你也可以在其他流程中调用,只需要修改验证流程即可。
- 实际上可以允许用户绑定多个通行密钥,这样在登陆的时候,可以选择使用哪个通行密钥登录。我这里只实现了一个,你可以在代码中修改。
- 创建 options 和验证响应数据时,都使用到了 challenge,这个值应该是一个随机数,并且只能使用一次,避免重放攻击,这一点
simplewebauthn
已经帮我们做到了,我们需要将其存储在 session 中用于后续验证。 - 通行密钥的API提供了很多选项用于详细的设置,可以查看相关文档。
结论
通行密钥提供了一种现代、安全、便捷的身份验证方法。通过利用 WebAuthn API,开发者可以轻松地将这一技术整合到他们的应用中,从而提高安全性并改善用户体验。随着更多浏览器和设备支持这一技术,通行密钥有望成为未来身份验证的标准方式。
参考资料
- Web Authentication API - MDN
- Passkey Form Autofill - Google Codelabs
- 通行密钥开发指南 - 廖雪峰
- SimpleWebAuthn Documentation