欢迎到原发地址点赞Thrift SSL
说到网络传输加密,首选肯定是https,加密协议完善方便,案例多而完整。
但是有时无可奈何也需要对内网调用的RPC进行加密,其实也是加ssl,但是Thrift的案例实在太少,大多没什么参考价值。
所以在给TCP添加ssl这个方向上进行探索。既然都使用ssl了,首先得有证书,公钥私钥等必备的材料,下文就是记录这个探索的过程。
方案验证
thrift的加密demo
gitee.com/liezh/sslth…
证书签发
私自签发,作为Java开发者只要使用java自带得keytool就可以了,由于公司里tomcat也需要加证书,所以顺便签发一个keystore,那么都能公用了。
- 签发keystore
keytool -genkeypair -alias certificatekey -keyalg RSA -validity 365 -keystore /work/key/tomcat.keystore
- 使用openssl导出公钥/私钥
# 2导出公钥证书,cer是windows的标准
keytool -export -alias certificatekey -keystore /work/key/tomcat.keystore -rfc -file sslthrift.cer
# 3keystore转换为p12
keytool -importkeystore -srckeystore /work/key/tomcat.keystore -destkeystore /work/key/tomcat.p12 -deststoretype PKCS12
# 4使用openssl生成.crt
openssl pkcs12 -in /work/key/tomcat.p12 -nokeys -out /work/key/sslthrift.crt
# 5使用openssl生成.key
openssl pkcs12 -in /work/key/tomcat.p12 -nocerts -nodes -out /work/key/sslthrift.key
- 把公钥导入生成truststore或者bks
由于bks是需要三方的签证类库bcprov提供的,所以先下载到本地,放到${JAVA_HOME}/lib/目录下
gitee.com/liezh/sslth…
# 使用bcprov对公钥进行签发
keytool -importcert -v -trustcacerts -alias jsnet \
-file /work/key/sslthrift.cer -keystore /work/key/sslthrift.bks \
-storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
-providerpath /work/java/jdk1.8.0_261/lib/bcprov-jdk15on-1.52.0.jar -storepass 123456
# 把公钥添加添加到truststore中签发
keytool -importcert -keystore sslthrift.truststore \
-alias localhost --file /work/key/sslthrift.crt -storepass 123456
其实一开始我是不知道需要用上bks的,是因为后面Android不支持使用SunX509标准的JKS证书。会报如下错误: java.security.KeyStoreException: JKS not found java.security.KeyStoreException: SunX509 not found 因为jks是Oracle Java发行版的标准,使用的证书标准是sun自定义的SunX509,证书类型支持JKS,是Java的keytools证书工具支持的证书私钥格式。使用的是jre的类库支持,而Android就用不上jre,只会使用到了jdk的api辅助开发。
所以就需要使用BKS的证书类型,使用bcprov库进行签名,加密解密。所以Android开发不能看着是写java就以为java中理所当然的类库都用得上,有时候就是这种基础的签名算法不支持,才想不通。
想通为什么找不到JKS的支持的原因,解决的方式就简单了,既然Thrift默认使用的jre支持的SunX509标准的JKS不能用,那就换一种在Android上能用的X509开放标准的BKS。
编码验证
服务端
其实Thrif服务端的代码不需要修改,还是原来的启动方式。
public static void main(String[] args) {
MobileServiceImpl handler = new MobileServiceImpl();
MobileService.Processor processor = new MobileService.Processor(handler);
TServerSocket serverTransport = new TServerSocket(port);
// 设置协议工厂为 TCompactProtocol.Factory
TCompactProtocol.Factory proFactory = new TCompactProtocol.Factory();
// 多线程服务模型
TThreadPoolServer.Args tArgs = new TThreadPoolServer.Args(serverTransport);
tArgs.processor(processor);
tArgs.protocolFactory(proFactory);
tArgs.minWorkerThreads = minWorkerThreads;
tArgs.maxWorkerThreads = maxWorkerThreads;
tArgs.requestTimeout = requestTimeout;
tArgs.requestTimeoutUnit = timeUnit;
// 线程池服务模型,使用标准的阻塞式IO,预先创建一组线程处理请求。
TServer server = new TThreadPoolServer(tArgs);
System.out
.println("Starting the project MobileService with TThreadPoolServer port:"
+ port + " version:" + PropertiesUtils.getValue("VERSION") + "...");
server.serve();
}
Nginx 代理
但是不能在直接暴露服务端口给客户端提供服务了,需要由nginx进行SSL加密后对客户端提供服务,由nginx解密后代理给Thrift MobileService,如下图:
stream {
log_format main '$remote_addr [$time_local] '
'$protocol $ssl_protocol/$ssl_cipher $status $bytes_sent $bytes_received '
'$session_time "$upstream_addr" '
'"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';
access_log /work/liezh/nginx/logs/thrift_access.log main;
error_log /work/liezh/nginx/logs/thrift_error.log main;
server {
listen 9001 ssl;
proxy_pass 127.0.0.1:9000;
ssl_certificate /home/liezh/sslthrift.crt;
ssl_certificate_key /home/liezh/sslthrift.key;
}
}
客户端
前面两part其实都挺简单的,而且合乎情理,容易理解,按理来说客户端的也是这样的情况的。但是可惜的是我这里遇到的客户端不是JVM平台了。
是Android平台,这是个问题。在没有充分了解每一个平台时不要那么自信,就算在JVM平台的方案认证通过了,也不代表在其他平台可以顺顺利利。
首先编码是很简单的,在原先的客户端的基础上添加SSL的支持。
问题一:Android不能用路径的形式读取assets目录下的文件
Android没办法以路径的形式读取assets目录下的文件,只能通过AssetManager#open("sslthrift.bks")读取输入流。
但是Thrift V0.10.0版本TSSLTransportFactory.TSSLTransportParameters#setTrustStore(ksPath,ksPass)只支持路径读取,源码如下:
public class AppContext extends Application {
private static Context instance;
@Override
public void onCreate() {
super.onCreate();
instance = getApplicationContext();
// 在应用启动时判断证书是否安装
if (!bksIsInstall()) {
try {
// 把assets目录下的证书(jks/bks)安装到sd卡目录上
AssetUtil.putAssetsToSDCard(instance, PropertiesUtils.getStringValue("keyStore"));
} catch (RuntimeException ignored) {
ToastUtils.showToast(ignored.getMessage(), getContext());
}
}
}
public boolean bksIsInstall() {
String bksPath = AppContext.getContext().getExternalFilesDir(null).getAbsolutePath()
+ File.separator
+ PropertiesUtils.getStringValue("keyStore");
File file = new File(bksPath);
return file.exists();
}
public static Context getContext()
{
return instance;
}
}
public User getUser() {
try {
/********** 改造点 ************/
TSSLTransportFactory.TSSLTransportParameters params
= new TSSLTransportFactory.TSSLTransportParameters();
String keyStore = PropertiesUtils.getStringValue("keyStore"); // keyStore=sslthrift.jks
// 这里是事先app在启动时检测,应用目录下是否安装过证书
// 只有在目录下安装过证书才能拿到证书的路径
// 路径如下:/storage/emulated/0/Android/data/com.liezh.sslthriftdemo/files/sslthrift.jks
String bksPath = AppContext.getContext().getExternalFilesDir(null).getAbsolutePath() + File.separator + keyStore;
String keyStorePass = "123456";
params.setTrustStore(bksPath, keyStorePass, "SunX509", "JKS");
transport = TSSLTransportFactory.getClientSocket(thrift_qxspd_ip, thrif_qxspd_port, thrift_timeout, params);
/********** 改造点 ************/
protocol = new TCompactProtocol(transport);
client = new MobileService.Client(protocol);
return client.getUser("liezh", "123456");
} catch (TException e) {
e.printStackTrace();
} finally {
if (transport != null && transport.isOpen()) {
transport.close();
}
}
}
问题二:JKS和SunX509 Android平台不支持
public class TSSLTransportFactory {
private static SSLContext createSSLContext(TSSLTransportFactory.TSSLTransportParameters params) throws TTransportException {
InputStream in = null;
InputStream is = null;
SSLContext ctx;
try {
ctx = SSLContext.getInstance(params.protocol);
TrustManagerFactory tmf = null;
KeyManagerFactory kmf = null;
KeyStore ks;
if (params.isTrustStoreSet) {
// @3. 默认是PKIX的签名标准,会和我们现在用keytool的签发的SunX509标准不一致
// 如果使用SunX509,会报 SunX509 not found
tmf = TrustManagerFactory.getInstance(params.trustManagerType);
// @4. TrustManagerFactory底层就是使用了java.security.KeyStore
// 会报java.security.KeyStoreException: JKS not found
ks = KeyStore.getInstance(params.trustStoreType);
in = getStoreAsStream(params.trustStore);
ks.load(in, params.trustPass != null ? params.trustPass.toCharArray() : null);
tmf.init(ks);
}
...
} catch (Exception var17) {
throw new TTransportException("Error creating the transport", var17);
} finally {
...
}
return ctx;
}
public static class TSSLTransportParameters {
public TSSLTransportParameters(String protocol, String[] cipherSuites, boolean clientAuth) {
this.protocol = "TLS";
// @1.
// 由于是在Android平台默认的算法是PKIX
this.keyManagerType = KeyManagerFactory.getDefaultAlgorithm();
this.keyStoreType = "JKS";
// 由于是在Android平台默认是不支持JKS的
this.trustManagerType = TrustManagerFactory.getDefaultAlgorithm();
this.trustStoreType = "JKS";
this.clientAuth = false;
this.isKeyStoreSet = false;
this.isTrustStoreSet = false;
if (protocol != null) {
this.protocol = protocol;
}
this.cipherSuites = cipherSuites != null ? (String[])Arrays.copyOf(cipherSuites, cipherSuites.length) : null;
this.clientAuth = clientAuth;
}
/**
* @2.
* 如果不指定trustManagerType、trustStoreType会默认使用PKIX和JKS,
* 是不行的Android不支持JKS
*/
public void setTrustStore(String trustStore, String trustPass, String trustManagerType, String trustStoreType) {
this.trustStore = trustStore;
this.trustPass = trustPass;
if (trustManagerType != null) {
this.trustManagerType = trustManagerType;
}
if (trustStoreType != null) {
this.trustStoreType = trustStoreType;
}
this.isTrustStoreSet = true;
}
}
}
看完上面配置Thrift TSSLTransportFactory配置SSL的源码,经过一番思考和百度,意识到,在Android平台不能使用keytool默认签发使用的SunX509标准的JKS。明白这点就简单了,使用别的标准签发的证书。
解决方案:使用bcprov替换jre的加密组件,作为加解密的工具,这也是Android默认支持的加解密组件。使用X509的标准签发BKS证书。如下:
# 使用bcprov对公钥进行签发
keytool -importcert -v -trustcacerts -alias jsnet \
-file /work/key/sslthrift.cer -keystore /work/key/sslthrift.bks \
-storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
-providerpath /work/java/jdk1.8.0_261/lib/bcprov-jdk15on-1.52.0.jar -storepass 123456
改造客户端的SSL配置:如下:
public User getUser() {
try {
/********** 改造点 ************/
TSSLTransportFactory.TSSLTransportParameters params
= new TSSLTransportFactory.TSSLTransportParameters();
String keyStore = PropertiesUtils.getStringValue("keyStore"); // keyStore=sslthrift.bks
// 这里是事先app在启动时检测,应用目录下是否安装过证书
// 只有在目录下安装过证书才能拿到证书的路径
// 路径如下:/storage/emulated/0/Android/data/com.liezh.sslthriftdemo/files/sslthrift.bks
String bksPath = AppContext.getContext().getExternalFilesDir(null).getAbsolutePath() + File.separator + keyStore;
String keyStorePass = "123456";
params.setTrustStore(bksPath, keyStorePass, "X509", "BKS");
transport = TSSLTransportFactory.getClientSocket(thrift_qxspd_ip, thrif_qxspd_port, thrift_timeout, params);
/********** 改造点 ************/
protocol = new TCompactProtocol(transport);
client = new MobileService.Client(protocol);
return client.getUser("liezh", "123456");
} catch (TException e) {
e.printStackTrace();
} finally {
if (transport != null && transport.isOpen()) {
transport.close();
}
}
}
验证
运行后端服务,重启nginx服务,启动app。
nginx的日志显示加密没有问题,确实有使用TLS1.2加密协议,对数据进行加密,并解密后转发到9000端口上的Thrift服务端。如下图日志显示:
查看app是否启动时是否安装证书成功,如下图:
总结
虽然整个过程里开发难度不大,但是在不知道Android平台不支持SunX509标准和JKS的情况下,确实存在很长时间的困惑期,明明java是应该支持JKS的,这是java默认的实现就支持的。
到了Android似乎就失效了,运行时直接报JKS not found,找不到。最后还是经过阅读Thrift源码,明白了Thrift针对Java平台默认是支持JKS的,但是Android平台显然不属于Java平台那方,Google使用java作为开发语言,但是提供的运行时基类和JVM平台是不一样的,所以jre上的一部分组件,在Android是不能用的,例如在本文里的加密组件是不能使用jre的版本的,也就为什么Android平台不支持SunX509标准和JKS证书的根本原因。
所以不能用的JVM类库就找替代方案,加解密就可以用bcprov-jdk15on-1.52.0.jar。
stackoverflow.com/questions/3…