1. 什麼是SSLSocket
JDK文檔指出,SSLSocket擴展Socket並提供使用SSL或TLS協議的安全套接字。
這種套接字是正常的流套接字,但是它們在基礎網絡傳輸協議(如TCP)上添加了安全保護層。
具體安全方面的討論見下一篇。本篇重點關注SSLSocket及相關幾個類的使用。
2. SSLSocket和相關類
SSLSocket來自jsse(Java Secure Socket Extension)。
(1)SSLContext: 此類的實例表示安全套接字協議的實現, 它是SSLSocketFactory、SSLServerSocketFactory和SSLEngine的工廠。
(2)SSLSocket: 擴展自Socket
(3)SSLServerSocket: 擴展自ServerSocket
(4)SSLSocketFactory: 抽象類,擴展自SocketFactory, SSLSocket的工廠
(5)SSLServerSocketFactory: 抽象類,擴展自ServerSocketFactory, SSLServerSocket的工廠
(6)KeyStore: 表示密鑰和證書的存儲設施
(7)KeyManager: 接口,JSSE密鑰管理器
(8)TrustManager: 接口,信任管理器(?翻譯得很拗口)
(9)X590TrustedManager: TrustManager的子接口,管理X509證書,驗證遠程安全套接字
3. SSLContext的使用
public static void main(String[] args) throws Exception {
X509TrustManager x509m = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
};
// 獲取一個SSLContext實例
SSLContext s = SSLContext.getInstance("SSL");
// 初始化SSLContext實例
s.init(null, new TrustManager[] { x509m },
new java.security.SecureRandom());
// 打印這個SSLContext實例使用的協議
System.out.println("缺省安全套接字使用的協議: " + s.getProtocol());
// 獲取SSLContext實例相關的SSLEngine
SSLEngine e = s.createSSLEngine();
System.out
.println("支持的協議: " + Arrays.asList(e.getSupportedProtocols()));
System.out.println("啓用的協議: " + Arrays.asList(e.getEnabledProtocols()));
System.out.println("支持的加密套件: "
+ Arrays.asList(e.getSupportedCipherSuites()));
System.out.println("啓用的加密套件: "
+ Arrays.asList(e.getEnabledCipherSuites()));
}
運行結果如下:
SSLContext.getProtocol(): 返回當前SSLContext對象的協議名稱
SSLContext.init(): 初始化當前SSLContext對象。 三個參數均可以爲null。 詳見JDK文檔。
SSLEngine.getSupportedProtocols()等幾個方法可以返回些 Engine上支持/已啓用的協議、支持/已啓用的加密套件
4. SSLSocket和SSLServerSocket的使用
這兩個類的用法跟Socket/ServerSocket的用法比較類似。看下面的例子(主要爲了驗證SSLSocket的用法 ,I/O和多線程處理比較隨意)
4.1 SSLServerSocket
(1)新建一個SSLServerSocket,並開始監聽來自客戶端的連接
// 拋出異常
// javax.net.ssl.SSLException: No available certificate or key corresponds
// to the SSL cipher suites which are enabled.
public static void notOk() throws IOException {
SSLServerSocketFactory factory = (SSLServerSocketFactory) SSLServerSocketFactory
.getDefault();
SSLServerSocket server = (SSLServerSocket) factory
.createServerSocket(10000);
System.out.println("ok");
server.accept();
}
server.accept()處拋出異常, 提示缺少證書。與ServerSocket不同, SSLServerSocket需要證書來進行安全驗證。
使用keytool工具生成一個證書。 步驟如下, 得到一個名爲cmkey的證書文件
(2)重新完善上面的代碼。 主要增加兩個功能: 使用名爲cmkey的證書初始化SSLContext, echo客戶端的消息。 代碼如下
// 啓動一個ssl server socket
// 配置了證書, 所以不會拋出異常
public static void sslSocketServer() throws Exception {
// key store相關信息
String keyName = "cmkey";
char[] keyStorePwd = "123456".toCharArray();
char[] keyPwd = "123456".toCharArray();
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
// 裝載當前目錄下的key store. 可用jdk中的keytool工具生成keystore
InputStream in = null;
keyStore.load(in = Test2.class.getClassLoader().getResourceAsStream(
keyName), keyPwd);
in.close();
// 初始化key manager factory
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory
.getDefaultAlgorithm());
kmf.init(keyStore, keyPwd);
// 初始化ssl context
SSLContext context = SSLContext.getInstance("SSL");
context.init(kmf.getKeyManagers(),
new TrustManager[] { new MyX509TrustManager() },
new SecureRandom());
// 監聽和接收客戶端連接
SSLServerSocketFactory factory = context.getServerSocketFactory();
SSLServerSocket server = (SSLServerSocket) factory
.createServerSocket(10002);
System.out.println("ok");
Socket client = server.accept();
System.out.println(client.getRemoteSocketAddress());
// 向客戶端發送接收到的字節序列
OutputStream output = client.getOutputStream();
// 當一個普通 socket 連接上來, 這裏會拋出異常
// Exception in thread "main" javax.net.ssl.SSLException: Unrecognized
// SSL message, plaintext connection?
InputStream input = client.getInputStream();
byte[] buf = new byte[1024];
int len = input.read(buf);
System.out.println("received: " + new String(buf, 0, len));
output.write(buf, 0, len);
output.flush();
output.close();
input.close();
// 關閉socket連接
client.close();
server.close();
}
4.2 SSLSocket
(1)我們先使用一個普通的Socket嘗試連接服務器端
// 通過socket連接服務器
public static void socket() throws UnknownHostException, IOException {
Socket s = new Socket("localhost", 10002);
System.out.println(s);
System.out.println("ok");
OutputStream output = s.getOutputStream();
InputStream input = s.getInputStream();
output.write("alert".getBytes());
System.out.println("sent: alert");
output.flush();
byte[] buf = new byte[1024];
int len = input.read(buf);
System.out.println("received:" + new String(buf, 0, len));
}
結果客戶端和服務器端都出錯。 客戶端的錯誤是接收到亂碼。
服務器則拋出異常
javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?
(2)改成SSLSocket, 但是不使用證書。客戶端拋出sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
// 不使用證書, 通過ssl socket連接服務器
// 拋出異常, 提示找不到證書
public static void sslSocket() throws UnknownHostException, IOException {
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory
.getDefault();
SSLSocket s = (SSLSocket) factory.createSocket("localhost", 10002);
System.out.println("ok");
OutputStream output = s.getOutputStream();
InputStream input = s.getInputStream();
output.write("alert".getBytes());
System.out.println("sent: alert");
output.flush();
byte[] buf = new byte[1024];
int len = input.read(buf);
System.out.println("received:" + new String(buf, 0, len));
}
程序客戶在不持有證書的情況下直接進行連接,服務器端會產生運行時異常javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown,不允許進行連接。 我們可以指定像下面這樣執行客戶端,服務器端可以成功echo客戶端的發出的字符串"alert"
java -Djavax.net.ssl.trustStore=cmkey Client
這裏的cmkey即前面生成的證書文件。
(3)改成SSLSocket, 對SSLContext進行如下初始化。
public static void sslSocket2() throws Exception {
SSLContext context = SSLContext.getInstance("SSL");
// 初始化
context.init(null,
new TrustManager[] { new Test2.MyX509TrustManager() },
new SecureRandom());
SSLSocketFactory factory = context.getSocketFactory();
SSLSocket s = (SSLSocket) factory.createSocket("localhost", 10002);
System.out.println("ok");
OutputStream output = s.getOutputStream();
InputStream input = s.getInputStream();
output.write("alert".getBytes());
System.out.println("sent: alert");
output.flush();
byte[] buf = new byte[1024];
int len = input.read(buf);
System.out.println("received:" + new String(buf, 0, len));
}