Gson增量解析

增量解析是我自己杜撰的一个名词,主要是有这样的场景,移动端请求服务端json数据的时候,考虑到流量资源的珍贵,如果本地已经请求过完整下信息,一般是将一个model更新的字段下发,而不是每次都下发完整的model。举个例子,我们向服务器请求user的信息,完整的数据格式可能是:

{“age”:20,”id”:1,”name”:”张三”,”sex”:”男”}

User中的id、name、sex可能是一直不变的,而age是有可能发生变化的,当客户端刷新user的信息时,服务端只下发age的信息就行了,以达到节省流量的目的,下发的数据格式可能是:

{“age”:25,”id”:1}

只有age发生变化,所以只下发了age的信息,id作为识别字段。我们用Gson解析的时候一般是直接调用fromJson函数,这个函数接受两个参数,一个是json字符串,一个是解析的model类。对于这种只有部分数据的json我们如何解析呢,难道重新定义一个SubUser类,处理这种解析然后赋值给User吗,这样做的代价是非常不灵活,更新的字段必须客户端写死,如果下次name或者sex也发生了变化呢,是不是SubUser类就需要修改然后发版?

能不能有个类似于fromJson的函数,他的第二个参数不再是一个类,而是一个实例,新的json数据只会更新这个实例的某些字段。Gson在解析的时候会调用model的默认构造函数new出一个实例,能不能不让它new而是直接在原有的实例基础上进行解析呢?查看Gson的官方文档,看到可以给Gson注册一个InstanceCreator,它是一个接口,顾名思义它是提供用户一个初始化model的函数:

1
2
3
4
public interface InstanceCreator<T> {

public T createInstance(Type type);
}

这样我们就可以把原来的user作为初始化的实例,实现增量更新的解析。这是对于object的解析,可以这么做。如果是list呢,如何进行增量解析呢,总不能把原来的list作为返回值吧,目前我还没有很好的思路,只是将列表拆分成object分别进行解析。

下面简单的写一个demo:

User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User{

public long id;

public String name;

public String sex;

public int age;

public long getId() {
return id;
}
}

GsonCreatorHelper类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class GsonCreatorHelper {

public static User fromJson(String json, final User old) {
try {
JSONObject object = new JSONObject(json);
long id = object.getLong("id");
if (id == old.getId()) {
Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new InstanceCreator<User>() {
@Override
public User createInstance(Type type) {
return old;
}
}).create();
return gson.fromJson(json, User.class);
}
} catch (JSONException e) {
e.printStackTrace();
}
return new Gson().fromJson(json, User.class);
}

public static List<User> fromJson(String json, final List<User> old) {
try {
List<User> result = new ArrayList<>();
JSONArray array = new JSONArray(json);
for (int i = 0; i < array.length(); i++) {
JSONObject object = array.getJSONObject(i);
final long id = object.getLong("id");
Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new InstanceCreator<User>() {
@Override
public User createInstance(Type type) {
return getUserById(old, id);
}
}).create();
result.add(gson.fromJson(object.toString(), User.class));
}
return result;
} catch (JSONException e) {
e.printStackTrace();
}
return new Gson().fromJson(json, new TypeToken<List<User>>() {
}.getType());
}

private static User getUserById(List<User> users, long id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return new User();
}
}

MainActivity测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

String json1 = "{\"age\":20,\"id\":1,\"name\":\"张三\",\"sex\":\"男\"}";
String json2 = "{\"age\":23,\"id\":1}";
String json3 = "[{\"age\":20,\"id\":1,\"name\":\"张三\",\"sex\":\"男\"},{\"age\":23,\"id\":2,\"name\":\"李四\",\"sex\":\"男\"}]";
String json4 = "[{\"age\":23,\"id\":1},{\"age\":33,\"id\":2}]";

User user1 = new Gson().fromJson(json1, User.class);
Log.e("user1", new Gson().toJson(user1));

User user2 = GsonCreatorHelper.fromJson(json2, user1);
Log.e("user2", new Gson().toJson(user2));

List<User> user3 = new Gson().fromJson(json3, new TypeToken<ArrayList<User>>() {}.getType());
Log.e("user3", new Gson().toJson(user3));

List<User> user4 = GsonCreatorHelper.fromJson(json4, user3);
Log.e("user4", new Gson().toJson(user4));
}
1
2
3
4
10-08 18:18:41.419 28438-28438/com.panda.gsoninstancecreatortest E/user1: {"age":20,"id":1,"name":"张三","sex":"男"}
10-08 18:18:41.425 28438-28438/com.panda.gsoninstancecreatortest E/user2: {"age":23,"id":1,"name":"张三","sex":"男"}
10-08 18:18:41.436 28438-28438/com.panda.gsoninstancecreatortest E/user3: [{"age":20,"id":1,"name":"张三","sex":"男"},{"age":23,"id":2,"name":"李四","sex":"男"}]
10-08 18:18:41.441 28438-28438/com.panda.gsoninstancecreatortest E/user4: [{"age":23,"id":1,"name":"张三","sex":"男"},{"age":33,"id":2,"name":"李四","sex":"男"}]

从结果中我们可以看到user2和user4都是在user1和user3的基础上继续解析了新的json数据。大功告成!

HTTPS原理及OKHTTP对HTTPS的支持

HTTPS原理

我们先看一下定义,来自wikipedia的一个介绍:

HTTPS (also called HTTP over Transport Layer Security (TLS), HTTP over SSL, and HTTP Secure) is a communications protocol for secure communication over a computer network which is widely used on the Internet. HTTPS consists of communication over Hypertext Transfer Protocol (HTTP) within a connection encrypted by Transport Layer Security, or its predecessor, Secure Sockets Layer. The main motivation for HTTPS is authentication of the visited website and protection of the privacy and integrity of the exchanged data.

从这个定义中我们可以看出,HTTPS是包含了HTTP协议及SSL /TLS协议这两部分内容,简单的理解就是基于SSL/TLS进行HTTP的加密传输。HTTP是一个应用层的协议,定义了很多请求和响应方通信的遵循的规则,这部分内容可以从HTTP权威指南这部巨作中得到很详细的介绍,这里就不赘述了。其实主要还是想探讨一下SSL/TLS协议的一些具体细节,毕竟这是HTTPS区别于HTTP最大的地方,首先我们来看一下一个SSL/TLS完整的握手过程。

SSL/TLS握手过程

很复杂的交互过程,但是理解下来就是用非对称加密的手段传递密钥,然后用密钥进行对称加密传递数据。在这个握手过程中最重要的就是证书校验,其他就是正常的数据交互过程。如何校验一个证书合法有很大的文章,处理不好就会让你的网络失去了安全性。一个证书的校验,主要包括以下几个方面:

  • 第一,校验证书是否是由客户端中“受信任的根证书颁发机构”颁发;
  • 第二,校验证书是否在上级证书的吊销列表;
  • 第三,校验证书是否过期;
  • 第四,校验证书域名是否一致。

一天我们的QA妹子气愤愤的找到我说,为啥别人的APP可以用Charles抓到HTTPS的包,为啥我们的不能,我心中窃喜的告诉她只能说明我们技高一筹了。具体如何做到的后面我会分享一下我们的做法,先讨论一下Charles如何实现https的抓包的,这里面涉及到一个中间人攻击的问题。

一个针对SSL的中间人攻击过程如下:

image.png

中间人其实是做了一个偷梁换柱的动作,核心是如何欺骗客户端,从而让客户端能够放心的与中间人进行数据交互而没有任何察觉。我们来看Charles如何做到HTTPS抓包的,网上有很多Charles如何抓HTTPS包的教程,几步就搞定了,其中最核心的就是:

将私有CA签发的数字证书安装到手机中并且作为受信任证书保存

自签发一个证书实现上述二、三、四条校验规则很简单,要把这个证书安装到手机端信任列表必须得到用户的许可,这里不好绕过,但是鉴于大部分用户的网络安全意识比较差,有时也会稀里糊涂的信任了,那我们作为APP的开发人员,能否避免这种情况的发生呢?

其实也很简单,我们把服务端的证书内置在我们的APP里,我们在做服务端证书校验的时候只比对是否和这个证书完全相同,不同就直接抛错,那中间人便没有办法绕过证书进行攻击。但是这里面也有一个问题就是服务端的证书可能会过期或者升级,而且服务端往往为了提高网络的安全性,证书的有效时间不会设置太长,这样APP就会因为这个证书的事情频繁发版,也很痛苦。(前段时间我司IOS的APP就是因为授权企业用户的证书没有及时更新,导致大家无法正常打开APP,血的教训导致我们不想重走这条路)可能你又想到了,我们可以把证书配置在后端,有更新的时候直接去下载不就完了,那我们的证书下载没有没拦截的风险吗,一旦拦截,我们所有的证书校验都会失效,比直接信任手机内置的证书更可怕。我们既不想只信任我们服务器的证书,又不想信任手机上所有的 CA 证书。有个不错的的信任方式是把签发我们服务器的证书的根证书导出打包到APP中,这样虽然不能做到百分之百的证书无漏洞,但是相比于信任手机中几百个证书,我们只信任一个风险会小很多,这也就是我们的QA妹子用Charles抓不了我们的包的原因。~~~

OKHTTP

作为一个Android开发者,我们来看一下广泛使用的网络库OKHTTP对于HTTPS的支持。下面这段话摘自OKHTTP对于HTTPS的介绍中(地址请戳):

OkHttp attempts to balance two competing concerns:

  • Connectivity to as many hosts as possible. That includes advanced hosts that run the latest versions of boringssl and less out of date hosts running older versions of OpenSSL.
  • Security of the connection. This includes verification of the remote webserver with certificates and the privacy of data exchanged with strong ciphers.

几个与HTTPS相关的API:

SSLSocketFactory:

安全套接层工厂,用于创建SSLSocket。默认的SSLSocket是信任手机内置信任的证书列表,我们可以通过OKHttpClient.Builder的sslSocketFactory方法定义我们自己的信任策略,比如实现上面提到的我们只信任服务端证书的根证书,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 载入证书
*/
public static SSLSocketFactory getSSLSocketFactory(InputStream... certificates) {
try {
//用我们的证书创建一个keystore
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
int index = 0;
for (InputStream certificate : certificates) {
String certificateAlias = "server"+Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
try {
if (certificate != null) {
certificate.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//创建一个trustmanager,只信任我们创建的keystore
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init(
null,
trustManagerFactory.getTrustManagers(),
new SecureRandom()
);
return sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

X509TrustManager:

1
2
3
4
5
6
7
public interface X509TrustManager extends TrustManager {
void checkClientTrusted(X509Certificate[] var1, String var2) throws CertificateException;

void checkServerTrusted(X509Certificate[] var1, String var2) throws CertificateException;

X509Certificate[] getAcceptedIssuers();
}

checkServerTrusted方式实现了对于服务端校验,这里一般使用系统默认的实现,有些教程讲到这样配置ssl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

}

public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}

public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}, null);
return sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError();
}
}

千万不能这么做,这样将你是没有做任何校验的,这里推荐使用系统默认的,他会在校验过程中发现有异常直接抛出。

HostnameVerifier:

1
2
3
public interface HostnameVerifier {
boolean verify(String var1, SSLSession var2);
}

这个接口主要实现对于域名的校验,OKHTTP实现了一个OkHostnameVerifier,对于证书中的IP及Host做了各种正则匹配,默认情况下使用的是这个策略。有时你遇到了一些奇怪的校验问题,大部分教程会教你这样:

1
2
3
4
5
6
OKHttpClient.Builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
})

其实这样你是完全放弃了hostname的校验,这也是相当不安全的。