在Java加密技術(八)中,我們模擬了一個基於RSA非對稱加密網絡的安全通信。現在我們深度了解一下現有的安全網絡通信——SSL。
我們需要構建一個由CA機構簽發的有效證書,這裡我們使用上文中生成的自簽名證書zlex.cer
這裡,我們將證書導入到我們的密鑰庫。
Shell代碼
keytool -import -alias www.zlex.org -file d:/zlex.cer -keystore d:/zlex-client.keystore
其中
-import表示導入
-alias指定別名,這裡是www.zlex.org
-file指定算法,這裡是d:/zlex.cer
-keystore指定存儲位置,這裡是d:/zlex-client.keystore
在這裡我使用的密碼為654321
控制台輸出:
Console代碼
輸入keystore密碼:
再次輸入新密碼:
所有者:CN=www.zlex.org, OU=zlex, O=zlex, L=BJ, ST=BJ, C=CN
簽發人:CN=www.zlex.org, OU=zlex, O=zlex, L=BJ, ST=BJ, C=CN
序列號:4a1e48df
有效期: Thu May 28 16:18:39 CST 2009 至Wed Aug 26 16:18:39 CST 2009
證書指紋:
MD5:19:CA:E6:36:E2:DF:AD:96:31:97:2F:A9:AD:FC:37:6A
SHA1:49:88:30:59:29:45:F1:69:CA:97:A9:6D:8A:CF:08:D2:C3:D5:C0:C4
簽名算法名稱:SHA1withRSA
版本: 3
信任這個認證? [否]: y
認證已添加至keystore中
OK,最復雜的准備工作已經完成。
接下來我們將域名www.zlex.org定位到本機上。打開C:\Windows\System32\drivers\etc\hosts文件,將www.zlex.org綁定在本機上。在文件末尾追加127.0.0.1 www.zlex.org。現在通過地址欄訪問http://www.zlex.org,或者通過ping命令,如果能夠定位到本機,域名映射就搞定了。
現在,配置tomcat。先將zlex.keystore拷貝到tomcat的conf目錄下,然後配置server.xml。將如下內容加入配置文件
Xml代碼
<Connector
SSLEnabled="true"
URIEncoding="UTF-8"
clientAuth="false"
keystoreFile="conf/zlex.keystore"
keystorePass="123456"
maxThreads="150"
port="443"
protocol="HTTP/1.1"
scheme="https"
secure="true"
sslProtocol="TLS" />
注意clientAuth="false"測試階段,置為false,正式使用時建議使用true。現在啟動tomcat,訪問https://www.zlex.org/。
顯然,證書未能通過認證,這個時候你可以選擇安裝證書(上文中的zlex.cer文件就是證書),作為受信任的根證書頒發機構導入,再次重啟浏覽器(IE,其他浏覽器對於域名www.zlex.org不支持本地方式訪問),訪問https://www.zlex.org/,你會看到地址欄中會有個小鎖,就說明安裝成功。所有的浏覽器聯網操作已經在RSA加密解密系統的保護之下了。但似乎我們感受不到。
這個時候很多人開始懷疑,如果我們要手工做一個這樣的https的訪問是不是需要把浏覽器的這些個功能都實現呢?不需要!
接著上篇內容,給出如下代碼實現:
Java代碼
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
import javax.crypto.Cipher;
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;
/**
* 證書組件
*
* @author 梁棟
* @version 1.0
* @since 1.0
*/
public abstract class CertificateCoder extends Coder {
/**
* Java密鑰庫(Java Key Store,JKS)KEY_STORE
*/
public static final String KEY_STORE = "JKS";
public static final String X509 = "X.509";
public static final String SunX509 = "SunX509";
public static final String SSL = "SSL";
/**
* 由KeyStore獲得私鑰
*
* @param keyStorePath
* @param alias
* @param password
* @return
* @throws Exception
*/
private static PrivateKey getPrivateKey(String keyStorePath, String alias,
String password) throws Exception {
KeyStore ks = getKeyStore(keyStorePath, password);
PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray());
return key;
}
/**
* 由Certificate獲得公鑰
*
* @param certificatePath
* @return
* @throws Exception
*/
private static PublicKey getPublicKey(String certificatePath)
throws Exception {
Certificate certificate = getCertificate(certificatePath);
PublicKey key = certificate.getPublicKey();
return key;
}
/**
* 獲得Certificate
*
* @param certificatePath
* @return
* @throws Exception
*/
private static Certificate getCertificate(String certificatePath)
throws Exception {
CertificateFactory certificateFactory = CertificateFactory
.getInstance(X509);
FileInputStream in = new FileInputStream(certificatePath);
Certificate certificate = certificateFactory.generateCertificate(in);
in.close();
return certificate;
}
/**
* 獲得Certificate
*
* @param keyStorePath
* @param alias
* @param password
* @return
* @throws Exception
*/
private static Certificate getCertificate(String keyStorePath,
String alias, String password) throws Exception {
KeyStore ks = getKeyStore(keyStorePath, password);
Certificate certificate = ks.getCertificate(alias);
return certificate;
}
/**
* 獲得KeyStore
*
* @param keyStorePath
* @param password
* @return
* @throws Exception
*/
private static KeyStore getKeyStore(String keyStorePath, String password)
throws Exception {
FileInputStream is = new FileInputStream(keyStorePath);
KeyStore ks = KeyStore.getInstance(KEY_STORE);
ks.load(is, password.toCharArray());
is.close();
return ks;
}
/**
* 私鑰加密
*
* @param data
* @param keyStorePath
* @param alias
* @param password
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, String keyStorePath,
String alias, String password) throws Exception {
// 取得私鑰
PrivateKey privateKey = getPrivateKey(keyStorePath, alias, password);
// 對數據加密
Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
/**
* 私鑰解密
*
* @param data
* @param keyStorePath
* @param alias
* @param password
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] data, String keyStorePath,
String alias, String password) throws Exception {
// 取得私鑰
PrivateKey privateKey = getPrivateKey(keyStorePath, alias, password);
// 對數據加密
Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
/**
* 公鑰加密
*
* @param data
* @param certificatePath
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String certificatePath)
throws Exception {
// 取得公鑰
PublicKey publicKey = getPublicKey(certificatePath);
// 對數據加密
Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
* 公鑰解密
*
* @param data
* @param certificatePath
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] data, String certificatePath)
throws Exception {
// 取得公鑰
PublicKey publicKey = getPublicKey(certificatePath);
// 對數據加密
Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
* 驗證Certificate
*
* @param certificatePath
* @return
*/
public static boolean verifyCertificate(String certificatePath) {
return verifyCertificate(new Date(), certificatePath);
}
/**
* 驗證Certificate是否過期或無效
*
* @param date
* @param certificatePath
* @return
*/
public static boolean verifyCertificate(Date date, String certificatePath) {
boolean status = true;
try {
// 取得證書
Certificate certificate = getCertificate(certificatePath);
// 驗證證書是否過期或無效
status = verifyCertificate(date, certificate);
} catch (Exception e) {
status = false;
}
return status;
}
/**
* 驗證證書是否過期或無效
*
* @param date
* @param certificate
* @return
*/
private static boolean verifyCertificate(Date date, Certificate certificate) {
boolean status = true;
try {
X509Certificate x509Certificate = (X509Certificate) certificate;
x509Certificate.checkValidity(date);
} catch (Exception e) {
status = false;
}
return status;
}
/**
* 簽名
*
* @param keyStorePath
* @param alias
* @param password
*
* @return
* @throws Exception
*/
public static String sign(byte[] sign, String keyStorePath, String alias,
String password) throws Exception {
// 獲得證書
X509Certificate x509Certificate = (X509Certificate) getCertificate(
keyStorePath, alias, password);
// 獲取私鑰
KeyStore ks = getKeyStore(keyStorePath, password);
// 取得私鑰
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, password
.toCharArray());
// 構建簽名
Signature signature = Signature.getInstance(x509Certificate
.getSigAlgName());
signature.initSign(privateKey);
signature.update(sign);
return encryptBASE64(signature.sign());
}
/**
* 驗證簽名
*
* @param data
* @param sign
* @param certificatePath
* @return
* @throws Exception
*/
public static boolean verify(byte[] data, String sign,
String certificatePath) throws Exception {
// 獲得證書
X509Certificate x509Certificate = (X509Certificate) getCertificate(certificatePath);
// 獲得公鑰
PublicKey publicKey = x509Certificate.getPublicKey();
// 構建簽名
Signature signature = Signature.getInstance(x509Certificate
.getSigAlgName());
signature.initVerify(publicKey);
signature.update(data);
return signature.verify(decryptBASE64(sign));
}
/**
* 驗證Certificate
*
* @param keyStorePath
* @param alias
* @param password
* @return
*/
public static boolean verifyCertificate(Date date, String keyStorePath,
String alias, String password) {
boolean status = true;
try {
Certificate certificate = getCertificate(keyStorePath, alias,
password);
status = verifyCertificate(date, certificate);
} catch (Exception e) {
status = false;
}
return status;
}
/**
* 驗證Certificate
*
* @param keyStorePath
* @param alias
* @param password
* @return
*/
public static boolean verifyCertificate(String keyStorePath, String alias,
String password) {
return verifyCertificate(new Date(), keyStorePath, alias, password);
}
/**
* 獲得SSLSocektFactory
*
* @param password
* 密碼
* @param keyStorePath
* 密鑰庫路徑
*
* @param trustKeyStorePath
* 信任庫路徑
* @return
* @throws Exception
*/
private static SSLSocketFactory getSSLSocketFactory(String password,
String keyStorePath, String trustKeyStorePath) throws Exception {
// 初始化密鑰庫
KeyManagerFactory keyManagerFactory = KeyManagerFactory
.getInstance(SunX509);
KeyStore keyStore = getKeyStore(keyStorePath, password);
keyManagerFactory.init(keyStore, password.toCharArray());
// 初始化信任庫
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(SunX509);
KeyStore trustkeyStore = getKeyStore(trustKeyStorePath, password);
trustManagerFactory.init(trustkeyStore);
// 初始化SSL上下文
SSLContext ctx = SSLContext.getInstance(SSL);
ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory
.getTrustManagers(), null);
SSLSocketFactory sf = ctx.getSocketFactory();
return sf;
}
/**
* 為HttpsURLConnection配置SSLSocketFactory
*
* @param conn
* HttpsURLConnection
* @param password
* 密碼
* @param keyStorePath
* 密鑰庫路徑
*
* @param trustKeyStorePath
* 信任庫路徑
* @throws Exception
*/
public static void configSSLSocketFactory(HttpsURLConnection conn,
String password, String keyStorePath, String trustKeyStorePath)
throws Exception {
conn.setSSLSocketFactory(getSSLSocketFactory(password, keyStorePath,
trustKeyStorePath));
}
}
增加了configSSLSocketFactory方法供外界調用,該方法為HttpsURLConnection配置了SSLSocketFactory。當HttpsURLConnection配置了SSLSocketFactory後,我們就可以通過HttpsURLConnection的getInputStream、getOutputStream,像往常使用HttpURLConnection做操作了。尤其要說明一點,未配置SSLSocketFactory前,HttpsURLConnection的getContentLength()獲得值永遠都是-1。
給出相應測試類:
Java代碼
import static org.junit.Assert.*; import java.io.DataInputStream; import java.io.InputStream; import java.net.URL; import javax.net.ssl.HttpsURLConnection; import org.junit.Test; /** * * @author 梁棟 * @version 1.0 * @since 1.0 */ public class CertificateCoderTest { private String password = "123456"; private String alias = "www.zlex.org"; private String certificatePath = "d:/zlex.cer"; private String keyStorePath = "d:/zlex.keystore"; private String clientKeyStorePath = "d:/zlex-client.keystore"; private String clientPassword = "654321"; @Test public void test() throws Exception { System.err.println("公鑰加密——私鑰解密"); String inputStr = "Ceritifcate"; byte[] data = inputStr.getBytes(); byte[] encrypt = CertificateCoder.encryptByPublicKey(data, certificatePath); byte[] decrypt = CertificateCoder.decryptByPrivateKey(encrypt, keyStorePath, alias, password); String outputStr = new String(decrypt); System.err.println("加密前: " + inputStr + "\n\r" + "解密後: " + outputStr); // 驗證數據一致 assertArrayEquals(data, decrypt); // 驗證證書有效 assertTrue(CertificateCoder.verifyCertificate(certificatePath)); } @Test public void testSign() throws Exception { System.err.println("私鑰加密——公鑰解密"); String inputStr = "sign"; byte[] data = inputStr.getBytes(); byte[] encodedData = CertificateCoder.encryptByPrivateKey(data, keyStorePath, alias, password); byte[] decodedData = CertificateCoder.decryptByPublicKey(encodedData, certificatePath); String outputStr = new String(decodedData); System.err.println("加密前: " + inputStr + "\n\r" + "解密後: " + outputStr); assertEquals(inputStr, outputStr); System.err.println("私鑰簽名——公鑰驗證簽名"); // 產生簽名 String sign = CertificateCoder.sign(encodedData, keyStorePath, alias, password); System.err.println("簽名:\r" + sign); // 驗證簽名 boolean status = CertificateCoder.verify(encodedData, sign, certificatePath); System.err.println("狀態:\r" + status); assertTrue(status); } @Test public void testHttps() throws Exception { URL url = new URL("https://www.zlex.org/examples/"); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); CertificateCoder.configSSLSocketFactory(conn, clientPassword, clientKeyStorePath, clientKeyStorePath); InputStream is = conn.getInputStream(); int length = conn.getContentLength(); DataInputStream dis = new DataInputStream(is); byte[] data = new byte[length]; dis.readFully(data); dis.close(); System.err.println(new String(data)); conn.disconnect(); } }
注意testHttps方法,幾乎和我們往常做HTTP訪問沒有差別,我們來看控制台輸出:
Console代碼
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD><TITLE>Apache Tomcat Examples</TITLE>
<META http-equiv=Content-Type content="text/html">
</HEAD>
<BODY>
<P>
<H3>Apache Tomcat Examples</H3>
<P></P>
<ul>
<li><a href="servlets">Servlets examples</a></li>
<li><a href="jsp">JSP Examples</a></li>
</ul>
</BODY></HTML>
通過浏覽器直接訪問https://www.zlex.org/examples/你也會獲得上述內容。也就是說應用甲方作為服務器構建tomcat服務,乙方可以通過上述方式訪問甲方受保護的SSL應用,並且不需要考慮具體的加密解密問題。甲乙雙方可以經過相應配置,通過雙方的tomcat配置有效的SSL服務,簡化上述代碼實現,完全通過證書配置完成SSL雙向認證!