基本概念
SSL(Secure Socket Layer)是netscape公司设计的主要用于web的安全传输协议。这种协议在WEB上获得了广泛的应用。 IETF(www.ietf.org)将SSL作了标准化,即RFC2246,并将其称为TLS(Transport Layer Security),从技术上讲,TLS 1.0与SSL 3.0的差别非常微小。
基本原理
先非对称加密传递对称加密所要用的钥匙,然后双方用该钥匙对称加密和解密往来的数据。
工作过程
浏览器向服务器发出请求,询问对方支持的对称加密算法和非对称加密算法;服务器回应自己支持的算法。
浏览器选择双方都支持的加密算法,并请求服务器出示自己的证书;服务器回应自己的证书。
浏览器随机产生一个用于本次会话的对称加密的钥匙,并使用服务器证书中附带的公钥对该钥匙进行加密后传递给服务器;服务器为本次会话保持该对称加密的钥匙。
第三方不知道服务器的私钥,即使截获了数据也无法解密。非对称加密让任何浏览器都可以与服务器进行加密会话。 浏览器使用对称加密的钥匙对请求消息加密后传送给服务器,服务器使用该对称加密的钥匙进行解密;服务器使用对称加密的钥匙对响应消息加密后传送给浏览器,浏览器使用该对称加密的钥匙进行解密。第三方不知道对称加密的钥匙,即使截获了数据也无法解密。对称加密提高了加密速度。 要求 服务器端需安装数字证书,用户可能需要确认证书。 会话过程中的加密与解密过程由浏览器与服务器自动完成,对用户完全透明。
使用keytool创建证书时,刚开始设置的用户名一定要是服务器的域名,否则,浏览器进行访问时也将提示证书有问题:
1.一种情况是发证机关是否值得信赖的,好比你拿出来的驾照是不是交管局这样的国家法定机构颁发的,还是你自己盖的章。
2.还有一种就是证书机构确实是交管局颁发的,但并不是发给你的,而是发给别人的,你出示别人的驾照时,人家交警会问,这是我们发的证,但是是发给你的吗?
SSL编程——默认参数方式
Socket编程是在http协议之上的,对应TCP协议。DatagramSocket编程对应UDP协议。使用Socket编程可以自定义应用层协议,当然可以自己构建HTTP协议。
服务器端程序
调用getDefault()静态方法得到SSLServerSocketFactory实例对象 需要通过javax.net.ssl.keyStore 和javax.net.ssl.keyStorePassword系统属性指定keystore的位置与密码,否则,KeyManager管理一个空的keystore,这可以通过在jsse文档中搜索getDefault关键字获知。
调用SSLServerSocketFactory对象的createServerSocket()方法得到ServerSocket。 循环调用ServerSocket.accept()等待外部连接,并启动新线程与每个连接的客户端进行对话。 打开浏览器进行访问测试,必须在keystore中存入与主机名相一致的keyEntry,且将该keyEntry的证书安装到浏览器中。 在这种采用默认参数的方式下,服务器端的keystore中只能存储一个keyEntry,否则,服务器程序就面临不知道选用哪个keyEntry的问题了。
客户端编程: 调用getDefault()静态方法得到SSLSocketFactory实例对象 需要通过javax.net.ssl.trustStore系统属性指定truststore的位置,由于不需要读取私钥信息,所以不用设置keystore的密码。 如果没有设置javax.net.ssl.trustStore系统属性,则查找<java-home>/lib/security/jssecacerts,没找到则接着查找<java-home>/lib/security/cacerts。 如果上面的默认的truststore没有找到,则TrustManager管理一个空的trueststore,这可以通过在jsse文档中搜索getDefault关键字获知。 调用SSLSocketFactory对象的createSocket()方法连接服务器,连接成功后与服务器进行对话。 运行客户端程序进行测试访问,如果服务器出示的证书不是由客户端已经信任的CA签名的,则必须在truststore中导入服务器端的证书(keytool -importcert -keystore ${JAVA_HOME}\jre\lib\security\cacerts -file zxx1.cer)。
服务器端代码:
public void init1()throws Exception{
String user_home = System.getProperty("user.home");
System.out.println(user_home);
System.setProperty("javax.net.ssl.keyStore",user_home + "/.keystore");
System.setProperty("javax.net.ssl.keyStorePassword", "123456");
ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
ServerSocket ss = factory.createServerSocket(443);
while(true){
Socket s = ss.accept();
new Thread(new Worker(s)).start();
}
}
private class Worker implements Runnable{
Socket s = null;
public Worker(Socket s){
this.s = s;
}
public void run() {
try {
byte buf[] = new byte[10240];
int len = s.getInputStream().read(buf);
if(len >0){
System.out.println(new String(buf,0,len));
s.getOutputStream().write("200 ok\r\n".getBytes());
s.getOutputStream().write("Content-length: 6\r\n\r\n".getBytes());
s.getOutputStream().write("1234567".getBytes());
s.getOutputStream().close();
s.getInputStream().close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端代码:关键的一点就是要导入服务器端的证书到自己的trustkeystore中。
private static void work1() throws Exception{
SSLSocketFactory factory = (SSLSocketFactory)SSLSocketFactory.getDefault();
Socket socket = factory.createSocket("localhost",443);
InputStream ips = socket.getInputStream() ;
OutputStream ops = socket.getOutputStream();
ops.write("GET / x".getBytes());
byte[] buf = new byte[1024];
int len = 0;
while((len=ips.read(buf))!=-1){
System.out.println(new String(buf,0,len));
}
ips.close();
ops.close();
}
总结:创建服务器端Socket时,应该指定自己的私钥和证书在哪和是什么;创建客户端Socket时,它要验证服务器端出示的证书是否是可信赖的,所以,对于信赖的证书应该导入到jre/lib/security/cacerts文件中。服务器端的证书由keyManagerFactory类管理,客户端的证书由TrustManagerFactory管理。
在JSSE中,即使客户端程序没有直接信任服务器所使用的证书,但服务器的证书是客户所信任的其他证书签发的,客户端程序照样可以正常运行。
SSL编程——定制SSLContext
服务器端程序
调用getInstance()静态方法得到SSLContext实例对象 调用SSLContext对象的init()方法进行初始化 调用init()方法时必须传入KeyManager数组, KeyManager数组通过KeyManagerFactory对象从keystore中去读取出来。 调用SSLContext对象的getServerSocketFactory()方法得到SSLServerSocketFactory实例对象,接着按照默认参数的例子编写后续代码。
客户端编程
调用getInstance()静态方法得到SSLContext实例对象 调用SSLContext对象的init()方法进行初始化 调用init()方法时必须传入TrustManager数组, TrustManager数组通过TrustManagerFactory对象从truststore中去读取出来。 调用SSLContext对象的getSocketFactory()方法得到SSLSocketFactory实例对象,接着按照默认参数的例子编写后续代码。
扩展
让服务器端程序从keystore里的多个KeyEntry中选择一个: 编写一个实现了X509KeyManager接口的自定义KeyManager类,这个自定义KeyManager类的chooseServerAlias方法返回要使用的keyEntry的别名, 而自定义KeyManager类中的其他方法则转发给原来通过KeyManagerFactory获得的KeyMananger对象。
服务器端代码:
public void init2() throws Exception{
String user_home = System.getProperty("user.home");
System.out.println(user_home);
char[] passphrase = "123456".toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(user_home + "/.keystore"), passphrase);
KeyManagerFactory kmf =
KeyManagerFactory.getInstance("SunX509");
//如果keystore中只有一个keyEntry,则此处表示keyEntry的密码可以与keystore的密码不同。
kmf.init(ks, passphrase);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
ServerSocketFactory factory = sslContext.getServerSocketFactory();
ServerSocket ss = factory.createServerSocket(443);
while(true){
Socket s = ss.accept();
new Thread(new Worker(s)).start();
}
}
这种定制方式可以让服务器端从多个keyEntry中选择一个keyEntry,
KeyManagerFactory.getInstance()方法接受的参数值可以在JSSE文档中搜索KeyManagerFactory看到,如何定制KeyManager的例子也可以在jsse文档中搜索,找到一个TrustManager定制的案例代码,可以推出如何定制KeyManager。
扩展后的服务器端代码:
public void init3() throws Exception{
String user_home = System.getProperty("user.home");
System.out.println(user_home);
char[] passphrase = "123456".toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(user_home + "/.keystore"), passphrase);
KeyManagerFactory kmf =
KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, passphrase);
KeyManager[] kms = kmf.getKeyManagers();
for(int i=0;i<kms.length;i++){
kms[i] = new ItcastKeyManager((X509KeyManager)kms[i],"mykey");
}
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(
kms, null, null);
ServerSocketFactory factory = sslContext.getServerSocketFactory();
ServerSocket ss = factory.createServerSocket(443);
while(true){
Socket s = ss.accept();
new Thread(new Worker(s)).start();
}
}
private class ItcastKeyManager implements X509KeyManager {
private X509KeyManager delegate;
private String serverKeyAlias;
public ItcastKeyManager(X509KeyManager delegate, String serverKeyAlias) {
super();
this.delegate = delegate;
this.serverKeyAlias = serverKeyAlias;
}
public String chooseClientAlias(String[] keyType, Principal[] issuers,
Socket socket) {
return delegate.chooseClientAlias(keyType, issuers, socket);
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers,
Socket socket) {
return serverKeyAlias;
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return delegate.getCertificateChain(alias);
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return delegate.getClientAliases(keyType, issuers);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return delegate.getPrivateKey(alias);
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return delegate.getServerAliases(keyType, issuers);
}
}
参考tomcat源码:找到了SSLImplementation的信息,然后根据这些SSLImplementation类找到相应的包,再找到下面的JSSEKeyManager.java文件
org\apache\coyote\http11\Http11Protocol.java
org\apache\tomcat\util\net\jsse\JSSEKeyManager.java
下面的程序演示了如何应用JSSEKeyManager:
org\apache\tomcat\util\net\jsse\JSSE14SocketFactory.java
客户端代码:运行SSL客户端程序时可以增加-Djavax.net.debug=all,在jsse文档搜索debug可以看到更详细的信息。
private static void work2() throws Exception{
char[] passphrase = "changeit".toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("${JAVA_HOME}\\jre\\lib\\security\\cacerts"), passphrase);
TrustManagerFactory managerFactory = TrustManagerFactory.getInstance("PKIX");
managerFactory.init(ks);
SSLContext context = SSLContext.getInstance("TLS");
context.init(null,managerFactory.getTrustManagers() , null);
SSLSocketFactory factory = context.getSocketFactory();
Socket socket = factory.createSocket("localhost",443);
InputStream ips = socket.getInputStream() ;
OutputStream ops = socket.getOutputStream();
ops.write("GET / x".getBytes());
byte[] buf = new byte[1024];
int len = 0;
while((len=ips.read(buf))!=-1){
System.out.println(new String(buf,0,len));
}
ips.close();
ops.close();
}
HTTPs协议编程
与TLS的区别
传输的数据内容应遵守Http协议格式 服务器出示的证书所有者的名称必须是URL地址中指定的主机名
基本步骤: 创建表示https协议路径的URL实例对象,并获得URLConnection对象。 调用URLConnection对象的setDoOutput()方法支持发送实体内容。 获得输出和输入流与服务器进行交互。
扩展步骤:在程序中定制truststore的位置 全局设置:使用javax.net.ssl.trustStore系统属性指定truststore的位置。 针对单个链接的设置:使用SSLContext对象的init()方法加载TrustManagerFactory对象从truststore中读取出来的TrustManager对象,然后调用SSLContext对象的getSocketFactory()方法得到SSLSocketFactory实例对象,最后再调用HttpsURLConnection 对象的setSSLSocketFactory方法。
代码1:
private static void https1() throws Exception{
URL url = new URL("https://localhost/abcd");
URLConnection connection = url.openConnection();
connection.setDoOutput(true);
OutputStream ops = connection.getOutputStream();
ops.write("GET / xsss".getBytes());
InputStream ips = connection.getInputStream() ;
byte[] buf = new byte[1024];
int len = 0;
while((len=ips.read(buf))!=-1){
System.out.println(new String(buf,0,len));
}
ips.close();
ops.close();
}
代码2:
private static void https2() throws Exception{
char[] passphrase = "changeit".toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("${JAVA_HOME}\\jre\\lib\\security\\cacerts"), passphrase);
TrustManagerFactory managerFactory = TrustManagerFactory.getInstance("PKIX");
managerFactory.init(ks);
SSLContext context = SSLContext.getInstance("TLS");
context.init(null,managerFactory.getTrustManagers() , null);
SSLSocketFactory factory = context.getSocketFactory();
URL url = new URL("https://localhost/abcd");
URLConnection connection = url.openConnection();
((HttpsURLConnection)connection).setSSLSocketFactory(factory);
connection.setDoOutput(true);
OutputStream ops = connection.getOutputStream();
ops.write("GET / x".getBytes());
InputStream ips = connection.getInputStream() ;
byte[] buf = new byte[1024];
int len = 0;
while((len=ips.read(buf))!=-1){
System.out.println(new String(buf,0,len));
}
ips.close();
ops.close();
}