直接使用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的單個項目。
註明:參考網上很多博文的內容,在此一併表示感謝!