Web 驗證 (WebAuthn) 允許使用智慧卡、安全性金鑰和生物特徵讀取器等裝置,為 MySQL 伺服器進行使用者驗證。WebAuthn 允許免密碼驗證,且可用於使用多重要素驗證的 MySQL 帳戶。自 8.2.0 版本以來,MySQL 企業版和 Connector/J 均支援此功能 — 如需詳細資訊,請參閱 WebAuthn 可插拔驗證。
以下說明如何將 WebAuthn 驗證與 Connector/J 搭配使用。此處假設有一個 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 Type 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!