SSL/TLS 雙向認證是保障客戶端與伺服器雙向身份驗證、通訊加密的核心手段。本文系統性講解 Java 實現雙向 SSL 的完整流程:憑證生成、Keystore/Truststore 配置、客戶端/伺服器端程式碼、常見問題排查。
| 術語 | 說明 | 類比 |
|---|---|---|
| Keystore | 存放自己的私鑰與身份憑證 | 身分證 + 私鑰保險箱 |
| Truststore | 存放信任方的公鑰憑證(CA 或自簽) | 信任的聯繫人名單 |
| 私鑰 | 解密/簽名,絕不外洩 | 只有自己有的鑰匙 |
| 公鑰憑證 | 加密/驗簽,可公開分享 | 公開的鎖/身分證副本 |
| CA | 憑證授權機構,簽發受信任憑證 | 公證處 |
Client Server │ │ ├───────── Client Hello ────────────────▶│ │ │ │◀──────── Server Hello + Certificate ───┤ │ (Server 公鑰憑證) │ │ │ │ 驗證 Server 憑證 (Truststore) │ │ ✓ 信任 CA / 自簽在 Truststore 中 │ │ │ ├───────── Certificate Request ──────────▶│ │ (要求 Client 憑證) │ │ │ │◀──────── Certificate Request ──────────┤ │ │ │ 發送 Client Certificate │ │ (Client 公鑰憑證) │ ├───────── Client Certificate ───────────▶│ │ │ │ 驗證 Client 憑證 (Truststore) │ │ ✓ 信任 CA / 自簽在 Truststore 中 │ │ │ │ 完成握手,建立加密通道 │ │◀──────── Server Hello Done ────────────┤ │ │ │ 加密應用層資料傳輸 │ ├──────────── Encrypted Data ───────────▶│ │◀──────────── Encrypted Data ───────────┤
mkdir -p ssl-demo/{server,client}
cd ssl-demo
# 1. 生成伺服器私鑰與自簽憑證
keytool -genkeypair \
-alias server \
-keyalg RSA \
-keysize 2048 \
-validity 365 \
-dname "CN=localhost, OU=Dev, O=MyCompany, L=Beijing, ST=Beijing, C=CN" \
-keypass server123 \
-keystore server/server.keystore.jks \
-storepass server123 \
-storetype JKS
# 2. 導出伺服器公鑰憑證
keytool -exportcert \
-alias server \
-keystore server/server.keystore.jks \
-storepass server123 \
-file server/server.cer
# 3. 查看憑證詳情
keytool -list -v -keystore server/server.keystore.jks -storepass server123
keytool -genkeypair \
-alias client \
-keyalg RSA \
-keysize 2048 \
-validity 365 \
-dname "CN=client1, OU=Dev, O=MyCompany, L=Beijing, ST=Beijing, C=CN" \
-keypass client123 \
-keystore client/client.keystore.jks \
-storepass client123 \
-storetype JKS
keytool -exportcert \
-alias client \
-keystore client/client.keystore.jks \
-storepass client123 \
-file client/client.cer
# 伺服器 Truststore:導入客戶端憑證
keytool -importcert \
-alias client \
-file client/client.cer \
-keystore server/server.truststore.jks \
-storepass server123 \
-noprompt
# 客戶端 Truststore:導入伺服器憑證
keytool -importcert \
-alias server \
-file server/server.cer \
-keystore client/client.truststore.jks \
-storepass client123 \
-noprompt
# 驗證
keytool -list -v -keystore server/server.truststore.jks -storepass server123
keytool -list -v -keystore client/client.truststore.jks -storepass client123
ssl-demo/
├── server/
│ ├── server.keystore.jks # 伺服器私鑰 + 憑證
│ ├── server.truststore.jks # 信任的客戶端憑證
│ └── server.cer # 伺服器公鑰憑證
└── client/
├── client.keystore.jks # 客戶端私鑰 + 憑證
├── client.truststore.jks # 信任的伺服器憑證
└── client.cer # 客戶端公鑰憑證
import javax.net.ssl.*;
import java.io.*;
import java.net.ServerSocket;
import java.security.KeyStore;
public class SslServer {
private static final int PORT = 8443;
private static final String KEYSTORE_PATH = "server/server.keystore.jks";
private static final String KEYSTORE_PASS = "server123";
private static final String TRUSTSTORE_PATH = "server/server.truststore.jks";
private static final String TRUSTSTORE_PASS = "server123";
public static void main(String[] args) throws Exception {
// 1. 載入 Keystore (私鑰)
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream(KEYSTORE_PATH), KEYSTORE_PASS.toCharArray());
// 2. 載入 Truststore (信任的客戶端憑證)
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream(TRUSTSTORE_PATH), TRUSTSTORE_PASS.toCharArray());
// 3. 初始化 KeyManagerFactory
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, KEYSTORE_PASS.toCharArray());
// 4. 初始化 TrustManagerFactory
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
// 5. 建立 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// 6. 建立 SSLServerSocketFactory
SSLServerSocketFactory ssf = sslContext.getServerSocketFactory();
SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(PORT);
// 關鍵:強制要求客戶端憑證
serverSocket.setNeedClientAuth(true);
// 可選:支援的協定/加密套件
serverSocket.setEnabledProtocols(new String[]{"TLSv1.3", "TLSv1.2"});
System.out.println("SSL Server started on port " + PORT + " (雙向認證)");
while (true) {
try (SSLSocket clientSocket = (SSLSocket) serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
// 可選:檢查客戶端憑證詳情
SSLSession session = clientSocket.getSession();
javax.security.cert.X509Certificate[] clientCerts = session.getPeerCertificateChain();
if (clientCerts != null && clientCerts.length > 0) {
System.out.println("Client CN: " + clientCerts[0].getSubjectDN().getName());
}
String msg = in.readLine();
System.out.println("Received: " + msg);
out.println("Server Echo: " + msg);
} catch (IOException e) {
System.err.println("Client error: " + e.getMessage());
}
}
}
}
import javax.net.ssl.*;
import java.io.*;
import java.net.Socket;
import java.security.KeyStore;
public class SslClient {
private static final String HOST = "localhost";
private static final int PORT = 8443;
private static final String KEYSTORE_PATH = "client/client.keystore.jks";
private static final String KEYSTORE_PASS = "client123";
private static final String TRUSTSTORE_PATH = "client/client.truststore.jks";
private static final String TRUSTSTORE_PASS = "client123";
public static void main(String[] args) throws Exception {
// 1. 載入 Keystore (自己的私鑰)
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream(KEYSTORE_PATH), KEYSTORE_PASS.toCharArray());
// 2. 載入 Truststore (信任的伺服器憑證)
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream(TRUSTSTORE_PATH), TRUSTSTORE_PASS.toCharArray());
// 3. 初始化 KeyManagerFactory
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, KEYSTORE_PASS.toCharArray());
// 4. 初始化 TrustManagerFactory
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
// 5. 建立 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// 6. 建立 SSLSocketFactory 並連線
SSLSocketFactory sf = sslContext.getSocketFactory();
try (SSLSocket socket = (SSLSocket) sf.createSocket(HOST, PORT);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
// 可選:驗證伺服器主機名
SSLParameters params = new SSLParameters();
params.setEndpointIdentificationAlgorithm("HTTPS");
socket.setSSLParameters(params);
System.out.println("Connected to server. Cipher: " + socket.getSession().getCipherSuite());
out.println("Hello from Client!");
String response = in.readLine();
System.out.println("Server response: " + response);
}
}
}
# application.yml
server:
port: 8443
ssl:
enabled: true
key-store: classpath:server/server.keystore.jks
key-store-password: server123
key-store-type: JKS
key-alias: server
trust-store: classpath:server/server.truststore.jks
trust-store-password: server123
trust-store-type: JKS
client-auth: need # 雙向認證關鍵:need = 強制, want = 可選
protocol: TLSv1.3
enabled-protocols: TLSv1.3,TLSv1.2
| 錯誤現象 | 可能原因 | 解決方案 |
|---|---|---|
| handshake_failure | 協定/加密套件不匹配 | 確認雙端支援相同 TLS 版本,檢查 enabledProtocols |
| certificate_unknown | 對方憑證不在 Truststore | 檢查 Truststore 是否正確導入對方憑證 |
| bad_certificate | 憑證格式錯誤/過期/吊銷 | 檢查憑證有效期、keytool -list 驗證 |
| unrecognized_name | SNI 主機名不匹配 | 客戶端設定 EndpointIdentificationAlgorithm 或關閉 SNI |
| Keystore was tampered | 密碼錯誤/檔案損壞 | 確認 storepass/keypass 正確,重新生成 |
| No available certificate | Keystore 無私鑰項 | 用 keytool -list 確認 PrivateKeyEntry 存在 |
# 1. 啟用 SSL 除錯日誌
java -Djavax.net.debug=ssl,handshake -jar your-app.jar
# 關鍵日誌解讀:
# *** ClientHello, TLSv1.3
# *** ServerHello, TLSv1.3
# *** Certificate chain (對方憑證鏈)
# *** CertificateRequest (請求客戶端憑證)
# *** ClientKeyExchange / CertificateVerify
# *** Finished
# *** Handshake completed
# 2. 使用 openssl 測試連線
openssl s_client -connect localhost:8443 -cert client/client.cer -key client/client.key -CAfile server/server.cer -tls1_3
# 3. 檢查憑證鏈
keytool -printcert -file server/server.cer
openssl x509 -in server/server.cer -text -noout
Java SSL 雙向認證的核心在於:雙方各持私鑰,交換公鑰憑證,通過 Truststore 驗證對方身份。掌握 keytool 憑證管理、SSLContext 初始化、setNeedClientAuth(true) 三個關鍵點,即可構建安全的雙向加密通道。生產環境建議配合自動化憑證管理、監控告警、定期演練輪換,確保長期安全運行。