背景
这几天,我们家娃吃奶粉的量嗷嗷的涨起来了。我这颗小鹿乱撞的心,忍不住去面了两家互联网金融公司。
因为没有准备,结果你懂的~
这两家共同都有一个共同点,特别关系安全问题,尤其是看到你用过 okhttp3,那不得给你撸脱毛了就不算完事儿。
协议
HTTP vs HTTPS
我们都知道,https 相比于之前的 http 多了一层, 如下:
HTTP是一个基于TCP/IP通信协议来传递数据的协议,TCP/IP通信协议只有四层,从上往下依次为:应用层、传输层、网络层、数据链路层这四层,大学课本上的计算机网络知识是不是来了。但是,HTTP 协议在在网上进行传输用的是明文,就像某鸟给你发的快递,你的手机号、姓名都是写的清清楚楚,用户看着都瑟瑟发抖。
后来京东和顺丰意识到了这一点,就对手机号中间四位做了加密处理,姓名中间那几个个字也看不到了,甚至快递小哥给你打电话都是虚拟号码,你自己的电话只有自己心里清楚。
HTTPS 也是这个路子,为了解决 HTTP 明文传输存在的安全问题,在应用层和传输层之间加了一层安全层:SSL/TLS。
SSL: Secure Socket Layer, 安全套接层
TLS: Transport Layer Security,传输层安全协议
关于 HTTP 和 HTTPS 的对比文章,知乎上有一大坨,这里贴几篇,喜欢的可以多撸几遍:
HTTPS优缺点
- 使用 HTTP 明文传输存在被窃听风险,HTTPS 通过内容加密解决,注意这一步用的是对称加密哦
- 使用 HTTP 明文传输存在通信内容被篡改风险,HTTPS 通过校验机制解决
- 使用 HTTP 明文传输存在冒充服务风险,HTTPS 通过身-份-证书认证机制解决
通过以上三点,我们知道HTTPS 相对于 HTTP 有很强的安全感,但是对于一些app 这种安全级别还不够,如果出现以下意外咋整?
我干脆去问了讯飞星火,他是这么回答我的:

我又去问了 ChatGPT,他的回答是这样的

这俩货说的差不多,HTTPS 只能保证传输过程的安全性,不能保证全链路的安全性,更不能满足特定安全标准的规范诉求,所以我们需要根据安全的级别要求,采取不同加密策略。
既然如此,我们使用的三方网络库这里特指 okhttp 是怎么全链路传输安全的?我们接着往下唠:
OKhttp 库
Okhttp 库是基于HTTP和HTTPS协议封装的网络请求库,用于 Android 和 Java 应用中的网络通信。库在这儿:github.com/square/okht… ,
我们可以先了解一下okhttp 库是如何保证传输安全的~
问:OKhttp是如何支持https协议,以保证传输安全的,尤其是如何进行防止侦听、篡改、拦截信息?
AI 回答:
OkHttp是一个流行的开源HTTP客户端库,由Square公司开发。它提供了对HTTP和HTTPS的支持,并通过内置的TLS(Transport Layer Security)来确保HTTPS连接的安全性。以下是OkHttp如何支持HTTPS协议以及保障传输安全的一些关键方面:
- TLS/SSL支持:
- OkHttp使用Java的
javax.net.ssl包来支持TLS/SSL协议。TLS是HTTP的安全版本,用于加密通信内容,防止中间人攻击、窃听和篡改。 - OkHttp通过
SSLSocketFactory和HostnameVerifier等类来处理TLS/SSL握手和验证服务器证书的过程。
- OkHttp使用Java的
- 证书验证:
- OkHttp会验证服务器提供的SSL证书,确保通信双方的身份合法。如果服务器的证书无效或无法验证,OkHttp会拒绝连接,从而防止中间人攻击。
- 证书锁定(Certificate Pinning):
- OkHttp支持证书锁定机制,即在应用中预先指定服务端的证书信息。这样可以防止使用伪造证书的攻击,即使系统受到信任的根证书被破坏,应用仍然可以保持安全。
- 连接池和持久连接:
- OkHttp通过连接池(Connection Pool)来重用已建立的连接,从而减少握手次数,提高性能。持久连接也有助于减少建立连接时的潜在风险。
- 安全配置:
- OkHttp允许开发者配置TLS版本、密码套件(Cipher Suites)等参数,以满足特定的安全要求。
- 拦截器(Interceptors):
- OkHttp提供了拦截器机制,可以在HTTP请求和响应的过程中插入自定义的逻辑。这使得开发者可以在请求或响应阶段添加额外的安全检查或处理逻辑,从而增强安全性。
总的来说,OkHttp通过支持TLS/SSL、证书验证、证书锁定、连接池等安全机制,以及提供灵活的配置和拦截器功能,确保在HTTPS连接中防止侦听、篡改和拦截信息的风险。开发者在使用OkHttp时,应该合理配置相关参数,以满足应用的安全需求。
好家伙,okhttp 是支持合理配置安全参数的,而且其拦截器机制可以很方便的进行安全检查,那还等什么,走一波~
策略一:对称加密(加密内容)
这里主打一个,客户端不想让别人看到自己的传输内容,想当然对自己的请求内容进行加密处理。基于这种思路我们封装一个 EncryptedOkHttpClient,代码如下:
public static OkHttpClient createEncryptedOkHttpClient() {
// 创建一个OkHttpClient.Builder
OkHttpClient.Builder builder = new OkHttpClient.Builder();
// 添加自定义拦截器,用于加密请求内容
builder.addInterceptor(new EncryptionInterceptor());
// 创建OkHttpClient实例
return builder.build();
}
注释里已经写了,通过EncryptionInterceptor拦截器对请求进行加密处理,这里选择加密请求体 RequestBody
在encryptRequestBody方法中,RequestBody 依赖 okio 的 Buffer 类转换为ByteArray用于加密,加密算法选择对称加密算法 AES 加密字节数据,实现如下:
private RequestBody encryptRequestBody(RequestBody originalRequestBody) throws IOException {
// 从原始RequestBody中读取字节数据
// Read the byte data from the original RequestBody using Okio
Buffer buffer = new Buffer();
originalRequestBody.writeTo(buffer);
byte[] bytes = buffer.readByteArray();
// 使用对称加密算法(AES)加密字节数据
byte[] encryptedBytes = encryptWithAES(bytes, SECRET_KEY);
// 创建新的RequestBody
return RequestBody.create(originalRequestBody.contentType(), encryptedBytes);
}
可以看到,AES 使用了encryptWithAES方法加密字节数据,同时传了SECRET_KEY这个密钥,那我们看看 AES 是怎么加密的:
private byte[] encryptWithAES(byte[] input, String key) {
try {
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(input);
} catch (Exception e) {
e.printStackTrace();
return new byte[0];
}
}
四行代码搞定,首先通过SecretKeySpec类将SECRET_KEY字符串加密成 SecretKey 对象,然后Cipher以加密模式 对密钥进行初始化然后加密 input 也就是转换为字节数组的请求体。 加密完成了,服务器当然要进行解密,解密方法如下:
public static String decrypt(String encryptedText) {
try {
byte[] encryptedData = Base64.decode(encryptedText,Base64.DEFAULT);
SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(encryptedData);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
可以看到,解密过程使用了相同AES算法和密钥SECRET_KEY,这就是对称加密使用一把钥匙上锁和开锁。但是这种加密算法有很大的问题:
首先,这把钥匙如果想通过网络传输让服务端知道,传输过程中被劫持了密钥就会暴露。
另外,SECRET_KEY是硬编码在代码中的,这也不安全,这可咋整啊?
千里之堤,溃于hacker
为了防止这种中间人攻击的问题,非对称加密开始表演了~
策略二:非对称加密
非对称加密是一把锁两把钥匙:公钥和私钥。前者是给大家伙用的,谁都能够来配一把公钥进行数据加密,但是要对加密数据进行解密,只能使用私钥。
假设我们用公钥加密一份数据,就不怕拦截了。因为只有拿着私钥的服务端才能解密数据,我们拿着服务器提供的公钥把策略一中的对称密钥给加密了,那不就解决了网络传输密钥的问题了。对的,HTTPS 也是这么做的,按照这个思路我们再添加一个 MixtureEncryptionInterceptor 拦截器。
// 添加自定义拦截器,用服务器非对称加密的公钥加密对称加密的密钥,然后用对称加密密钥加密请求内容
builder.addInterceptor(new MixtureEncryptionInterceptor());
MixtureEncryptionInterceptor 拦截器同样实现 Interceptor 接口如下:

其 intercept 方法跟 EncryptionInterceptor 一模一样,具体的变化在 encryptRequestBody() 方法中。具体实现如下:
private RequestBody encryptRequestBody(RequestBody originalRequestBody) throws IOException {
// 生成对称加密的密钥
byte[] secretKeyBytes = generateSecretKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "AES");
// 使用服务器的公钥加密对称加密的密钥
byte[] encryptedSecretKey = encryptWithPublicKey(secretKeyBytes, SERVER_PUBLIC_KEY);
// 从原始 RequestBody 中读取字节数据
Buffer buffer = new Buffer();
originalRequestBody.writeTo(buffer);
byte[] bytes = buffer.readByteArray();
// 使用对称加密算法(AES)加密请求体
byte[] encryptedRequestBodyBytes = encryptWithAES(bytes, secretKeySpec);
// 创建新的 RequestBody,将加密后的密钥和请求体一并传输
return RequestBody.create(null, concatenateArrays(encryptedSecretKey, encryptedRequestBodyBytes));
}
如代码中注释,整个混合加密共 4 个步骤,依次是:
- 生成对称加密的密钥,用来加密传输内容。代码如下:
/**
* try block 里使用的是加密算法和随机数生成器,生成的较为复杂的密钥
* catch block 里使用的是示范性的非安全密钥
* @return
*/
private byte[] generateSecretKey() {
// 生成对称加密的密钥
try {
// 创建KeyGenerator对象,指定使用AES算法
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
// 初始化KeyGenerator对象,设置密钥长度为128位
keyGenerator.init(128, new SecureRandom());
// 生成密钥
SecretKey secretKey = keyGenerator.generateKey();
// 获取密钥的字节数组表示形式
byte[] keyBytes = secretKey.getEncoded();
// 打印密钥的字节数组表示形式
for (byte b : keyBytes) {
Log.d(TAG,b + " ");
}
return keyBytes;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
// 这里简单地示范了生成密钥的过程,实际上可以使用更复杂的方法来生成密钥
return "YourSecretKey".getBytes(StandardCharsets.UTF_8);
}
}
如注释所言,上面try block 里使用的是加密算法和随机数生成器,生成的较为复杂的密钥,catch block 里使用的是示范性的非安全密钥。这里主要是想说明生成对称密钥的方式有很多,但是硬编码生成密钥那是不推荐的,因为太不安全了,很容易被恶意用户获取到。
- 使用服务器的公钥加密对称加密的密钥,防止被破解





































































































































































































































































































新方案加上课程Id排序方式以后,搜索结果和原方案一致。


