直接使用TCP通信,方便灵活。 单将端口保留在外,又不安全,因此我们需要使用SSL, 对通信过程进行握手和加密, 确保安全。
准备证书
要使用ssl单向验证,就必须先要生成服务端和客户端的证书,并相将服务器证书添加到客户端信任证书中,具体流程如下
在命令行窗口执行如下所示命令(默认证书会在当前执行命令的目录下生成):
第一步 生成服务器端私钥和证书仓库命令
keytool -genkey -alias mySrvAlias1 -keysize 2048 -validity 365 -keyalg RSA -dname “CN=localhost” -keypass skeypass123 -storepass sstorepass456 -keystore yqServer.jks
-keysize 2048 密钥长度2048位(这个长度的密钥目前可认为无法被暴力破解)
-validity 365 证书有效期365天,测试中365就高了,实际生产中我们会冲认证机构获取证书,有效期比较长
-keyalg RSA 使用RSA非对称加密算法
-dname "CN=localhost" 设置Common Name为localhost
-keypass skeypass123 密钥的访问密码为skeypass123
-storepass sstorepass456 密钥库的访问密码为sstorepass456
-keystore sChat.jks 指定生成的密钥库文件为sChata.jks
第二步 生成服务器端自签名证书
keytool -export -alias mySrvAlias1 -keystore yqServer.jks -storepass sstorepass456 -file yqServer.cer
第三步 :生成客户端的密钥对和证书仓库,用于将服务器端的证书保存到客户端的授信证书仓库中
keytool -genkey -alias myClientAlias1 -keysize 2048 -validity 365 -keyalg RSA -dname “CN=localhost” -keypass ckeypass987 -storepass cstorepass654 -keystore yqClient.jks
第四步 :将服务器端证书导入到客户端的证书仓库中
keytool -import -trustcacerts -alias mySrvAlias1 -file yqServer.cer -storepass cstorepass654 -keystore yqClient.jks
如果只做单向认证,到此就可以结束了,如果是双响认证,则还需第五步和第六步
第五步 生成客户端自签名证书
keytool -export -alias myClientAlias1 -keystore yqClient.jks -storepass cstorepass654 -file yqClient.cer
第六步 将客户端的自签名证书导入到服务器端的信任证书仓库中:
keytool -import -trustcacerts -alias myClientSelfAlias -file yqClient.cer -storepass sstorepass456 -keystore yqServer.jks
到此,证书就生成完毕了,我们就可以得到两个jks文件,一个是服务端的yqServer.jks ,一个是客户端的yqClient.jks , 两个cer文件yqServer.cer和yqClient.cer
单向认证与双向认证区别:
与单向认证不同的是, 双向认证中,服务端也需要对客户端进行安全认证,这就意味着客户端的自签名证书也需要导入到服务器的数组证书仓库中。
我们一般使用https,都是单向认证,就是我们详细该网站就是可信任的网站,不是伪造假冒的。
我们使用网上银行或者一些需要高安全性的服务时需要双向认证,因为有U盾之类的东西,银行或者其他需要高安全性的服务已经将颁发给我们的证书添加到自己的信任列表中了。
Server端代码
我们使用Netty,服务端和客户端非常简单,主要是为了说明ssl,消息解码就直接使用LineBasedFrameDecoder。
服务器端首先需要加载自己证书
@Slf4j
public class MyServerSslContextFactory {
private static final String PROTOCOL = "TLS";
private static SSLContext sslContext;
public static SSLContext getServerContext(String pkPath, String storepass, String keypass){
if(sslContext !=null) return sslContext;
InputStream in =null;
try{
//密钥管理器
KeyManagerFactory kmf = null;
if(pkPath!=null){
//密钥库KeyStore
KeyStore ks = KeyStore.getInstance("JKS");
//加载服务端证书
in = new FileInputStream(pkPath);
//加载服务端的KeyStore, 该密钥库的密码"storepass,storepass指定密钥库的密码(获取keystore信息所需的密码)
ks.load(in, storepass.toCharArray());
kmf = KeyManagerFactory.getInstance("SunX509");
//初始化密钥管理器, keypass 指定别名条目的密码(私钥的密码)
kmf.init(ks, keypass.toCharArray());
}
//信任库 caPath is String,双向认证再开启这一段
/*TrustManagerFactory tf = null;
InputStream caIn = null;
if (caPath != null) {
KeyStore tks = KeyStore.getInstance("JKS");
caIn = new FileInputStream(caPath);
tks.load(caIn, storepass.toCharArray());
tf = TrustManagerFactory.getInstance("SunX509");
tf.init(tks);
}*/
//获取安全套接字协议(TLS协议)的对象
sslContext = SSLContext.getInstance(PROTOCOL);
//初始化此上下文
//参数一:认证的密钥 参数二:对等信任认证,如果双向认证就写成tf.getTrustManagers()
/ 参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null
sslContext.init(kmf.getKeyManagers(), null, null);
}catch(Exception e){
throw new Error("Failed to init the server-side SSLContext", e);
}finally{
if(in !=null){
try {
in.close();
} catch (IOException e) {
log.info("close InputStream.", e);
}
}
// close caIn 双向证书需要,关闭caIn
}
return sslContext;
}
}
服务器端handler配置
@Slf4j
public class Server {
private static final int PORT = 5566;
public void bind() throws Exception {
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws IOException {
// ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
log.info("current dir:{}", System.getProperty("user.dir"));
String jksPath = (System.getProperty("user.dir")+ "/nettyssl/src/main/resources/certs/yqServer.jks");
SSLContext sslContext =
MyServerSslContextFactory.getServerContext(jksPath, "sstorepass456", "skeypass123");
//设置为服务器模式
SSLEngine sslEngine = sslContext.createSSLEngine();
sslEngine.setUseClientMode(false);
//是否需要验证客户端 。 如果是双向认证,则需要将其设置为true,同时将client证书添加到server的信任列表中
sslEngine.setNeedClientAuth(false);
ch.pipeline().addLast("ssl", new SslHandler(sslEngine));
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast("processMsg", new SslDemoServerSideHandler());
}
});
// 绑定端口,同步等待成功
b.bind(PORT).sync();
System.out.println("Netty server start on : " + PORT);
}
public static void main(String[] args) throws Exception {
new Server().bind();
}
}
Client端代码
客户端需要加载自己的信任证书列表
@Slf4j
public class MyClientSslContextFactory {
private static final String PROTOCOL = "TLS";
private static SSLContext sslContext;
public static SSLContext getClientContext(String caPath, String storepass){
if(sslContext !=null) return sslContext;
InputStream trustInput = null;
try{
//信任库
TrustManagerFactory tf = null;
if (caPath != null) {
//密钥库KeyStore
KeyStore ks = KeyStore.getInstance("JKS");
//加载客户端证书
trustInput = new FileInputStream(caPath);
ks.load(trustInput, storepass.toCharArray());
tf = TrustManagerFactory.getInstance("SunX509");
// 初始化信任库
tf.init(ks);
}
//双向认证时需要加载自己的证书
/*KeyManagerFactory kmf = null;
if (pkPath != null) {
KeyStore ks = KeyStore.getInstance("JKS");
keyIn = new FileInputStream(pkPath);
ks.load(keyIn, storepass.toCharArray());
kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, keypass.toCharArray());
}*/
sslContext = SSLContext.getInstance(PROTOCOL);
//设置信任证书. 双向认证时,第一个参数kmf.getKeyManagers()
sslContext.init(null,tf == null ? null : tf.getTrustManagers(), null);
}catch(Exception e){
throw new Error("Failed to init the client-side SSLContext");
}finally{
if(trustInput !=null){
try {
trustInput.close();
} catch (IOException e) {
log.info("close InputStream.", e);
}
}
}
return sslContext;
}
}
客户端handler配置
@Slf4j
public class NettyClient {
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
EventLoopGroup group = new NioEventLoopGroup();
public void connect( String host, int port){
// 配置客户端NIO线程组
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch){
log.info("client current dir:{}", System.getProperty("user.dir"));
String clientPath = (System.getProperty("user.dir")+ "/nettyssl/src/main/resources/certs/yqClient.jks");
//客户方模式
SSLContext sslContext =
MyClientSslContextFactory.getClientContext(clientPath, "cstorepass654");
SSLEngine sslEngine = sslContext.createSSLEngine();
sslEngine.setUseClientMode(true);
ch.pipeline().addLast("ssl", new SslHandler(sslEngine));
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast("processMsg", new SslDemoClientSideHandler());
}
});
// 发起异步连接操作
ChannelFuture future = b.connect(host, port).sync();
future.channel().closeFuture().sync();
} catch (Exception ex) {
log.info("connection exception", ex);
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
String destIp = "192.168.119.1";
int port = 5566;
new NettyClient().connect(destIp, port);
}
源代码在这里,欢迎fork,加星。谢谢! 运行时请修改证书路径,或者带中证书路径, 因为该项目是一个IDEA工程下的一个module, 不是单个idea的单个项目。
注明:参考网上很多博文的内容,在此一并表示感谢!