Java WebSocket Security 加密通信实践
最近在项目中用到了WebSocket通信的内容
第一版的实现方案:spring-websocket + nv-websocket-client
第一版的模式已经正常得到应用,但有个问题,就是传输过程没有加密,完全明文的方式,这在当前的时代不合时宜,所以考虑对通信过程进行加密传输。
第二版的实现方案:Java-WebSocket
这是一个Java的WebSocket开发组件,支持整合到后端、客户端,支持SSL/TLS加密传输协议。由于我目前的项目主要应用场景在局域网内,所以对于常规的CA签名证书部署并不合适,我就用自签名的形式进行加密传输检验。
话不多说,直接开干。
一、准备工作
准备自签名证书
keytool -genkey -validity 3650 -keyalg RSA -sigalg SHA256withRSA -keystore "keystore.jks" -storepass "storepassword" -keypass "keypassword" -alias "default" -dname "CN=127.0.0.1, OU=MyOrgUnit, O=MyOrg, L=MyCity, S=MyRegion, C=MyCountry"
这里要特别注意 -keyalg RSA -sigalg SHA256withRSA 这两个属性,后面要将jks转换成bks才能在Android平台使用,没有这两个属性会导致handshake的时候报“Possible no handshake recieved!”,说明签名校验不通过。
github上的Issue说明
将jks签名文件转换成bks,因为Android只能读取bks格式的签名秘钥,转换方法有好几种,这里介绍一个命令行方式的
转换方法
二、程序实现
java后台
WebSocketImpl.DEBUG = true;
ChatServer chatServer = new ChatServer(9089); // 这里可以自定义端口,默认80
logger.info("charServer port:" + chatServer.getPort());
// load up the key store
String STORETYPE = "JKS";
String KEYSTORE = "/Users/randysu/keystore.jks";
String STOREPASSWORD = "storepassword"; // 之前在制作keystore.jks时指定的store密码
String KEYPASSWORD = "keypassword"; // 之前在制作keystore.jks时指定的key密码
KeyStore ks = KeyStore.getInstance( STORETYPE );
File kf = new File( KEYSTORE );
ks.load( new FileInputStream( kf ), STOREPASSWORD.toCharArray() );
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init( ks, KEYPASSWORD.toCharArray() );
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init( ks );
SSLContext sslContext = null;
sslContext = SSLContext.getInstance( "TLS" );
sslContext.init( kmf.getKeyManagers(), tmf.getTrustManagers(), null );
// DefaultSSLWebSocketServerFactory 这里用的是默认的加密仓库,默认允许加载所有的签名方式,也可以自定义加密仓库,移除不能校验通过的签名方式
// SSLEngine engine = sslContext.createSSLEngine();
// List<String> ciphers = new ArrayList<String>( Arrays.asList(engine.getEnabledCipherSuites()));
// ciphers.remove("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
// List<String> protocols = new ArrayList<String>( Arrays.asList(engine.getEnabledProtocols()));
// protocols.remove("SSLv3");
// CustomSSLWebSocketServerFactory factory = new CustomSSLWebSocketServerFactory(sslContext, protocols.toArray(new String[]{}), ciphers.toArray(new String[]{}));
// chatserver.setWebSocketFactory(factory);
chatServer.setWebSocketFactory( new DefaultSSLWebSocketServerFactory( sslContext ) );
chatServer.start();
Android端
// load up the key store
String STORETYPE = "bks";
String KEYSTORE = Environment.getExternalStorageDirectory().getPath() + File.separator + "keystore.bks";
String STOREPASSWORD = "storepassword";
String KEYPASSWORD = "keypassword";
KeyStore ks = KeyStore.getInstance(STORETYPE);
File kf = new File(KEYSTORE);
ks.load(new FileInputStream(kf), STOREPASSWORD.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
kmf.init(ks, KEYPASSWORD.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(ks);
SSLContext sslContext = null;
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
SSLSocketFactory factory = sslContext.getSocketFactory();
URI socketUri = new URI("wss://192.168.1.230:9089/server");
SSLWebSocketClient socketClient = new SSLWebSocketClient(socketUri, mWsListener);
socketClient.setSocket(factory.createSocket());
socketClient.connect();
// socketClient.connectBlocking(); // 阻塞当前线程直至后台返回连接成功或失败
SSLWebSocketClient 继承 WebSocketClient(Java-WebSocketde的对象),mWsListener是一个连接状态的回调接口,在里面处理连接状态。
mWsListener = new WsListener() {
@Override
public void onConnected(ServerHandshake handshakedata) {
//连接成功
Logger.i("websoeket opened connection");
}
@Override
public void onTextMessage(String message) {
//服务端消息来了
Logger.i("websoeket received:" + message);
}
@Override
public void onDisconnected(int code, String reason, boolean remote) {
//连接断开,remote判定是客户端断开还是服务端断开
Logger.i("websoeket Connection closed by " + (remote ? "remote server" : "us"));
}
@Override
public void onConnectError(Exception ex) {
// 连接错误
Logger.i("websoeket error:" + ex);
}
};
}
java后台向Android发送消息
WebSocket对象的send(String str)方法
Android端向java后台发送消息
SSLWebSocketClient的send(String str)方法
这里要注意一点,Android定义的证书格式是“ X509”,Java后台定义的证书格式是“ SunX509”,如果Android定义“SunX509”会报错,同理也不能在Java后台定义“X509”。
三、实测结果

推荐一本书《HTTPS权威指南》
Done!