Java与WebAuthn(FIDO2):实现无密码、强认证的身份验证机制
大家好,今天我们来深入探讨如何利用Java和WebAuthn(FIDO2)构建一套无密码、强认证的身份验证机制。WebAuthn的出现,为我们提供了一种安全、便捷、且标准化的方式,取代传统的密码验证,显著提升用户账户的安全性。
1. WebAuthn 概述
WebAuthn(Web Authentication)是由 W3C 和 FIDO 联盟共同制定的开放标准。它允许网站和应用程序利用各种认证器(Authenticator)进行用户身份验证,而无需依赖传统的密码。这些认证器可以是硬件安全密钥(如 YubiKey)、指纹扫描仪、面部识别器,甚至是智能手机上的平台认证器。
1.1 WebAuthn 的核心概念:
- 依赖方 (Relying Party, RP): 通常是网站或应用程序,负责发起和验证 WebAuthn 认证过程。 在我们的示例中,这将是我们的 Java 后端服务器和前端 Web 应用程序。
- 用户代理 (User Agent): 用户的浏览器,负责与认证器进行通信,并将结果传递给依赖方。
- 认证器 (Authenticator): 提供认证功能的硬件或软件设备。它生成和存储密钥对,并执行加密操作以验证用户身份。
- 凭证 (Credential): 由认证器创建并存储的密钥对,用于在后续的认证过程中验证用户身份。凭证与特定的依赖方和用户账户相关联。
1.2 WebAuthn 的优势:
- 安全性: 使用非对称密钥加密,密钥存储在认证器硬件中,防止密码泄露和重放攻击。
- 便捷性: 用户可以使用生物识别或硬件密钥进行身份验证,无需记住复杂的密码。
- 标准化: WebAuthn 是一种开放标准,得到主流浏览器和操作系统的支持,易于集成和部署。
- 防钓鱼: 凭证与特定的域名绑定,有效防止钓鱼攻击。
2. WebAuthn 的工作流程
WebAuthn 的工作流程主要包括两个阶段:注册 (Registration) 和认证 (Authentication)。
2.1 注册 (Registration):
- 依赖方 (RP) 发起注册: RP 向用户代理发送注册请求,包含 RP ID (通常是域名)、用户 ID 和其他可选参数。
- 用户代理与认证器交互: 用户代理接收到注册请求后,提示用户选择认证器并进行验证(例如,插入安全密钥、扫描指纹)。
- 认证器生成凭证: 认证器生成一个新的密钥对,并将公钥部分返回给用户代理。私钥部分安全地存储在认证器中。
- 用户代理将凭证返回给 RP: 用户代理将公钥和凭证元数据(例如,认证器类型、凭证 ID)发送回 RP。
- RP 存储凭证: RP 将公钥和凭证元数据与用户账户关联存储在数据库中。
2.2 认证 (Authentication):
- 依赖方 (RP) 发起认证: RP 向用户代理发送认证请求,包含 RP ID 和允许使用的凭证 ID 列表。
- 用户代理与认证器交互: 用户代理接收到认证请求后,提示用户选择认证器并进行验证。
- 认证器生成签名: 认证器使用存储的私钥对挑战 (Challenge) 进行签名。
- 用户代理将签名返回给 RP: 用户代理将签名和凭证 ID 发送回 RP。
- RP 验证签名: RP 使用存储的公钥验证签名是否有效。如果验证成功,则用户身份验证通过。
3. Java 后端实现 WebAuthn
接下来,我们将使用 Java 实现 WebAuthn 的后端逻辑。我们将使用 java-webauthn-server 库,它简化了 WebAuthn 的集成过程。
3.1 添加依赖:
首先,需要在 pom.xml 文件中添加 java-webauthn-server 库的依赖:
<dependency>
<groupId>com.github.webauthn-json</groupId>
<artifactId>webauthn-server-json</artifactId>
<version>0.1.0</version>
</dependency>
3.2 配置 WebAuthn 服务:
import com.webauthn4j.server.WebAuthnServer;
import com.webauthn4j.server.WebAuthnServerCore;
import com.webauthn4j.server.ServerProperty;
import com.webauthn4j.server.ServerPropertyProvider;
import com.webauthn4j.util.Base64UrlUtil;
import java.util.Collections;
import java.util.Set;
public class WebAuthnService {
private final WebAuthnServer webAuthnServer;
private final String relyingPartyId;
private final String relyingPartyName;
public WebAuthnService(String relyingPartyId, String relyingPartyName) {
this.relyingPartyId = relyingPartyId;
this.relyingPartyName = relyingPartyName;
ServerPropertyProvider serverPropertyProvider = (clientDataHash) -> {
Set<String> origins = Collections.singleton(relyingPartyId);
return new ServerProperty(relyingPartyId, relyingPartyName, clientDataHash, origins, null);
};
this.webAuthnServer = new WebAuthnServerCore(serverPropertyProvider);
}
public WebAuthnServer getWebAuthnServer() {
return webAuthnServer;
}
public String getRelyingPartyId() {
return relyingPartyId;
}
public String getRelyingPartyName() {
return relyingPartyName;
}
}
解释:
WebAuthnService类封装了 WebAuthn Server 的配置。relyingPartyId通常是你的域名。relyingPartyName是你的应用程序名称。ServerPropertyProvider提供ServerProperty对象,其中包含 RP ID、RP Name 和 Client Data Hash 等信息。 Client Data Hash 是从前端获取的,用于防止重放攻击。
3.3 注册流程实现:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.webauthn4j.data.PublicKeyCredentialCreationOptions;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.data.PublicKeyCredentialParameters;
import com.webauthn4j.data.PublicKeyCredentialType;
import com.webauthn4j.data.PublicKeyCredentialDescriptor;
import com.webauthn4j.data.AuthenticatorSelectionCriteria;
import com.webauthn4j.data.AuthenticatorAttachment;
import com.webauthn4j.data.ResidentKeyRequirement;
import com.webauthn4j.data.AttestationConveyancePreference;
import com.webauthn4j.data.UserVerificationRequirement;
import com.webauthn4j.data.AuthenticatorTransport;
import com.webauthn4j.data.RegistrationData;
import com.webauthn4j.validator.RegistrationContextValidationResponse;
import com.webauthn4j.data.attestation.AttestationObject;
import com.webauthn4j.data.client.ClientRegistrationResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.UUID;
public class RegistrationController {
private final WebAuthnService webAuthnService;
private final ObjectMapper objectMapper = new ObjectMapper();
private final UserRepository userRepository;
public RegistrationController(WebAuthnService webAuthnService, UserRepository userRepository) {
this.webAuthnService = webAuthnService;
this.userRepository = userRepository;
}
public void startRegistration(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
User user = userRepository.findByUsername(username);
if (user == null) {
user = new User();
user.setUsername(username);
user.setId(UUID.randomUUID().toString()); // Assign a unique ID
userRepository.save(user);
}
Challenge challenge = new DefaultChallenge();
request.getSession().setAttribute("registrationChallenge", challenge);
request.getSession().setAttribute("registrationUser", user);
PublicKeyCredentialCreationOptions registrationOptions = createRegistrationOptions(user.getId(), user.getUsername(), challenge);
String json = objectMapper.writeValueAsString(registrationOptions);
response.setContentType("application/json");
response.getWriter().write(json);
}
private PublicKeyCredentialCreationOptions createRegistrationOptions(String userId, String username, Challenge challenge) {
// See: https://www.w3.org/TR/webauthn-2/#dictionary-credential-params
List<PublicKeyCredentialParameters> pubKeyCredParams = new ArrayList<>();
pubKeyCredParams.add(new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7)); // ES256
pubKeyCredParams.add(new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -257)); // RS256
// See: https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialuserentity
String userDisplayName = username;
byte[] userIdBytes = userId.getBytes();
// See: https://www.w3.org/TR/webauthn-2/#dictdef-authenticatorselectioncriteria
AuthenticatorSelectionCriteria authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
AuthenticatorAttachment.PLATFORM,
true,
UserVerificationRequirement.PREFERRED,
ResidentKeyRequirement.REQUIRED
);
// See: https://www.w3.org/TR/webauthn-2/#enumdef-attestationconveyancepreference
AttestationConveyancePreference attestation = AttestationConveyancePreference.NONE;
return new PublicKeyCredentialCreationOptions(
webAuthnService.getRelyingPartyId(),
userIdBytes,
username,
userDisplayName,
challenge,
pubKeyCredParams,
60000L, // Timeout in milliseconds
Collections.emptyList(), // excludeCredentials
authenticatorSelectionCriteria,
attestation,
null
);
}
public void finishRegistration(HttpServletRequest request, HttpServletResponse response) throws IOException {
User user = (User) request.getSession().getAttribute("registrationUser");
Challenge challenge = (Challenge) request.getSession().getAttribute("registrationChallenge");
if (user == null || challenge == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid registration state");
return;
}
try {
JsonNode registrationResponseJson = objectMapper.readTree(request.getReader());
// Extract data from the registration response
String clientDataJSONString = registrationResponseJson.get("response").get("clientDataJSON").asText();
String attestationObjectString = registrationResponseJson.get("response").get("attestationObject").asText();
String credentialIdString = registrationResponseJson.get("id").asText();
String credentialTypeString = registrationResponseJson.get("type").asText();
// Decode Base64URL encoded strings
byte[] clientDataJSON = Base64UrlUtil.decode(clientDataJSONString);
byte[] attestationObjectBytes = Base64UrlUtil.decode(attestationObjectString);
byte[] credentialId = Base64UrlUtil.decode(credentialIdString);
// Construct the AttestationObject
AttestationObject attestationObject = AttestationObject.fromBytes(attestationObjectBytes);
// Construct the ClientRegistrationResponse
ClientRegistrationResponse clientRegistrationResponse = new ClientRegistrationResponse(clientDataJSON, attestationObjectString);
// Build RegistrationData object
RegistrationData registrationData = new RegistrationData(clientRegistrationResponse, attestationObject);
RegistrationContextValidationResponse registrationContextValidationResponse = webAuthnService.getWebAuthnServer().validate(
registrationData,
challenge.getValue(),
webAuthnService.getRelyingPartyId(),
Collections.singleton(webAuthnService.getRelyingPartyId())
);
// Retrieve validated key information
byte[] publicKey = registrationContextValidationResponse.getAttestationObject().getAuthenticatorData().getAttestedCredentialData().getCredentialPublicKey();
// Store credential information in the database
Credential credential = new Credential();
credential.setUserId(user.getId());
credential.setCredentialId(credentialId);
credential.setPublicKey(publicKey);
credential.setAttestationType(registrationContextValidationResponse.getAttestationObject().getFormat());
userRepository.saveCredential(credential);
request.getSession().removeAttribute("registrationChallenge");
request.getSession().removeAttribute("registrationUser");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write("Registration successful");
} catch (Exception e) {
e.printStackTrace();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("Registration failed: " + e.getMessage());
}
}
}
解释:
startRegistration方法:- 生成一个随机的
Challenge,并将其存储在 Session 中。 - 创建
PublicKeyCredentialCreationOptions对象,其中包含注册所需的参数。 - 将
PublicKeyCredentialCreationOptions对象序列化为 JSON,并返回给前端。
- 生成一个随机的
createRegistrationOptions方法:- 创建
PublicKeyCredentialCreationOptions对象,其中包含 RP ID、用户 ID、用户名、Challenge、支持的公钥算法等信息。
- 创建
finishRegistration方法:- 从 Session 中获取
Challenge和User对象。 - 从请求中获取注册响应数据。
- 使用
webAuthnService.getWebAuthnServer().validate()方法验证注册响应。 - 如果验证成功,则将凭证信息存储到数据库中。
- 从 Session 中获取
3.4 认证流程实现:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.webauthn4j.data.PublicKeyCredentialRequestOptions;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.data.PublicKeyCredentialDescriptor;
import com.webauthn4j.data.AuthenticatorTransport;
import com.webauthn4j.data.AuthenticationData;
import com.webauthn4j.validator.AuthenticationContextValidationResponse;
import com.webauthn4j.data.client.ClientAuthenticationResponse;
import com.webauthn4j.util.Base64UrlUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.stream.Collectors;
public class AuthenticationController {
private final WebAuthnService webAuthnService;
private final ObjectMapper objectMapper = new ObjectMapper();
private final UserRepository userRepository;
public AuthenticationController(WebAuthnService webAuthnService, UserRepository userRepository) {
this.webAuthnService = webAuthnService;
this.userRepository = userRepository;
}
public void startAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
User user = userRepository.findByUsername(username);
if (user == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("User not found");
return;
}
Challenge challenge = new DefaultChallenge();
request.getSession().setAttribute("authenticationChallenge", challenge);
request.getSession().setAttribute("authenticationUser", user);
List<Credential> credentials = userRepository.findCredentialsByUserId(user.getId());
PublicKeyCredentialRequestOptions authenticationOptions = createAuthenticationOptions(credentials, challenge);
String json = objectMapper.writeValueAsString(authenticationOptions);
response.setContentType("application/json");
response.getWriter().write(json);
}
private PublicKeyCredentialRequestOptions createAuthenticationOptions(List<Credential> credentials, Challenge challenge) {
List<PublicKeyCredentialDescriptor> allowCredentials = credentials.stream()
.map(credential -> new PublicKeyCredentialDescriptor(
com.webauthn4j.data.PublicKeyCredentialType.PUBLIC_KEY,
credential.getCredentialId(),
Collections.emptyList()
))
.collect(Collectors.toList());
return new PublicKeyCredentialRequestOptions(
challenge,
60000L, // Timeout in milliseconds
webAuthnService.getRelyingPartyId(),
allowCredentials,
UserVerificationRequirement.PREFERRED
);
}
public void finishAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
User user = (User) request.getSession().getAttribute("authenticationUser");
Challenge challenge = (Challenge) request.getSession().getAttribute("authenticationChallenge");
if (user == null || challenge == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid authentication state");
return;
}
try {
JsonNode authenticationResponseJson = objectMapper.readTree(request.getReader());
// Extract data from the authentication response
String credentialIdString = authenticationResponseJson.get("id").asText();
String clientDataJSONString = authenticationResponseJson.get("response").get("clientDataJSON").asText();
String authenticatorDataString = authenticationResponseJson.get("response").get("authenticatorData").asText();
String signatureString = authenticationResponseJson.get("response").get("signature").asText();
String userHandleString = authenticationResponseJson.get("response").has("userHandle") ? authenticationResponseJson.get("response").get("userHandle").asText() : null;
// Decode Base64URL encoded strings
byte[] credentialId = Base64UrlUtil.decode(credentialIdString);
byte[] clientDataJSON = Base64UrlUtil.decode(clientDataJSONString);
byte[] authenticatorDataBytes = Base64UrlUtil.decode(authenticatorDataString);
byte[] signature = Base64UrlUtil.decode(signatureString);
byte[] userHandle = (userHandleString != null) ? Base64UrlUtil.decode(userHandleString) : null;
// Construct the ClientAuthenticationResponse
ClientAuthenticationResponse clientAuthenticationResponse = new ClientAuthenticationResponse(clientDataJSON, authenticatorDataString, signature, userHandleString);
// Build AuthenticationData object
AuthenticationData authenticationData = new AuthenticationData(clientAuthenticationResponse, credentialId);
Credential credential = userRepository.findCredentialByCredentialId(credentialId);
if (credential == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Credential not found");
return;
}
AuthenticationContextValidationResponse authenticationContextValidationResponse = webAuthnService.getWebAuthnServer().validate(
authenticationData,
challenge.getValue(),
webAuthnService.getRelyingPartyId(),
Collections.singleton(webAuthnService.getRelyingPartyId()),
credential.getPublicKey()
);
// Perform additional checks, such as user verification
if (!authenticationContextValidationResponse.getAuthenticatorData().isUserPresent()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("User not present");
return;
}
// Authentication successful
request.getSession().removeAttribute("authenticationChallenge");
request.getSession().removeAttribute("authenticationUser");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write("Authentication successful");
} catch (Exception e) {
e.printStackTrace();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("Authentication failed: " + e.getMessage());
}
}
}
解释:
startAuthentication方法:- 生成一个随机的
Challenge,并将其存储在 Session 中。 - 从数据库中检索用户的凭证列表。
- 创建
PublicKeyCredentialRequestOptions对象,其中包含认证所需的参数。 - 将
PublicKeyCredentialRequestOptions对象序列化为 JSON,并返回给前端。
- 生成一个随机的
createAuthenticationOptions方法:- 创建
PublicKeyCredentialRequestOptions对象,其中包含 Challenge、允许使用的凭证 ID 列表等信息。
- 创建
finishAuthentication方法:- 从 Session 中获取
Challenge和User对象。 - 从请求中获取认证响应数据。
- 使用
webAuthnService.getWebAuthnServer().validate()方法验证认证响应。 - 如果验证成功,则用户身份验证通过。
- 从 Session 中获取
3.5 User 和 Credential 的数据模型:
public class User {
private String id;
private String username;
// Getters and setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
public class Credential {
private String userId;
private byte[] credentialId;
private byte[] publicKey;
private String attestationType;
// Getters and setters
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public byte[] getCredentialId() {
return credentialId;
}
public void setCredentialId(byte[] credentialId) {
this.credentialId = credentialId;
}
public byte[] getPublicKey() {
return publicKey;
}
public void setPublicKey(byte[] publicKey) {
this.publicKey = publicKey;
}
public String getAttestationType() {
return attestationType;
}
public void setAttestationType(String attestationType) {
this.attestationType = attestationType;
}
}
//UserRepository interface (Example, you can use any ORM or database access method)
interface UserRepository {
User findByUsername(String username);
void save(User user);
List<Credential> findCredentialsByUserId(String userId);
Credential findCredentialByCredentialId(byte[] credentialId);
void saveCredential(Credential credential);
}
3.6 数据库存储:
你需要选择一个数据库来存储用户信息和凭证信息。 以下是一个简单的表格,展示了可能的数据库结构:
| 表名 | 列名 | 数据类型 | 说明 |
|---|---|---|---|
| users | id | VARCHAR(36) | 用户 ID (UUID) |
| username | VARCHAR(255) | 用户名 | |
| credentials | user_id | VARCHAR(36) | 用户 ID (外键,关联 users 表) |
| credential_id | BLOB | 凭证 ID (Base64URL 编码) | |
| public_key | BLOB | 公钥 (DER 编码) | |
| attestation_type | VARCHAR(255) | 认证声明类型 (例如: fido-u2f, packed) |
4. 前端实现 WebAuthn
前端需要使用 WebAuthn API 与浏览器和认证器进行交互。 以下是使用 JavaScript 的示例代码:
4.1 注册流程:
async function register() {
const username = document.getElementById("username").value;
// 1. Start Registration
const registrationOptions = await fetch("/registration/start?username=" + username)
.then(response => response.json());
// Convert base64url to ArrayBuffer
registrationOptions.challenge = base64urlToArrayBuffer(registrationOptions.challenge);
registrationOptions.user.id = base64urlToArrayBuffer(registrationOptions.user.id);
if (registrationOptions.excludeCredentials) {
registrationOptions.excludeCredentials = registrationOptions.excludeCredentials.map(c => {
c.id = base64urlToArrayBuffer(c.id);
return c;
});
}
// 2. Call navigator.credentials.create()
const credential = await navigator.credentials.create({
publicKey: registrationOptions
});
// Convert ArrayBuffer to base64url
const attestationObject = arrayBufferToBase64url(credential.response.attestationObject);
const clientDataJSON = arrayBufferToBase64url(credential.response.clientDataJSON);
const rawId = arrayBufferToBase64url(credential.rawId);
// 3. Send Registration Response to Server
const registrationResult = await fetch("/registration/finish", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: rawId,
type: credential.type,
response: {
attestationObject: attestationObject,
clientDataJSON: clientDataJSON
}
})
}).then(response => response.text());
alert(registrationResult);
}
// Utility functions for base64url encoding/decoding
function base64urlToArrayBuffer(base64url) {
const padding = '='.repeat((4 - base64url.length % 4) % 4);
const base64 = (base64url + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
function arrayBufferToBase64url(buffer) {
let binary = '';
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/+/g, '-')
.replace(///g, '_')
.replace(/=+$/, '');
}
4.2 认证流程:
async function authenticate() {
const username = document.getElementById("username").value;
// 1. Start Authentication
const authenticationOptions = await fetch("/authentication/start?username=" + username)
.then(response => response.json());
// Convert base64url to ArrayBuffer
authenticationOptions.challenge = base64urlToArrayBuffer(authenticationOptions.challenge);
if (authenticationOptions.allowCredentials) {
authenticationOptions.allowCredentials = authenticationOptions.allowCredentials.map(c => {
c.id = base64urlToArrayBuffer(c.id);
return c;
});
}
// 2. Call navigator.credentials.get()
const credential = await navigator.credentials.get({
publicKey: authenticationOptions
});
// Convert ArrayBuffer to base64url
const clientDataJSON = arrayBufferToBase64url(credential.response.clientDataJSON);
const authenticatorData = arrayBufferToBase64url(credential.response.authenticatorData);
const signature = arrayBufferToBase64url(credential.response.signature);
const userHandle = credential.response.userHandle ? arrayBufferToBase64url(credential.response.userHandle) : null;
const rawId = arrayBufferToBase64url(credential.rawId);
// 3. Send Authentication Response to Server
const authenticationResult = await fetch("/authentication/finish", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: rawId,
type: credential.type,
response: {
clientDataJSON: clientDataJSON,
authenticatorData: authenticatorData,
signature: signature,
userHandle: userHandle
}
})
}).then(response => response.text());
alert(authenticationResult);
}
解释:
- 前端代码使用
navigator.credentials.create()和navigator.credentials.get()方法与浏览器和认证器进行交互。 - 需要将 Base64URL 编码的字符串转换为 ArrayBuffer,然后再传递给 WebAuthn API。
- 需要将 ArrayBuffer 转换为 Base64URL 编码的字符串,然后再发送给后端服务器。
5. 安全性考虑
- Challenge 的随机性: 确保 Challenge 是高度随机且不可预测的,以防止重放攻击。
- Client Data Hash 验证: 在后端验证 Client Data Hash,以确保客户端发送的数据没有被篡改。
- Attestation 验证: 验证 Attestation 对象,以确保认证器是可信的。
- 用户验证: 强制用户验证(例如,使用指纹或 PIN 码)以增加安全性。
- 传输层安全: 使用 HTTPS 协议来保护通信安全。
6. 注意事项
- 浏览器支持: 确保你的目标浏览器支持 WebAuthn API。
- 认证器兼容性: 不同的认证器可能具有不同的特性和功能。
- 错误处理: 在前端和后端实现完善的错误处理机制。
- 用户体验: 设计友好的用户界面,引导用户完成注册和认证过程。
- 密钥管理: 安全地存储和管理用户的公钥和凭证信息。
7. 总结
通过结合 Java 后端和 JavaScript 前端,我们成功地构建了一个基于 WebAuthn 的无密码身份验证系统。 这不仅提升了安全性,也改善了用户体验。希望今天的分享能帮助大家更好地理解和应用 WebAuthn 技术。