使用TLS单向/双向认证,加密Netty程序
一. 项目准备
创建一个Netty服务端和客户端项目。
参考https://blog.csdn.net/u013071014/article/details/117325053?spm=1001.2014.3001.5501
二. KeyTool生成证书
-
生成Netty服务器公钥、私钥和证书仓库:
keytool -genkey -alias server -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=cn" -keypass 123456 -storepass 123456 -keystore serverStore.jks
-
导出Netty服务端签名证书
keytool -export -alias server -keystore serverStore.jks -storepass 123456 -file server.cer
-
生成Netty客户端公钥、私钥和仓库证书
keytool -genkey -alias client -keysize 2048 -validity 3650 -keyalg RSA -dname "CN=cn" -keypass 123456 -storepass 123456 -keystore clientStore.jks
-
将Netty服务端证书导入到客户端证书仓库
keytool -import -trustcacerts -alias server -file server.cer -storepass 123456 -keystore clientStore.jks
-
导出Netty客户端签名证书
keytool -export -alias client -keystore clientStore.jks -storepass 123456 -file client.cer
-
将客户端签名证书导入服务端证书仓库
keytool -import -trustcacerts -alias client -file client.cer -storepass 123456 -keystore serverStore.jks
-genkey 生成秘钥
-alias 别名
-keyalg 秘钥算法
-keysize 秘钥长度
-validity 有效期
-keystore 生成秘钥库的存储路径和名称
-keypass 秘钥口令
-storepass 秘钥库口令
-dname 拥有者信息,CN:姓名;OU:组织单位名称;O:组织名称;L:省/市/自治区名称;C:国家/地区代码
查看证书:
keytool -list -keystore serverStore.jks -storepass 123456
三. 单向认证
-
单向认证过程:
SSL单向认证只要求站点部署了ssl证书就行,任何用户都可以去访问(IP被限制除外等),只是服务端提供了身份认证。 1.客户端的浏览器向服务器传送客户端SSL协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。 2.服务器向客户端传送SSL协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。 3.客户利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的CA是否可靠,发行者证书的公钥能否正确解开服务器证书的"发行者的数字签名",服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。 4.用户端随机产生一个用于后面通讯的"对称密码",然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的"预主密码"传给服务器。 5.如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的"预主密码"一起传给服务器。 6.如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的CA是否可靠,发行CA 的公钥能否正确解开客户证书的发行CA的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的"预主密码",然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。 7.服务器和客户端用相同的主密码即"通话密码",一个对称密钥用于SSL协议的安全数据通讯的加解密通讯。同时在SSL通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。 8.客户端向服务器端发出信息,指明后面的数据通讯将使用的步骤7中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。 9.服务器向客户端发出信息,指明后面的数据通讯将使用的步骤7中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。 10.SSL的握手部分结束,SSL安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。
-
Netty服务端启动程序
package com.example.nettyserver.netty; import com.example.nettyserver.utils.SslContextFactoryOne; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.ssl.SslHandler; import io.netty.util.CharsetUtil; import javax.net.ssl.SSLEngine; import java.io.File; public class NettyServer { public static final String path = "serverStore.jks"; public void start() { // 主线程组,用于接收请求 NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); // 工作线程组,用于处理数据 NioEventLoopGroup workerGroup = new NioEventLoopGroup(8); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // 设置队列大小 .option(ChannelOption.SO_BACKLOG, 128) // 两小时内没有数据通信时,TCP会自动发送一个活动探测报文 .childOption(ChannelOption.SO_KEEPALIVE, true) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); // add TLS,单向认证 File directory = new File(File.separator+"root"+ File.separator + "tlsconf" + File.separator + path); String serverPath = directory.getCanonicalPath(); SSLEngine engine = SslContextFactoryOne.getServerContext(serverPath, serverPath).createSSLEngine(); // 设置服务端模式 engine.setUseClientMode(false); // 需要验证客户端身份,如果是双向验证,需要设为true engine.setNeedClientAuth(false); pipeline.addLast("ssl", new SslHandler(engine)); // 添加编解码 pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8)); pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8)); // 添加业务处理器 pipeline.addLast(new NettyServerHandler()); } }); // 绑定端口,接收连接 System.out.println(System.currentTimeMillis() + " Start data channel"); ChannelFuture future = serverBootstrap.bind("127.0.0.1", 8888).sync(); future.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }
-
单向认证工具类
package com.example.nettyserver.utils; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; public class SslContextFactoryOne { private static final String PROTOCOL = "TLS"; // 服务器安全套接字协议 private static SSLContext SERVER_CONTEXT; // 客户端安全套接字协议 private static SSLContext CLIENT_CONTEXT; // 使用KeyTool生成密钥库和密钥时配置的密码 private static String keyPassword = "123456"; public static SSLContext getServerContext(String pkPath,String caPath) { if (SERVER_CONTEXT != null) { return SERVER_CONTEXT; } InputStream in =null; try{ //密钥管理器 KeyManagerFactory kmf = null; if(pkPath!=null){ //密钥库KeyStore KeyStore ks = KeyStore.getInstance("JKS"); //加载服务端证书 in = new FileInputStream(pkPath); //加载服务端的KeyStore ;sNetty是生成仓库时设置的密码,用于检查密钥库完整性的密码 ks.load(in, keyPassword.toCharArray()); kmf = KeyManagerFactory.getInstance("SunX509"); //初始化密钥管理器 kmf.init(ks, keyPassword.toCharArray()); } //获取安全套接字协议(TLS协议)的对象 SERVER_CONTEXT= SSLContext.getInstance(PROTOCOL); //初始化此上下文 //参数一:认证的密钥 参数二:对等信任认证 参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null SERVER_CONTEXT.init(kmf.getKeyManagers(), null, null); }catch(Exception e){ throw new Error("Failed to initialize the server-side SSLContext", e); }finally{ if(in !=null){ try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } return SERVER_CONTEXT; } public static SSLContext getClientContext(String pkPath,String caPath){ if (CLIENT_CONTEXT!=null) { return CLIENT_CONTEXT; } InputStream tIn = null; try{ //信任库 TrustManagerFactory tf = null; if (caPath != null) { //密钥库KeyStore KeyStore tks = KeyStore.getInstance("JKS"); //加载客户端证书 tIn = new FileInputStream(caPath); tks.load(tIn, keyPassword.toCharArray()); tf = TrustManagerFactory.getInstance("SunX509"); // 初始化信任库 tf.init(tks); } CLIENT_CONTEXT = SSLContext.getInstance(PROTOCOL); //设置信任证书 CLIENT_CONTEXT.init(null,tf == null ? null : tf.getTrustManagers(), null); }catch(Exception e){ throw new Error("Failed to initialize the client-side SSLContext"); }finally{ if(tIn !=null){ try { tIn.close(); } catch (IOException e) { e.printStackTrace(); } } } return CLIENT_CONTEXT; } }
-
客户端启动程序
package com.example.nettyclient.netty; import com.example.nettyclient.utils.SslContextFactoryOne; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.ssl.SslHandler; import javax.net.ssl.SSLEngine; import java.io.File; public class NettyClient { public static final String path = "clientStore.jks"; public void start() { NioEventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); File directory = new File(File.separator+"root"+ File.separator + "tlsconf" + File.separator + path); String clientPath = directory.getCanonicalPath(); SSLEngine engine = SslContextFactoryOne.getClientContext(clientPath, clientPath).createSSLEngine(); engine.setUseClientMode(true);//客户方模式 pipeline.addLast("ssl", new SslHandler(engine)); pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder()); pipeline.addLast(new NettyClientHandler()); } }); ChannelFuture future = bootstrap.connect("127.0.0.1", 8888).sync(); System.out.println(System.currentTimeMillis() + " Client connect success..."); // 发送消息 future.channel().writeAndFlush("Client say Hello"); // 等待连接被关闭 future.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { group.shutdownGracefully(); } } }
四. 双向认证
-
双向认证过程
双向认证则是需要服务端与客户端提供身份认证,只能是服务端允许的客户能去访问,安全性相对于要高一些。 1.浏览器发送一个连接请求给安全服务器。 2.服务器将自己的证书,以及同证书相关的信息发送给客户浏览器。 3.客户浏览器检查服务器送过来的证书是否是由自己信赖的CA中心(如沃通CA)所签发的。如果是,就继续执行协议;如果不是,客户浏览器就给客户一个警告消息:警告客户这个证书不是可以信赖的,询问客户是否需要继续。 4.接着客户浏览器比较证书里的消息,例如域名和公钥,与服务器刚刚发送的相关消息是否一致,如果是一致的,客户浏览器认可这个服务器的合法身份。 5.服务器要求客户发送客户自己的证书。收到后,服务器验证客户的证书,如果没有通过验证,拒绝连接;如果通过验证,服务器获得用户的公钥。 6.客户浏览器告诉服务器自己所能够支持的通讯对称密码方案。 7.服务器从客户发送过来的密码方案中,选择一种加密程度最高的密码方案,用客户的公钥加过密后通知浏览器。 8.浏览器针对这个密码方案,选择一个通话密钥,接着用服务器的公钥加过密后发送给服务器。 9.服务器接收到浏览器送过来的消息,用自己的私钥解密,获得通话密钥。 10.服务器、浏览器接下来的通讯都是用对称密码方案,对称密钥是加过密的。
-
Netty服务端启动程序
package com.example.nettyserver.netty; import com.example.nettyserver.utils.SslContextFactoryOne; import com.example.nettyserver.utils.SslContextFactoryTwo; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.ssl.SslHandler; import io.netty.util.CharsetUtil; import javax.net.ssl.SSLEngine; import java.io.File; public class NettyServer { public static final String path = "serverStore.jks"; public void start() { // 主线程组,用于接收请求 NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); // 工作线程组,用于处理数据 NioEventLoopGroup workerGroup = new NioEventLoopGroup(8); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // 设置队列大小 .option(ChannelOption.SO_BACKLOG, 128) // 两小时内没有数据通信时,TCP会自动发送一个活动探测报文 .childOption(ChannelOption.SO_KEEPALIVE, true) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); // add TLS,单向认证 File directory = new File(File.separator+"root"+ File.separator + "tlsconf" + File.separator + path); String serverPath = directory.getCanonicalPath(); SSLEngine engine = SslContextFactoryTwo.getServerContext(serverPath, serverPath).createSSLEngine(); // 设置服务端模式 engine.setUseClientMode(false); // 需要验证客户端身份,如果是双向验证,需要设为true engine.setNeedClientAuth(true); pipeline.addLast("ssl", new SslHandler(engine)); // 添加编解码 pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8)); pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8)); // 添加业务处理器 pipeline.addLast(new NettyServerHandler()); } }); // 绑定端口,接收连接 System.out.println(System.currentTimeMillis() + " Start data channel"); ChannelFuture future = serverBootstrap.bind("127.0.0.1", 8888).sync(); future.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }
-
单向认证工具类
package com.example.nettyserver.utils; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; public class SslContextFactoryTwo { private static final String PROTOCOL = "TLS"; // 服务器安全套接字协议 private static SSLContext SERVER_CONTEXT; // 客户端安全套接字协议 private static SSLContext CLIENT_CONTEXT; // 使用KeyTool生成密钥库和密钥时配置的密码 private static String keyPassword = "123456"; public static SSLContext getServerContext(String pkPath,String caPath){ if(SERVER_CONTEXT!=null) { return SERVER_CONTEXT; } InputStream in =null; InputStream tIn = null; try{ //密钥管理器 KeyManagerFactory kmf = null; if(pkPath!=null){ KeyStore ks = KeyStore.getInstance("JKS"); in = new FileInputStream(pkPath); ks.load(in, keyPassword.toCharArray()); kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, keyPassword.toCharArray()); } //信任库 TrustManagerFactory tf = null; if (caPath != null) { KeyStore tks = KeyStore.getInstance("JKS"); tIn = new FileInputStream(caPath); tks.load(tIn, keyPassword.toCharArray()); tf = TrustManagerFactory.getInstance("SunX509"); tf.init(tks); } SERVER_CONTEXT= SSLContext.getInstance(PROTOCOL); //初始化此上下文 //参数一:认证的密钥 参数二:对等信任认证 参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null SERVER_CONTEXT.init(kmf.getKeyManagers(),tf.getTrustManagers(), null); }catch(Exception e){ throw new Error("Failed to initialize the server-side SSLContext", e); }finally{ if(in !=null){ try { in.close(); } catch (IOException e) { e.printStackTrace(); } in = null; } if (tIn != null){ try { tIn.close(); } catch (IOException e) { e.printStackTrace(); } tIn = null; } } return SERVER_CONTEXT; } public static SSLContext getClientContext(String pkPath,String caPath){ if(CLIENT_CONTEXT!=null) { return CLIENT_CONTEXT; } InputStream in = null; InputStream tIn = null; try{ KeyManagerFactory kmf = null; if (pkPath != null) { KeyStore ks = KeyStore.getInstance("JKS"); in = new FileInputStream(pkPath); ks.load(in, keyPassword.toCharArray()); kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, keyPassword.toCharArray()); } TrustManagerFactory tf = null; if (caPath != null) { KeyStore tks = KeyStore.getInstance("JKS"); tIn = new FileInputStream(caPath); tks.load(tIn, keyPassword.toCharArray()); tf = TrustManagerFactory.getInstance("SunX509"); tf.init(tks); } CLIENT_CONTEXT = SSLContext.getInstance(PROTOCOL); //初始化此上下文 //参数一:认证的密钥 参数二:对等信任认证 参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null CLIENT_CONTEXT.init(kmf.getKeyManagers(),tf.getTrustManagers(), null); }catch(Exception e){ throw new Error("Failed to initialize the client-side SSLContext"); }finally{ if(in !=null){ try { in.close(); } catch (IOException e) { e.printStackTrace(); } in = null; } if (tIn != null){ try { tIn.close(); } catch (IOException e) { e.printStackTrace(); } tIn = null; } } return CLIENT_CONTEXT; } }
-
客户端启动程序
package com.example.nettyclient.netty; import com.example.nettyclient.utils.SslContextFactoryOne; import com.example.nettyclient.utils.SslContextFactoryTwo; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.ssl.SslHandler; import javax.net.ssl.SSLEngine; import java.io.File; public class NettyClient { public static final String path = "clientStore.jks"; public void start() { NioEventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); File directory = new File(File.separator+"root"+ File.separator + "tlsconf" + File.separator + path); String clientPath = directory.getCanonicalPath(); SSLEngine engine = SslContextFactoryTwo.getClientContext(clientPath, clientPath).createSSLEngine(); engine.setUseClientMode(true);//客户方模式 pipeline.addLast("ssl", new SslHandler(engine)); pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder()); pipeline.addLast(new NettyClientHandler()); } }); ChannelFuture future = bootstrap.connect("127.0.0.1", 8888).sync(); System.out.println(System.currentTimeMillis() + " Client connect success..."); // 发送消息 future.channel().writeAndFlush("Client say Hello"); // 等待连接被关闭 future.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { group.shutdownGracefully(); } } }