Java安全之SSL/TLS


在前面所講到的一些安全技術手段如:消息摘要、加解密算法、數字簽名和數據證書等,一般都不會由開發者直接地去使用,而是經過了一定的封裝,甚至形成了某些安全協議,再暴露出一定的接口來供開發者使用。因為直接使用這些安全手段,對開發者的學習成本太高,需要深入了解底層實現才行,而直接使用封裝后暴露出來的接口就容易多了。

在這些封裝與協議的背后,很多都使用到了SSL/TSL協議,其中最常見的HTTPS就是在HTTP協議的基礎上加入了SSL/TLS協議形成的,來保障Web訪問安全性。SSL/TLS協議包含兩個協議:SSL(Secure Socket Layer,安全套接字層)和TLS(Transport Layer Security,傳輸層安全)協議。SSL由Netscape公司研發,位於TCP/IP參考模型中的網絡傳輸層,作為網絡通訊提供安全及數據完完整性的一種安全協議。TLS是基於SSL協議之上的通用化協議,它同樣位於TCP/IP參考模型中的網絡傳輸層,作為SSL協議的繼承者,成為下一代網絡安全性與數據完整性安全協議。

SSL/TLS協議的具體實現與細節肯定是很復雜的,可以百度一下慢慢了解,下面主要列舉一下Java中經常遇見的與SSL/TLS有關的情況:

一、Tomcat中HTTPS協議的配置

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true"
maxThreads="150" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="conf/serverKeyStore.jks"
keystorePass="gitblit"
truststoreFile="conf/caKeyStore.p12"
truststorePass="gitblit"
truststoreType="pkcs12"/>

屬性解釋: port:協議監聽端口號 protocol: 協議實現類 maxThreads: 最大線程數 SSLEnabledschemesecuresslProtocol:基本上是固定配置 keystoreFile:服務器KeyStore文件路徑 keystorePass:服務器KeyStore密碼 clientAuth:是否驗證客戶端 truststoreFile:服務器信任KeyStore文件路徑 truststorePass:服務器信任KeyStore密碼 truststoreType:服務器信任KeyStore類型,如不指定,默認為jks

在服務器KeyStore文件中最好只有一個條目,當然條目為KeyEntry類型,因為在該配置中無法配置被用於安全通信的條目別名,如果有多個條目的話,服務器會任意選行一個條目,就會造成所使用條目不確定的情況。服務器信任KeyStore中存儲的條目是CertificateEntry類型,正確情況下里面只存儲了服務器信任的證書。一般說來這些證書都是有一根證書頒發給客戶端使用的,而這個根證書肯定是服務器所有的,所以服務器信任KeyStore中最好只存儲一個為客戶端頒發證書的根證書,這樣只要信任了該根證書,也就會信任該根證書頒發的所有證書,利於添加新的客戶端。當然還有一種更笨的做法就是將根證書頒發給客戶端使用的證書全部添加到服務器信任KeyStore文件中。如果clientAuth配置為false,即單向認證,只認證服務端而不,而不認證客戶端,那么,truststoreFile, truststorePass, truststoreType這幾個屬性是用不上的,可不配置。

二、SSLSocket中使用

package com.xtayfjpk.security.jsse;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyStore;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.junit.Test;

public class SSLSocketTest {

@Test
public void testRunServer() throws Exception {
//獲取SSL上下文
SSLContext context = SSLContext.getInstance("SSL");
String keyStorePassword = "password";
//獲取服務端KeyStore
KeyStore serverKeys = getKeyStore("serverKeys", "jks", keyStorePassword);
//獲取KeyManagerFactory
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
String privateKeyPassword = "password";
//初始化KeyManagerFactory
keyManagerFactory.init(serverKeys, privateKeyPassword.toCharArray());

//獲取TrustManagerFactory
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String trustStorePassword = "password";
//獲取服務端信任KeyStore
KeyStore serverTrustKeys = getKeyStore("serverTrust", "jks", trustStorePassword);
//初始化TrustManagerFactory
trustManagerFactory.init(serverTrustKeys);
//初始化SSL上下文
context.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

//使用SSL上下文獲取SSLServerSocketFactory
SSLServerSocketFactory ssf = (SSLServerSocketFactory) context.getServerSocketFactory();
//使用SSLServerSocketFactory創建出SSLServerSocket,並監聽指定端口
SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(9999);
//設置需要對客戶端進行認證
serverSocket.setNeedClientAuth(true);

while(true) {
try {
//等待客戶端連接
SSLSocket socket = (SSLSocket) serverSocket.accept();
InputStream in = socket.getInputStream();

byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}

}

@Test
public void testRunClient() throws Exception {
SSLContext context = SSLContext.getInstance("SSL");
String keyStorePassword = "password";
KeyStore clientKeys = SimpleSSLServer.getKeyStore("clientKeys", "jks", keyStorePassword);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
String privateKeyPassword = "xtayfjpk";
keyManagerFactory.init(clientKeys, privateKeyPassword.toCharArray());

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String trustStorePassword = "password";
KeyStore serverTrustKeys = getKeyStore("clientTrust", "jks", trustStorePassword);
trustManagerFactory.init(serverTrustKeys);
context.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

//使用SSL上下文創建SSLSocket
SSLSocketFactory factory = (SSLSocketFactory) context.getSocketFactory();

String host = "127.0.0.1";
//創建SSLSocket
SSLSocket socket = (SSLSocket) factory.createSocket(host, 9999);
//與服務端進行通信
OutputStream outputStream = socket.getOutputStream();
outputStream.write("xtayfjpk".getBytes());
outputStream.flush();
outputStream.close();
socket.close();

}

public static KeyStore getKeyStore(String keyStorePath, String type, String keyStorePassword) throws Exception {
KeyStore keyStore = KeyStore.getInstance(type);
FileInputStream in = new FileInputStream(keyStorePath);
keyStore.load(in, keyStorePassword.toCharArray());
in.close();
return keyStore;
}
}

能過上面的例子能夠發現,這和Tomcat的https協議的配置是一致的,因為Tomcat底層肯定也是使用SSLSocket來實現https協議的。SSLServerSocketFactory還有個getDefault方法直接返回一個SSLServerSocketFactory實例,如果使用此方法不需要創建與初始化SSLContext,有關SSL相關的配置設置在系統屬性中,設置系統屬性的方式有兩種:一是在虛擬機啟動的時候設置,如下所示:

-Djavax.net.ssl.keyStore=clientKeys
-Djavax.net.ssl.keyStorePassword=password
-Djavax.net.ssl.trustStore=clientTrust
-Djavax.net.ssl.trustStorePassword=password

二是通過System.setProperty設置。

三、HttpsURLConnection中使用

package com.xtayfjpk.security.jsse;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyStore;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.junit.Test;

public class HttpsUrlConnectionTest {

@Test
public void test() throws Exception {
URL url = new URL("https://localhost:8443/");
HttpsURLConnection connection = HttpsURLConnection.class.cast(url.openConnection());


SSLContext context = SSLContext.getInstance("SSL");
String keyStorePassword = "gitblit";
KeyStore clientKeys = SimpleSSLServer.getKeyStore("D:\\java-app\\apache-tomcat-6.0.35\\conf\\clientKeyStore.p12", "pkcs12", keyStorePassword);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
String privateKeyPassword = "gitblit";
keyManagerFactory.init(clientKeys, privateKeyPassword.toCharArray());

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String trustStorePassword = "gitblit";
KeyStore serverTrustKeys = getKeyStore("D:\\java-app\\apache-tomcat-6.0.35\\conf\\clientTrustStore.jks", "jks", trustStorePassword);
trustManagerFactory.init(serverTrustKeys);
context.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

//使用SSL上下文創建SSLSocket
SSLSocketFactory factory = (SSLSocketFactory) context.getSocketFactory();
//如果服務端沒設置需要認證客戶端的話,可以不用設置SSLSocketFactory
connection.setSSLSocketFactory(factory);

connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
String line = null;
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while((line=reader.readLine())!=null) {
System.out.println(line);
}
}

public static KeyStore getKeyStore(String keyStorePath, String type, String keyStorePassword) throws Exception {
KeyStore keyStore = KeyStore.getInstance(type);
FileInputStream in = new FileInputStream(keyStorePath);
keyStore.load(in, keyStorePassword.toCharArray());
in.close();
return keyStore;
}
}

四、使服務端KeyStore支持多條目,並可指定被使用條目別名

在Tomcat中HTTPS協議的配置時說到,服務器KeyStore最好只有一個條目,否則會造成所使用條目不確定的情況。但有時候你可能會想,該KeyStore中存儲多個條目,在啟動時通過配置條目別名來指定具體的條目,因為Tomcat中沒有提供別名配置支持,所以KeyStore中最好還是只有一個條目。但如果是自己寫SSLSocket程序,可能通過擴展來支持,如下:

package com.xtayfjpk.security;

import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509KeyManager;

public class MyAliasedX509ExtendKeyManager extends X509ExtendedKeyManager {
private String keyAlias;
private X509KeyManager keyManager;

public MyAliasedX509ExtendKeyManager(String keyAlias, X509KeyManager keyManager) {
this.keyAlias = keyAlias;
this.keyManager = keyManager;
}


//提供給客戶端使用,用於選擇客戶端Keystore中的一個別名
@Override
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
String alias = keyAlias==null ? keyManager.chooseClientAlias(keyTypes, issuers, socket) : keyAlias;
return alias;
}

//提供給服務端使用,用於選擇服務端Keystore中的一個別名
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
String alias = keyAlias==null ? keyManager.chooseServerAlias(keyType, issuers, socket) : keyAlias;
return alias;
}

@Override
public X509Certificate[] getCertificateChain(String alias) {
return keyManager.getCertificateChain(alias);
}

@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return keyManager.getClientAliases(keyType, issuers);
}

@Override
public PrivateKey getPrivateKey(String alias) {
return keyManager.getPrivateKey(alias);
}

@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return keyManager.getServerAliases(keyType, issuers);
}

@Override
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
String alias = keyAlias==null ? super.chooseEngineClientAlias(keyType, issuers, engine) : keyAlias;
return alias;
}

@Override
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
String alias = keyAlias==null ? super.chooseEngineServerAlias(keyType, issuers, engine) : keyAlias;
return alias;
}
}

通過繼承X509ExtendedKeyManager,自己實現一個KeyManager,別名通過構造方法傳入,然后使用自己的KeyManager實現類包裝KeyManagerFactory創建的KeyManager即可通過別名達到指定KeyStore中被使用條目的目的。

------------------ END ---------------------
及時獲取更多精彩文章,請掃碼關注如下公眾號《Java精講》:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM