SSL/TLS的工作原理
l 基本概念
ü SSL(Secure Socket Layer)是netscape公司设计的主要用于web的安全传输协议。这种协议在WEB上获得了广泛的应用。
ü IETF(www.ietf.org)将SSL作了标准化,即RFC2246,并将其称为TLS(Transport Layer Security),从技术上讲,TLS 1.0与SSL 3.0的差别非常微小。
ü 基本原理:先非对称加密传递对称加密所要用的钥匙,然后双方用该钥匙对称加密和解米往来的数据。
l 工作过程
ü 浏览器向服务器发出请求,询问对方支持的对称加密算法和非对称加密算法;服务器回应自己支持的算法。
ü 浏览器选择双方都支持的加密算法,并请求服务器出示自己的证书;服务器回应自己的证书。
ü 浏览器随机产生一个用于本次会话的对称加密的钥匙,并使用服务器证书中附带的公钥对该钥匙进行加密后传递给服务器;服务器为本次会话保持该对称加密的钥匙。第三方不知道服务器的私钥,即使截获了数据也无法解密。非对称加密让任何浏览器都可以与服务器进行加密会话。
ü 浏览器使用对称加密的钥匙对请求消息加密后传送给服务器,服务器使用该对称加密的钥匙进行解密;服务器使用对称加密的钥匙对响应消息加密后传送给浏览器,浏览器使用该对称加密的钥匙进行解密。第三方不知道对称加密的钥匙,即使截获了数据也无法解密。对称加密提高了加密速度。
l 要求
ü 服务器端需安装数字证书,用户可能需要确认证书。
ü 会话过程中的加密与解密过程由浏览器与服务器自动完成,对用户完全透明。
SSL 和 HTTPS 不仅可以加密通信,而且可以用于服务器和客户身份的验证。
在jdk帮助文档的jsse部分,可以看到SSL工作原理介绍的内容。
使用keytool创建证书时,刚开始设置的用户名一定要是服务器的域名,否则,浏览器进行访问时也将提示证书有问题:
1.一种情况是发证机关是否值得信赖的,好比你拿出来的驾照是不是交管局这样的国家法定机构颁发的,还是你们村的村支书自己盖的章。
2.还有一种就是证书机构确实是交管局颁发的,但并不是发给你的,而是发给你老婆的,你出示你老婆的驾照时,人家交警会问,这是我们发的证,但是是发给你的吗?
讲课时,刚开始创建的证书的用户故意不给localhost,实验失败后,再将老的证书的名称修改为其他名称,接着再创建一个给localhost的新证书,并且证书别名用老证书的名称,这样,就不用修改服务器端的配置,但要重新启动服务器才能生效。
SSL编程——默认参数方式
l 服务器端程序
• 调用getDefault()静态方法得到SSLServerSocketFactory实例对象
• 需要通过javax.net.ssl.keyStore和javax.net.ssl.keyStorePassword系统属性指定keystore的位置与密码,否则,KeyManager管理一个空的keystore,这可以通过在jsse文档中搜索getDefault关键字获知。
• 调用SSLServerSocketFactory对象的createServerSocket()方法得到ServerSocket。
• 循环调用ServerSocket.accept()等待外部连接,并启动新线程与每个连接的客户端进行对话。
• 打开浏览器进行访问测试,必须在keystore中存入与主机名相一致的keyEntry,且将该keyEntry的证书安装到浏览器中。
l 客户端编程:
• 调用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中导入服务器端的证书。
在这种采用默认参数的方式下,服务器端的keystore中只能存储一个keyEntry,否则,服务器程序就面临不知道选用哪个keyEntry的问题了。
客户端导入证书时执行的命令为:C:\Java\jdk1.6.0_21\bin>keytool -importcert -keystoreC:\Java\jdk1.6.0_21\jre\lib\security\cacerts -file zxx1.cer
baidu搜索SSLServerSocket,即有了参考代码。
服务器端代码:
publicvoid init1()throws Exception{
Stringuser_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");
ServerSocketFactoryfactory = SSLServerSocketFactory.getDefault();
ServerSocketss = factory.createServerSocket(443);
while(true){
Sockets = ss.accept();
newThread(new Worker(s)).start();
}
}
privateclass Worker implements Runnable{
Sockets = null;
publicWorker(Socket s){
this.s= s;
}
publicvoid run() {
try {
bytebuf[] = new byte[10240];
intlen = s.getInputStream().read(buf);
if(len>0){
System.out.println(newString(buf,0,len));
s.getOutputStream().write("200ok\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中。
privatestatic void work1() throws Exception{
SSLSocketFactoryfactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
Socketsocket = factory.createSocket("localhost",443);
InputStreamips = socket.getInputStream() ;
OutputStreamops = socket.getOutputStream();
ops.write("GET/ x".getBytes());
byte[]buf = new byte[1024];
intlen = 0;
while((len=ips.read(buf))!=-1){
System.out.println(newString(buf,0,len));
}
ips.close();
ops.close();
}
总结:创建服务器端Socket时,应该指定自己的私钥和证书在哪和是什么;创建客户端Socket时,它要验证服务器端出示的证书是否是可信赖的,所以,对于信赖的证书应该导入到jre/lib/security/cacerts文件中。服务器端的证书由keyManagerFactory类管理,客户端的证书由TrustManagerFactory管理。
在JSSE中,即使客户端程序没有直接信任服务器所使用的证书,但服务器的证书是客户所信任的其他证书签发的,客户端程序照样可以正常运行。
SSL编程——定制SSLContext
l 服务器端程序
• 调用getInstance()静态方法得到SSLContext实例对象
• 调用SSLContext对象的init()方法进行初始化
• 调用init()方法时必须传入KeyManager数组, KeyManager数组通过KeyManagerFactory对象从keystore中去读取出来。
• 调用SSLContext对象的getServerSocketFactory()方法得到SSLServerSocketFactory实例对象,接着按照默认参数的例子编写后续代码。
l 客户端编程:
• 调用getInstance()静态方法得到SSLContext实例对象
• 调用SSLContext对象的init()方法进行初始化
• 调用init()方法时必须传入TrustManager数组, TrustManager数组通过TrustManagerFactory对象从truststore中去读取出来。
• 调用SSLContext对象的getSocketFactory()方法得到SSLSocketFactory实例对象,接着按照默认参数的例子编写后续代码。
l 扩展:让服务器端程序从keystore里的多个KeyEntry中选择一个:
• 编写一个实现了X509KeyManager接口的自定义KeyManager类,这个自定义KeyManager类的chooseServerAlias方法返回要使用的keyEntry的别名, 而自定义KeyManager类中的其他方法则转发给原来通过KeyManagerFactory获得的KeyMananger对象。
• 实验方式:在keystore中创建两个keyEntry,先指定其中一个,然后再指定另外一个,这两个当中必然有一个会导致客户端出错。
这种定制方式可以让服务器端从多个keyEntry中选择一个keyEntry,
KeyManagerFactory.getInstance()方法接受的参数值可以在JSSE文档中搜索KeyManagerFactory看到,如何定制KeyManager的例子也可以在jsse文档中搜索,找到一个TrustManager定制的案例代码,可以推出如何定制KeyManager。
服务器端代码:
publicvoid init2() throws Exception{
Stringuser_home = System.getProperty("user.home");
System.out.println(user_home);
char[]passphrase = "123456".toCharArray();
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(newFileInputStream(user_home + "/.keystore"), passphrase);
KeyManagerFactorykmf =
KeyManagerFactory.getInstance("SunX509");
//如果keystore中只有一个keyEntry,则此处表示keyEntry的密码可以与keystore的密码不同。
kmf.init(ks,passphrase);
SSLContextsslContext = SSLContext.getInstance("TLS");
sslContext.init(
kmf.getKeyManagers(), null, null);
ServerSocketFactoryfactory = sslContext.getServerSocketFactory();
ServerSocketss = factory.createServerSocket(443);
while(true){
Sockets = ss.accept();
newThread(new Worker(s)).start();
}
}
客户端代码:运行SSL客户端程序时可以增加-Djavax.net.debug=all,在jsse文档搜索debug可以看到更详细的信息。
privatestatic void work2() throws Exception{
char[]passphrase = "changeit".toCharArray();
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(newFileInputStream("C:\\Java\\jdk1.6.0_21\\jre\\lib\\security\\cacerts"),passphrase);
TrustManagerFactorymanagerFactory = TrustManagerFactory.getInstance("PKIX");
managerFactory.init(ks);
SSLContextcontext = SSLContext.getInstance("TLS");
context.init(null,managerFactory.getTrustManagers(), null);
SSLSocketFactoryfactory = context.getSocketFactory();
Socketsocket = factory.createSocket("localhost",443);
InputStreamips = socket.getInputStream() ;
OutputStreamops = socket.getOutputStream();
ops.write("GET/ x".getBytes());
byte[]buf = new byte[1024];
intlen = 0;
while((len=ips.read(buf))!=-1){
System.out.println(newString(buf,0,len));
}
ips.close();
ops.close();
}
先在下面的第一个文件中找到了SSLImplementation的信息,然后根据这些SSLImplementation类找到相应的包,再找到下面的JSSEKeyManager.java文件
jakarta-tomcat-5.5.9-src.zip\jakarta-tomcat-5.5.9-src\jakarta-tomcat-connectors\http11\src\java\org\apache\coyote\http11\Http11Protocol.java
jakarta-tomcat-5.5.9-src.zip\jakarta-tomcat-5.5.9-src\jakarta-tomcat-connectors\util\java\org\apache\tomcat\util\net\jsse\JSSEKeyManager.java
下面的程序演示了如何应用JSSEKeyManager:
jakarta-tomcat-5.5.9-src.zip\jakarta-tomcat-5.5.9-src\jakarta-tomcat-connectors\util\java\org\apache\tomcat\util\net\jsse\JSSE14SocketFactory.java
扩展后的服务器端代码:
publicvoid init3() throws Exception{
Stringuser_home = System.getProperty("user.home");
System.out.println(user_home);
char[]passphrase = "123456".toCharArray();
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(newFileInputStream(user_home + "/.keystore"), passphrase);
KeyManagerFactorykmf =
KeyManagerFactory.getInstance("SunX509");
kmf.init(ks,passphrase);
KeyManager[]kms = kmf.getKeyManagers();
for(inti=0;i<kms.length;i++){
kms[i]= new ItcastKeyManager((X509KeyManager)kms[i],"mykey");
}
SSLContextsslContext = SSLContext.getInstance("TLS");
sslContext.init(
kms, null, null);
ServerSocketFactoryfactory = sslContext.getServerSocketFactory();
ServerSocketss = factory.createServerSocket(443);
while(true){
Sockets = ss.accept();
newThread(new Worker(s)).start();
}
}
privateclass ItcastKeyManager implements X509KeyManager {
private X509KeyManager delegate;
private String serverKeyAlias;
publicItcastKeyManager(X509KeyManager delegate, String serverKeyAlias) {
super();
this.delegate= delegate;
this.serverKeyAlias= serverKeyAlias;
}
publicString chooseClientAlias(String[] keyType, Principal[] issuers,
Socketsocket) {
returndelegate.chooseClientAlias(keyType, issuers, socket);
}
@Override
publicString chooseServerAlias(String keyType, Principal[] issuers,
Socketsocket) {
returnserverKeyAlias;
}
@Override
publicX509Certificate[] getCertificateChain(String alias) {
returndelegate.getCertificateChain(alias);
}
@Override
publicString[] getClientAliases(String keyType, Principal[] issuers) {
returndelegate.getClientAliases(keyType, issuers);
}
@Override
publicPrivateKey getPrivateKey(String alias) {
returndelegate.getPrivateKey(alias);
}
@Override
publicString[] getServerAliases(String keyType, Principal[] issuers) {
returndelegate.getServerAliases(keyType, issuers);
}
}
Https协议编程
l 与TLS的区别
• 传输的数据内容应遵守Http协议格式
• 服务器出示的证书所有者的名称必须是URL地址中指定的主机名
l 基本步骤:
• 创建表示https协议路径的URL实例对象,并获得URLConnection对象。
• 调用URLConnection对象的setDoOutput()方法支持发送实体内容。
• 获得输出和输入流与服务器进行交互。
l 扩展步骤:在程序中定制truststore的位置
• 全局设置:使用javax.net.ssl.trustStore系统属性指定truststore的位置。
• 针对单个链接的设置:使用SSLContext对象的init()方法加载TrustManagerFactory对象从truststore中读取出来的TrustManager对象,然后调用SSLContext对象的getSocketFactory()方法得到SSLSocketFactory实例对象,最后再调用HttpsURLConnection对象的setSSLSocketFactory方法。
l 学员邮件问题:
• 注册机构供了一个.p12文件keystore.p12(里面包含公钥和密钥),还有这个.p12文件的密码。自己通过通过keytool把p12文件中的证书导出来,再把它装入jks的truststore中,命令为:
keytool-importcert -keystore keystore.p12 -storetype pkcs12 -file xxx.crt
keytool-importcert -keystore xxx.jks -file xxx.crt
• 加载keystore.p12的程序代码:
KeyStorekeyStore = KeyStore.getInstance("pkcs12");
错误教学法:服务器端先不出示与主机名相匹配的证书,看到错误后后改为出示与主机名匹配的证书。
代码1:
privatestatic void https1() throws Exception{
URLurl = new URL("https://localhost/abcd");
URLConnectionconnection = url.openConnection();
connection.setDoOutput(true);
OutputStreamops = connection.getOutputStream();
ops.write("GET/ xsss".getBytes());
InputStreamips = connection.getInputStream() ;
byte[]buf = new byte[1024];
intlen = 0;
while((len=ips.read(buf))!=-1){
System.out.println(newString(buf,0,len));
}
ips.close();
ops.close();
}
代码2:
private static void https2() throwsException{
char[]passphrase = "changeit".toCharArray();
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(newFileInputStream("C:\\Java\\jdk1.6.0_21\\jre\\lib\\security\\cacerts"),passphrase);
TrustManagerFactorymanagerFactory = TrustManagerFactory.getInstance("PKIX");
managerFactory.init(ks);
SSLContextcontext = SSLContext.getInstance("TLS");
context.init(null,managerFactory.getTrustManagers(), null);
SSLSocketFactoryfactory = context.getSocketFactory();
URLurl = new URL("https://localhost/abcd");
URLConnectionconnection = url.openConnection();
((HttpsURLConnection)connection).setSSLSocketFactory(factory);
connection.setDoOutput(true);
OutputStreamops = connection.getOutputStream();
ops.write("GET/ x".getBytes());
InputStreamips = connection.getInputStream() ;
byte[]buf = new byte[1024];
intlen = 0;
while((len=ips.read(buf))!=-1){
System.out.println(newString(buf,0,len));
}
ips.close();
ops.close();
}
Tomcat体系结构
Tomcat是由很多模块组合出来的一个大大程序,每个模块各司其责,每个模块是如何组合成一个大程序的呢?Server.xml文件好比药的配方。
一个Service元素代表一种服务,譬如,卖火车票是一个服务,而卖飞机票又是另一个服务,connector相当于某种服务下的一种售票方式,可以在火车站对外售票,也可以在售票点对外售票,engine用于处理买票的内部工作,不管通过哪种方式接收进来的卖票请求,内部卖票处理工作始终一样,即都是用这个engine。这个机制的好处在于有非常好的扩展性,如果想增加网上买票,只要再加上一个网上卖票的Connector即可,engine还是原来的。
我餐馆的例子更好,服务员接电话,厨师炒菜,有没有厨师一个人接电话接受订单,还带炒菜的啊?这是单模块。
为Tomcat配置SSL功能的实验步骤
解决问题的思路:
• tomcat要支持SSL这种链接方式,只需要扩充安装一个支持SSL的Connector对象
• 客户端访问这个connector时,这个connector必须出示数字证书,这就需要先产生或获取证书,然后让连接器使用此证书。
实验步骤:
• 使用keytool创建或导入Web服务器所需要的证书。
• 修改server.xml文件,为Tomat增加一个支持SSL功能的连接器。取消其中对SSL连接器的注释,并根据安装的数字证书信息对一些参数进行调整即可。
• 编写一个用于检查访问协议是否是https的jsp程序,如果不是,则将请求重定向为https协议。
直接打开tomcat的https连接器,根据报告的错误信息就知道keystore文件存储在哪里了?当然,这要求计算机上以前没有使用keytool工具创建默认的证书存储文件,这个默认文件为<当前登录用户的主目录>/.keystore,例如,C:\Documents and Settings\IBM\.keystore。
使用keytool为tomcat产生数字证书时,用户的姓名部分必须填写成tomcat服务器的主机名。
在tomcat5.5中,没有keyalias选项来指定用哪个证书,在tomcat 6.x开始,就多了一个选项来指定所用证书的名称了
使用浏览器进行访问时https://localhost:8443,千万别忘了使用https和正确的端口号,一不小心就写成了http和用错了端口号。
根据浏览器的提示,安装完证书后,关闭浏览器后,重新打开浏览器进行访问,就没问题了。
从Tomcat文档中看到的:
Finally, using name-based virtual hosts ona secured connection can be problematic. This is a design limitation of the SSLprotocol itself. The SSL handshake, where the client browser accepts the servercertificate, must occur before the HTTP request is accessed. As a result, therequest information containing the virtual host name cannot be determined priorto authentication, and it is therefore not possible to assign multiple certificatesto a single IP address. If all virtual hosts on a single IP address need toauthenticate against the same certificate, the addition of multiple virtualhosts should not interfere with normal SSL operations on the server. Be aware,however, that most client browsers will compare the server's domain nameagainst the domain name listed in the certificate, if any (applicable primarilyto official, CA-signed certificates). If the domain names do not match, thesebrowsers will display a warning to the client user. In general, onlyaddress-based virtual hosts are commonly used with SSL in a productionenvironment.