Web 身份验证 (WebAuthn) 使得可以使用智能卡、安全密钥和生物识别读取器等设备对 MySQL 服务器进行用户身份验证。WebAuthn 支持无密码身份验证,并且可以用于使用多因素身份验证的 MySQL 帐户。自 8.2.0 版起,它受 MySQL 企业版和 Connector/J 支持,有关详细信息,请参阅 WebAuthn 可插拔身份验证。
以下内容说明了如何在 Connector/J 中使用 WebAuthn 身份验证。它假设有一个运行的 MySQL 服务器,并且配置为支持 WebAuthn 身份验证,身份验证插件 authentication_webauthn
已加载,并且系统变量 authentication_webauthn_rp_id
已正确配置。虽然并不总是如此,但 FIDO 身份验证通常与 多因素身份验证 配合使用,因此可能需要额外的配置,但通常情况下,默认的 MySQL 安装已准备好进行多因素身份验证。
创建 MySQL 用户
创建要链接到 FIDO 设备的 MySQL 用户。使用具有 root 用户权限的 mysql 客户端
mysql > CREATE USER 'johndoe'@'%' IDENTIFIED WITH caching_sha2_password BY 's3cr3t' AND IDENTIFIED WITH authentication_webauthn;
Query OK, 0 rows affected (0,02 sec)
通过您刚刚创建的用户注册 FIDO 设备。这可以通过在安装了设备的同一系统上运行 mysql 客户端来完成,这可能需要在您的工作机器上安装 mysql 客户端或将 FIDO 设备移动到 MySQL 服务器运行的系统。无论哪种情况,请发出以下命令(可能需要其他连接到正确服务器的命令选项)
$ mysql --user=johndoe --password1 --register-factor=2
Enter password: <type "s3cr3t">
Please insert FIDO device and follow the instruction. Depending on the device, you may have to perform gesture action multiple times.
1. Perform gesture action (Skip this step if you are prompted to enter device PIN).
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.2.0-commercial MySQL Enterprise Server - Commercial
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql >
获取第三方依赖项
MySQL Connector/J 是一个 JDBC 类型 4 驱动程序,它是 100% 纯 Java 实现,但是,没有纯 Java 库支持 Connector/J 可以使用的身份验证设备。因此,开发人员需要实现处理与身份验证设备交互的代码,为此需要以下第三方库。
libfido2
本机库,它必须安装在应用程序将要运行的系统中。-
一些 Java 绑定,例如 Java 本机接口 (JNI) 或 Java 本机访问 (JNA)。在以下示例中,Java 本机访问 (JNA) 用于在
libfido2
库之上实现我们最小的 Java 绑定。
实现本机绑定
创建一个简单的类(以下称为 FidoAssertion
),它实现了 Java 和 libfido2
本机库之间的最小绑定集(如果需要,请参阅 libfido2
手册)
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.PointerType;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
public class FidoAssertion {
private interface LibFido2 extends Library {
public static int FIDO_OK = 0;
static class FidoAssertT extends PointerType {}
static class FidoDevInfoT extends PointerType {}
static class FidoDevT extends PointerType {}
LibFido2 INSTANCE = Native.load("fido2", LibFido2.class);
int fido_assert_allow_cred(FidoAssertT assrt, byte[] ptr, int len);
int fido_assert_authdata_len(FidoAssertT assrt, int idx);
Pointer fido_assert_authdata_ptr(FidoAssertT assrt, int idx);
void fido_assert_free(PointerByReference assrt);
FidoAssertT fido_assert_new();
int fido_assert_count(FidoAssertT assrt);
int fido_assert_set_clientdata_hash(FidoAssertT assrt, byte[] ptr, int len);
int fido_assert_set_rp(FidoAssertT assrt, String id);
int fido_assert_sig_len(FidoAssertT assrt, int idx);
Pointer fido_assert_sig_ptr(FidoAssertT assrt, int idx);
int fido_dev_close(FidoDevT dev);
void fido_dev_free(PointerByReference dev);
int fido_dev_get_assert(FidoDevT dev, FidoAssertT assrt, String pin);
void fido_dev_info_free(PointerByReference devlist, int n);
int fido_dev_info_manifest(FidoDevInfoT devlist, int ilen, IntByReference olen);
FidoDevInfoT fido_dev_info_new(int n);
String fido_dev_info_path(FidoDevInfoT di);
FidoDevInfoT fido_dev_info_ptr(FidoDevInfoT devList, int size);
FidoDevT fido_dev_new();
int fido_dev_open(FidoDevT dev, String path);
boolean fido_dev_supports_credman(FidoDevT dev);
void fido_init(int flags);
}
private LibFido2.FidoAssertT fidoAssert;
private LibFido2.FidoDevT fidoDev;
private byte[] clientDataHash;
private String relyingPartyId;
private byte[] credentialId;
private boolean supportsCredMan = false;
public FidoAssertion() {
LibFido2.INSTANCE.fido_init(0);
initializeFidoDevice();
}
private void initializeFidoDevice() {
LibFido2.FidoDevInfoT fidoDevInfo = LibFido2.INSTANCE.fido_dev_info_new(1);
IntByReference olen = new IntByReference();
int r = LibFido2.INSTANCE.fido_dev_info_manifest(fidoDevInfo, 1, olen);
if (r != LibFido2.FIDO_OK) {
throw new RuntimeException("Failed locating FIDO devices.");
}
LibFido2.FidoDevInfoT dev = LibFido2.INSTANCE.fido_dev_info_ptr(fidoDevInfo, 0);
String path = LibFido2.INSTANCE.fido_dev_info_path(dev);
LibFido2.INSTANCE.fido_dev_info_free(new PointerByReference(fidoDevInfo.getPointer()), 1);
this.fidoDev = LibFido2.INSTANCE.fido_dev_new();
r = LibFido2.INSTANCE.fido_dev_open(this.fidoDev, path);
if (r != LibFido2.FIDO_OK) {
throw new RuntimeException("Failed opening the FIDO device.");
}
this.supportsCredMan = LibFido2.INSTANCE.fido_dev_supports_credman(this.fidoDev);
}
boolean supportsCredentialManagement() {
return this.supportsCredMan;
}
void setClienDataHash(byte[] clientDataHash) {
this.clientDataHash = clientDataHash;
}
void setRelyingPartyId(String relyingPartyId) {
this.relyingPartyId = relyingPartyId;
}
void setCredentialId(byte[] credentialId) {
this.credentialId = credentialId;
}
void computeAssertions() {
int r;
this.fidoAssert = LibFido2.INSTANCE.fido_assert_new();
// Set the Relying Party Id.
r = LibFido2.INSTANCE.fido_assert_set_rp(this.fidoAssert, this.relyingPartyId);
if (r != LibFido2.FIDO_OK) {
throw new RuntimeException("Failed setting the relying party id.");
}
// Set the Client Data Hash.
r = LibFido2.INSTANCE.fido_assert_set_clientdata_hash(this.fidoAssert, this.clientDataHash, this.clientDataHash.length);
if (r != LibFido2.FIDO_OK) {
throw new RuntimeException("Failed setting the client data hash.");
}
// Set the Credential Id. Not applicable when resident keys are used.
if (this.credentialId.length > 0) {
r = LibFido2.INSTANCE.fido_assert_allow_cred(this.fidoAssert, this.credentialId, this.credentialId.length);
if (r != LibFido2.FIDO_OK) {
throw new RuntimeException("Failed setting the credential id.");
}
}
// Obtain the assertion(s) from the FIDO device.
r = LibFido2.INSTANCE.fido_dev_get_assert(this.fidoDev, this.fidoAssert, null);
if (r != LibFido2.FIDO_OK) {
throw new RuntimeException("Failed obtaining the assertion(s) from the FIDO device.");
}
}
public int getAssertCount() {
int assertCount = LibFido2.INSTANCE.fido_assert_count(this.fidoAssert);
return assertCount;
}
public byte[] getAuthenticatorData(int idx) {
int authDataLen = LibFido2.INSTANCE.fido_assert_authdata_len(this.fidoAssert, idx);
Pointer authData = LibFido2.INSTANCE.fido_assert_authdata_ptr(this.fidoAssert, idx);
byte[] authenticatorData = authData.getByteArray(0, authDataLen);
return authenticatorData;
}
public byte[] getSignature(int idx) {
int sigLen = LibFido2.INSTANCE.fido_assert_sig_len(this.fidoAssert, idx);
Pointer sigData = LibFido2.INSTANCE.fido_assert_sig_ptr(this.fidoAssert, idx);
byte[] signature = sigData.getByteArray(0, sigLen);
return signature;
}
public void freeResources() {
LibFido2.INSTANCE.fido_dev_close(this.fidoDev);
LibFido2.INSTANCE.fido_dev_free(new PointerByReference(this.fidoDev.getPointer()));
LibFido2.INSTANCE.fido_assert_free(new PointerByReference(this.fidoAssert.getPointer()));
}
}
使用 Java 8 编译器(或更高版本)编译该类。
$ javac -classpath *:. FidoAssertion.java
实现身份验证回调
MySQL Connector/J 使用一个可插拔回调类,该类在身份验证过程和与身份验证设备的交互之间交换数据。此类必须是接口 com.mysql.cj.callback.MysqlCallbackHandler
的实例,该接口定义了一个单一方法:void handle(MysqlCallback cb);
。此方法接受的 MysqlCallback
参数是 com.mysql.cj.callback.WebAuthnAuthenticationCallback
的实例,它包含 FIDO 断言代码之前实现所需的所有数据。同样,它还会将来自 FIDO 设备的输出(身份验证器数据和签名)传递到正在运行的身份验证过程。
以下是 WebAuthnAuthenticationCallback
的一种可能的实现方式。
import com.mysql.cj.callback.MysqlCallback;
import com.mysql.cj.callback.MysqlCallbackHandler;
import com.mysql.cj.callback.WebAuthnAuthenticationCallback;
public class AuthenticationWebAuthnCallbackHandler implements MysqlCallbackHandler {
@Override
public void handle(MysqlCallback cb) {
if (!WebAuthnAuthenticationCallback.class.isAssignableFrom(cb.getClass())) {
return;
}
WebAuthnAuthenticationCallback webAuthnAuthCallback = (WebAuthnAuthenticationCallback) cb;
FidoAssertion libFido2Assertion = new FidoAssertion();
webAuthnAuthCallback.setSupportsCredentialManagement(libFido2Assertion.supportsCredentialManagement());
libFido2Assertion.setClienDataHash(webAuthnAuthCallback.getClientDataHash());
libFido2Assertion.setRelyingPartyId(webAuthnAuthCallback.getRelyingPartyId());
libFido2Assertion.setCredentialId(webAuthnAuthCallback.getCredentialId());
System.out.println("Please perform the gesture action on your FIDO device.");
libFido2Assertion.computeAssertions();
for (int i = 0; i < libFido2Assertion.getAssertCount(); i++) {
webAuthnAuthCallback.addAuthenticatorData(libFido2Assertion.getAuthenticatorData(i));
webAuthnAuthCallback.addSignature(libFido2Assertion.getSignature(i));
}
libFido2Assertion.freeResources();
}
}
请注意,此实现负责要求用户执行手势操作。在实际用例中,这最终将触发一个事件,例如,为用户打开一个弹出消息。
编译此代码
$ javac -classpath *:. AuthenticationWebAuthnCallbackHandler.java
此类的名称必须通过连接属性 authenticationWebAuthnCallbackHandler
提供给 Connector/J。
实现应用程序
实现客户端应用程序。以下实现只是一个概念证明,它使用之前创建的用户创建到 MySQL 服务器的 MySQL 连接,并检查连接是否成功建立。请注意,FIDO 身份验证需要某种形式的人为交互,因此,这不是适用于典型三层体系结构的解决方案,在三层体系结构中,通常会在应用程序服务器中配置单个数据库用户,并且从远程机器建立到数据库的连接。
这是一个简单的客户端应用程序代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.util.Properties;
import com.mysql.cj.conf.PropertyKey;
public class AuthenticationWebAuthnApp {
private static final String HOST = "localhost";
private static final String PORT = "3306";
private static final String USER = "johndoe";
private static final String PASS = "s3cr3t";
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.setProperty(PropertyKey.authenticationWebAuthnCallbackHandler.getKeyName(), AuthenticationWebAuthnCallbackHandler.class.getName());
String url = "jdbc:mysql://" + USER + ":" + PASS + "@" + HOST + ":" + PORT + "/";
try (Connection conn = DriverManager.getConnection(url, props)) {
ResultSet rs = conn.createStatement().executeQuery("SELECT CURRENT_USER()");
rs.next();
System.out.println(rs.getString(1) + " AUTHENTICATED SUCCESSFULLY!");
}
}
}
编译代码
$ javac -classpath *:. AuthenticationWebAuthnApp.java
运行代码
$ /usr/lib/jvm/jdk-17/bin/java -classpath *:. AuthenticationWebAuthnApp
Please perform the gesture action on your FIDO device.
johndoe@% AUTHENTICATED SUCCESSFULLY!