基于 WebAuthn / FIDO2 实现生物识别无密登录
本指南将向你展示如何使用 Vert.x Web 构建完全兼容 FIDO2 规范的现代 Web 无密认证应用程序。
Web Authentication API(简称 WebAuthn)是由 W3C 和 FIDO 联盟联合制定的全球标准,得到了 Google、Apple、Mozilla、Microsoft 以及 Yubico 的全面原生支持。该标准允许服务器通过非对称公私钥密码学实现用户的硬件级注册与免密验签(例如指纹 Touch ID、人脸 Face ID、Windows Hello 或 YubiKey 安全密钥),彻底终结弱密码隐患。
你将构建什么
你将构建一个无需用户记忆或存储任何密码的 Vert.x Web 应用,全程通过 WebAuthn 标准完成基于生物特征和安全密钥的注册与双向认证。
前置要求
-
文本编辑器或 IDE
-
Java 11 或更高版本(推荐 Java 17+)
-
支持安全认证硬件的现代浏览器(Chrome、Safari、Edge 或 Firefox)
安全前置:启用 SSL/TLS
WebAuthn 属于高安全等级浏览器 API。现代浏览器出于安全规范,强制要求必须在 HTTPS / SSL 加密环境下才允许调用 navigator.credentials(即使本地开发也是如此)。
我们可以使用 Java 自带的 keytool 工具为本机 IP 生成合规的自签名证书:
export IP=10.0.0.2
export CERTSTORE_SECRET=password (1)
keytool \
-genkeypair \
-alias rsakey \
-keyalg rsa \
-storepass ${CERTSTORE_SECRET} \
-keystore certstore.jks \
-storetype JKS \
-dname "CN=${IP}.nip.io,O=Vert.x Development" (2)
| 1 | 生产环境切勿使用测试弱口令! |
| 2 | 将 CN 替换为你本机的局域网 IP(配合 .nip.io 泛域名解析)。 |
转换为现代标准的 PKCS#12 格式:
keytool \
-importkeystore \
-srckeystore certstore.jks \
-destkeystore certstore.jks \
-deststoretype pkcs12
凭证注册交互流程 (Registration Flow)
在登录前,用户需要先将自己的 FIDO2 认证器录入系统,类似于注册流程:
-
用户仅需输入 用户名(如邮箱);
-
服务端(Relying Party)生成随机高熵安全挑战码(Challenge);
-
浏览器将挑战下发给本地硬件安全模块(如触控指纹或 USB Key);
-
认证器在硬件隔离区内部生成全新专属密钥对;
-
认证器对挑战进行私钥签名并附带公钥回传给服务端;
-
服务端验证签名正确性并将公钥持久化绑定到该用户账户下。
身份认证登录流程 (Authentication Flow)
-
用户输入用户名发起登录;
-
服务端生成新的防重放挑战码;
-
浏览器请求本地硬件认证器;
-
用户轻触指纹或刷脸,认证器使用本地私钥对挑战进行签名;
-
浏览器将签名断言提交给服务端;
-
服务端使用此前存储的公钥核验签名,验证通过即完成登录。
编写服务端核心代码
1. 认证器元数据持久化存储
我们需要提供一套接口实现来读写认证器公钥元数据。我们先使用内存存储演示:
// Dummy database, real world workloads
// use a persistent store or course!
InMemoryStore database = new InMemoryStore();
2. 配置 WebAuthn 安全提供者
使用 WebAuthnOptions 配置认证策略:
// create the webauthn security object
WebAuthn webAuthN = WebAuthn.create(
vertx,
new WebAuthnOptions() (1)
.setRelyingParty(new RelyingParty()
.setName("Vert.x FIDO2/webauthn Howto"))
.setUserVerification(UserVerification.DISCOURAGED) (2)
.setAttestation(Attestation.NONE) (3)
.setRequireResidentKey(false) (4)
.setChallengeLength(64) (5)
.addPubKeyCredParam(PublicKeyCredential.ES256) (6)
.addPubKeyCredParam(PublicKeyCredential.RS256)
.addTransport(AuthenticatorTransport.USB) (7)
.addTransport(AuthenticatorTransport.NFC)
.addTransport(AuthenticatorTransport.BLE)
.addTransport(AuthenticatorTransport.INTERNAL))
// where to load/update authenticators data
.authenticatorFetcher(database::fetcher)
.authenticatorUpdater(database::updater);
| 1 | 在 WebAuthnOptions 中定义安全默认参数; |
| 2 | 用户校验要求(User Verification); |
| 3 | 硬件认证(Attestation)策略; |
| 4 | 是否强制驻留密钥(Resident Keys / Passkeys); |
| 5 | 安全挑战码长度(至少 32 字节); |
| 6 | 允许协商的非对称加密算法(如 ES256、RS256); |
| 7 | 允许的硬件传输通道(USB、NFC、BLE 或内置平台认证器)。 |
3. 初始化 Web 路由与处理器
在开启 FIDO2 时,必须挂载以下标准处理器:
-
BodyHandler:解析 POST 请求体; -
SessionHandler:维护认证会话并防范挑战重放。
final Router app = Router.router(vertx);
app.route() (1)
.handler(StaticHandler.create());
app.post() (2)
.handler(BodyHandler.create());
app.route() (3)
.handler(SessionHandler
.create(LocalSessionStore.create(vertx)));
WebAuthnHandler webAuthnHandler = WebAuthnHandler.create(webAuthN) (4)
.setOrigin(String.format("https://%s.nip.io:8443", System.getenv("IP")))
// required callback
.setupCallback(app.post("/webauthn/callback"))
// optional register callback
.setupCredentialsCreateCallback(app.post("/webauthn/register"))
// optional login callback
.setupCredentialsGetCallback(app.post("/webauthn/login"));
app.route()
.handler(webAuthnHandler);
app.route("/protected") (5)
.handler(ctx ->
ctx.response()
.end(
"FIDO2 is Awesome!\n" +
"No Password phishing here!\n"));
| 1 | 分发前端静态资源; |
| 2 | 开启请求正文解析; |
| 3 | 挂载 Session 处理器; |
| 4 | 将 WebAuthn 专用处理器挂载到指定前缀; |
| 5 | 声明受保护的安全业务路由。 |
4. 启动 HTTPS 服务端
vertx.createHttpServer(
new HttpServerOptions()
.setSsl(true)
.setKeyStoreOptions(
new JksOptions()
.setPath("certstore.jks")
.setPassword(System.getenv("CERTSTORE_SECRET"))))
.requestHandler(app)
.listen(8443, "0.0.0.0")
.onSuccess(v -> {
System.out.printf("Server: https://%s.nip.io:8443%n", System.getenv("IP"));
start.complete();
})
.onFailure(start::fail);
启动命令:
IP=10.0.0.2 mvn exec:java
编写前端调用脚本
页面 HTML 结构包含注册、登录按钮与用户名输入框:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>WebAuthn Howto</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="message"></div>
<div> (1)
<label for="displayName">Display Name: </label>
<input id="displayName" name="displayName" type="text" value="Your Name"><br/>
<label for="username">Username: </label>
<input id="username" name="username" type="email" value="me@server.com">
</div>
<hr>
<div> (2)
<button id="register">register</button>
<button id="login">login</button>
</div>
<hr>
<div> (3)
<a href="/protected">Want some secret info?</a>
</div>
<script src="vertx-auth-webauthn-client.js"></script> (4)
<script src="main.js"></script> (5)
</body>
</html>
前端交互基于 Vert.x 提供的轻量客户端辅助脚本 vertx-auth-webauthn-client.js,直接使用现代标准的 Promise 风格发起调用:
const webAuthn = new WebAuthn({
callbackPath: '/webauthn/callback',
registerPath: '/webauthn/register',
loginPath: '/webauthn/login'
});
绑定注册按钮的点击事件:
registerButton.onclick = () => {
webAuthn
.register({
name: document.getElementById('username').value,
displayName: document.getElementById('displayName').value
})
.then(() => {
displayMessage('registration successful');
})
.catch(err => {
displayMessage('registration failed');
console.error(err);
});
};
绑定登录按钮的点击事件:
loginButton.onclick = () => {
webAuthn
.login({
name: document.getElementById('username').value
})
.then(() => {
displayMessage('You are logged in');
})
.catch(err => {
displayMessage('Invalid credential');
console.error(err);
});
};
当用户点击注册或登录时,浏览器将自动弹出系统级生物认证或安全密钥授权窗口。一旦指纹或面容识别通过,用户即可无缝登入系统并访问 /protected 专属资源!
本文总结
通过本指南,我们掌握了:
-
FIDO2 / WebAuthn 非对称密钥免密认证底层运行逻辑;
-
配置 Vert.x WebAuthn 服务端与 HTTPS 安全传输;
-
结合前端客户端库实现原生生物识别交互体验。