Java安全(JCA/JSSE):SSL/TLS

基本概念

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();		
	}

参见:https://gitee.com/xxssyyyyssxx/unihttp/blob/master/unihttp-core/src/main/java/top/jfunc/http/ssl/SSLSocketFactoryBuilder.java

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值