在Android 4.1+中强制开启TLSv1.1和TLSv1.2

在做MQTT时遇到了android4.4手机没法连接服务端的情况,查阅发现是因为系统默认没有开启TLSv1.1和TLSv1.2的支持。抛出的错误如下:

1
2
3
 MqttException (0) – javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x7861ada8: Failure in SSL library, usually a protocol error
 
error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version (external/openssl/ssl/s23_clnt.c:741 0x71e78cf8:0x00000000)

Android 4.1开始支持TLSv1.1和TLSv1.2,但是在4.1~4.4版本中默认都是关闭。在某些情况下可能会导致SSL链接建立失败的问题(服务端强制使用1.1和/或1.2,而android 4.1~4.4中没有开启。),这时需要开启TLSv1.1和TLSv1.2。

我们需要使用自定义的SSLSocketFactory去创建SSL链接。

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
54
55
public class CustomSslSocketFactory extends SSLSocketFactory {
 
    private SSLSocketFactory sslSocketFactory;
 
    public CustomSslSocketFactory(TrustManager[] trustManager) throws KeyManagementException, NoSuchAlgorithmException {
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, trustManager, null);
        sslSocketFactory = context.getSocketFactory();
    }
 
    @Override
    public String[] getDefaultCipherSuites() {
        return sslSocketFactory.getDefaultCipherSuites();
    }
 
    @Override
    public String[] getSupportedCipherSuites() {
        return sslSocketFactory.getSupportedCipherSuites();
    }
 
    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(s, host, port, autoClose));
    }
 
    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port));
    }
 
    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port, localHost, localPort));
    }
 
    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port));
    }
 
    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(address, port, localAddress, localPort));
    }
 
    @Override
    public Socket createSocket() throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket());
    }
 
    private Socket enableTLSOnSocket(Socket socket) {
        ((SSLSocket) socket).setEnabledProtocols(new String[] {"TLSv1.2", "TLSv1.1"});
        return socket;
    }
}

需要注意的是如果遇到如下错误,那需要override一个空参数的createSocket()方法。

1
2
3
4
5
6
7
Error:java.net.SocketException: Unconnected is not implemented 
 
//Override createSocket()
    @Override
    public Socket createSocket() throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket());
    }

使用这个自定义的SSLSocketFactory类就可以去做TLSv1.1和TLSv1.2的SSL链接就可以了。

Retrofit/OkHttp SSL Pining

这周在做竞品研究的时候发现,只要安装抓包工具的相关CA证书,就可以看到SSL加密的数据了。为了防止request结构被其他人看到,决定使用SSL Pinning,使得App只认绑定证书。这样即便安装了抓包工具的证书,也会因为证书的key不对而不会建立链接。

OkHttp提供了一个CertificatePinner类可以方便的设置SSL Pinning,但实验下来发现在Android中不行。使用抓包工具还是可以看到加密内容。或许是我的使用方法有问题。不过我还是找另外一种方法做SSL Pinning。

这里使用的方法是从CA证书中读取相关的信息,并创建一个只信任指定CA证书的SSLSocketFactory对象,注入到OkHttp中。这样OkHttp会使用注入的SSLSocketFactory去创建SSL Socket了。

第一步提取CA证书(以Google为例):

Get Google CA
Get Google CA
Click “View Certificate”
Click “Export”

导出后得到一个后缀名为.crt的文件,该文件既是Google的CA证书。

 

第二步读取CA证书并创建创建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
SSLSocketFactory = sslSocketFactory = null;
try {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
 
    InputStream caInput = context.getResources().openRawResource(R.raw.ca);
    Certificate ca = null;
    try {
        ca = certificateFactory.generateCertificate(caInput);
    } catch (CertificateException e) {
        e.printStrackTrace();
    } finally {
        caInput.close();
    }
 
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    if (ca == null) {
        return null;
    }
    keyStore.setCertificateEntry("ca", ca);
 
    String algorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
    trustManagerFactory.init(keyStore);
 
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
 
    sslSocketFactory = sslContext.getSocketFactory();
} catch (CertificateException|IOException|KeyStoreException|NoSuchAlgorithmException|KeyManagementException e) {
    e.printStackTrace();
}

 

第三步在创建OkHttpClient时将SSLSocketFactory注入到Builder:

1
2
3
OkHttpClient client = new OkHttpClient.Builder()
        .sslSocketFactory(SslSocketFactoryHelper.getSslSocketFactory(context.get()))
        .build();

这样就大功告成了。

再测试一下会发现因为CA证书不对,链接会无法建立。也就达到了SSL Pinning的效果。

Retrofit 2简单入门

Retrofit是一个Android/Java的type-safe HTTP客户端,它能够大大简化写网络请求的复杂程度。而之前发布的2.0版本改动非常大。1.x版本默认使用URLConnection作为底层Http Client,而2.0版本开始只能使用OkHttpClient作为底层Http Client。而数据格式的Converter配置也发生了变化。这篇文章做一个简单的入门小结。

首先在Android Studio中引入Retrofit 2以及相关的依赖库:

1
2
3
4
5
    compile 'com.squareup.okhttp3:okhttp:3.2.0'
    compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
 
    compile 'com.squareup.retrofit2:retrofit:2.0.0'
    compile 'com.squareup.retrofit2:converter-moshi:2.0.0'

这里是用Moshi converter库作为Converter例子。

首先创建一个最简单的HTTP客户端:

1
    Retrofit retrofit = new Retrofit.Builder().baseUrl("http://www.example.com/").build();

1.x中使用endPoint(String)方法指定endPoint,在2.0中改成了baseUrl(String)方法。

创建一个使用Moshi converter的HTTP客户端:

1
2
3
4
    Retrofit retrofit = new Retrofit.Builder()
                                .baseUrl("http://www.example.com/")
                                .addConverterFactory(MoshiConverterFactory.create())
                                .build();

使用这个客户端收到的返回消息的body都会使用Moshi Json库去做反序列化过程。

当我们所有的请求头部都有指定字段时,我们可以先创建一个定制的OkHttpClient对象,再把这个对象设置为Retrofit的底层Http client就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    OKHttpClient client = new OkHttpClient.Builder();
    client.addInterceptor(new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request().newBuilder()
                    .addHeader("Context-Type", "application/json").build();
            return chain.proceed(request);
        }
    });
 
    Retrofit retrofit = new Retrofit.Builder()
                                .client(client)
                                .baseUrl("http://www.example.com/")
                                .build();

下面我们创建一个Api接口:

1
2
3
4
5
    interface Api {
        @GET("api/example/{uuid}")
        Call<void> getInfo(@Path("uuid") String uuid);
    }
</void>

发起请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    Retrofit retrofit = new Retrofit.Builder().baseUrl("http://www.example.com/").build();
    Api api = retrofit.create(Api.class);
    Call<void> getInfo = api.getInfo("uuid");
    getInfo.enqueue(new Callback</void><void>() {
        @Override
        public void onResponse(Call</void><void> call, Response</void><void> response) {
            if (response.isSuccessful() {
                // server says success
            } else {
                // server says failure
            }
        };
 
        @Override
        public void onFailure(Call</void><void> call, Throwable t) {
            // request failed. network error or something else.
        }
    });
</void>

我们通过enqueue(Callback)方法发起请求并且获得结果。Callback接口中的onResponse方法代表服务器成功收到了请求并且返回了结果,我们需要通过Response.isSuccessful()方法得知返回的结果是否正确(也就是说是否按照Converter成功反序列化了),如果其返回为false,则可能是反序列化失败活着服务器返回了400等其它错误。而Callback.onFailure方法代表请求发送失败,也就是说请求没有发送到服务器,一般是Api接口写的有问题,这个需要检查接口的定义了。

Retrofit使用起来非常方便。当然它还支持非常好的自定义功能,这个需要进一步探索。

在Android Studio中配置数据库ORM库greenDAO

1.首先在项目app目录下的build.gradle中添加依赖:

1
compile 'de.greenrobot:greendao:2.0.0'

2.然后通过File菜单中的New选项新建一个Java Library Module,并且取名为greendaogenerator,Class取名为GreenDaoGenerator,并且在其build.gradle中添加依赖:

1
compile 'de.greenrobot:greendao-generator:2.0.0'

3.在新建的Java Library Module中的GreenDaoGenerator.java中开始编写数据库生成代码:

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
import de.greenrobot.daogenerator.DaoGenerator;
import de.greenrobot.daogenerator.Schema;
 
/*
 * example: https://github.com/greenrobot/greenDAO/blob/master/DaoExampleGenerator/src/de/greenrobot/daogenerator/gentest/ExampleDaoGenerator.java
 */
public class GreenDaoGenerator {
    // TODO: your db operation java file location
    private final static String LOCATION = "";
 
    public static void main(String[] args) throws Exception {
        Schema schema = new Schema(1, "example.db");
 
        // TODO: add schema
        addTable(schema);
 
        new DaoGenerator().generateAll(schema, LOCATION);
    }
 
    private static void addTable(Schema schema) {
        // TODO: add schema table
        Entity entity = schema.addEntity("Table");
        entity.addIdProperty();
    }
}

这个类是你生成数据库操作类的工具。Schema的构造方法中第一个参数是数据库的版本号,第二个是创建的数据库文件名。addTable则是创建表格用,可以写多个这样的方法来创建不同的表格。而main中最后一句则是生成相关的数据库操作类,相关的文件会放倒LOCATION指定的目录下。
注意,在APP发布后,每次对数据库结构进行改动后,都应该对数据库版本进行升级。并且在相应的Helper类中把数据库改动的内容通过相应的方法来实现数据库结构的升级(以后讲述)。
4.新建一个Run/Debug Configuration,类型为Application,Main Class选择GreenDaoGenerator,Module则选择为greendaogenerator,并且赋予这个Configuration一个名字为DaoGenerator。完成后选择这个新建的Configuration,并且运行。一些顺利的话,你会在设定的Location中看到生成的数据库操作类。到此几本的配置操作就大功完成啦。

总结Android App的一些技巧以及注意事项

我做Android App开发其实也就半年多时间,而且我是从Python转到Java,之前也没有系统学习过Java,下面内容是总结这半年多的经验,但难免理解不到位的地方,看到的各位请大胆直接在评论中指出。

1. App中可以直接写一个继承自Application的自定义类用作一个单例,可以存放一些全局的方法和变量。但请记住单例不能跨进程(我犯过这个错误)!在不同进程中Android会为每个进程创建一个Application对象,进程间通信还请使用AIDL或者Handler/Messenger模式。创建一个自定义的Application类很简单,只要继承自Application就可以了:

1
2
3
4
5
6
7
public class BaseApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // your code here
    }
}

然后在AndroidManifest.xml中将android:name指定为该自定义Application:

1
2
3
4
5
6
7
8
9
10
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.example" >
 
    <application android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" 
        android:name="BaseApplication">
 
     </application>
</manifest>

这样就大功告成了。在Activity中通过(BaseApplication) getApplication();的方法来访问。

2. 自定义一个Activity基类是一个很好的方法,通过把一些通用方法写入Activity可以减少那些真正展现在用户面前的Activity代码。通用方法往往是界面布局上的方法,比如设定ActionBar,设定FullScreen等。之前我写的几个App都没有使用这个方法,导致部分Activity类的代码特别长,看起来吃力。结合上面的Application自定义类,我们可以创建一个不错的BaseActivity:

1
2
3
4
5
6
7
8
9
public class BaseActivity extends ActionBarActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        application = (BaseApplication) getApplication();
    }
 
    protected BaseApplication application;
}

3. Activity与Service通信有好几种方法,不同的方法适合不同情况。如果Activity与Service在同进程内,那么直接使用Binder绑定的方法进行通信;如果Activity与Service在不同进程中,但Activity只需要向Service发送信息而不需要从Service中获取信息,那么使用Handler/Messenger的方法进行通信会比较方便;如果Activity需要从另一进程中的Service获取信息,那么必须使用AIDL的方法进行通信。

a. 同进程内使用Binder方法非常简单,只需要创建一个自定义的Binder类然后创建一个getService方法即可:

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
// Binder example: MService.java
public class MService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }
 
    private LocalBinder binder = new LocalBinder();
    public class LocalBinder extends Binder {
        public MService getService() {
            return MService.this;
        }
    }
}
 
 
// Binder example: MActivity.java
public class MActivity extends BaseActivity {
    @Override
    public void onStart() {
        super.onStart();
        bindService(new Intent(this, MService.class), mServiceConnection, Context.BIND_AUTO_CREATE);
    }
 
    @Override
    public void onStop() {
        super.onStop();
        if (isMServiceBound) {
            unbindService(mServiceConnection);
            isMServiceBound = false;
        }
    }
 
    private boolean isMServiceBound = false;
    private MService mService;
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = ((MService.LocalBinder) service).getService();
            isMServiceBound = true;
        }
 
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
            isMServiceBound = false;
        }
    }
}

b. 不同进程中使用Handler/Messenger模式。这种方法适用于由Activity向Service发送消息。当然也是可以实现反向发送消息,不过需要两边都实现一套Handler/Messenger,对于这种情况我更倾向使用AIDL的方法去实现。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Handler/Messenger example: MService.java
public class MService extends Service {
    @Override
    public IBinder onBind() {
        return messenger.getBinder();
    }
 
    private void sayHello(int name) {
        Log.i("MService", "hello " + Integer.toString(name));
    }
 
    private final Messenger messenger = new Messenger(new IncomingHandler());
    public final static int MESSAGE_SAY_HELLO = 0;
    private class IncomingHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            swtich (msg.what) {
                case MESSAGE_SAY_HELLO:
                    sayHello(msg.arg1);
                    break;
                defaule:
                    super.handleMessage(msg);
            }
        }
    }
}
 
 
// Handler/Messenger example: MActivity.java
public class MainActivity extends BaseActivity {
    @Override
    public void onStart() {
        super.onStart();
        bindService(new Intent(this, MService.class), mServiceConnection, Context.BIND_AUTO_CREATE);
    }
 
    @Override
    publicvoid onStop() {
        if (isMServiceBound) {
            unbindService(mServiceConnection);
            isMServiceBound = false;
        }
    }
 
    private void sayHello() {
        try {
            // 这里偷懒了,关于使用Message传递其他类型数据的方法请参考官方文档
            mService.send(Message.obtain(null, MService.MESSAGE_SAY_HELLO, 123, 0));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
 
    private boolean isMServiceBound = false;
    private Messenger mService;
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = new Messenger(service);
            isMServiceBound = true;
            sayHello();
        }
 
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
            isMServiceBound = false;
        }
    }
}

Handler/Messenger还可以在很多情形下使用,我想我应该避免使用Broadcast而更多的使用Handler/Messenger模式才对,因为这种模式相比广播更加安全而且效率更加高。

c. AIDL是Android中实现跨线程访问的推荐方法,AIDL的全程是Android Interface Definition Language,它的语法就是一些Java的声明语法,所以很容易理解。在Android Studio中AIDL文件都是放在app/src/main/aidl/中相对应于java代码目录结构的文件夹下。比如我有一个app/src/main/java/com/example/example/service/MService.java文件,那么这个文件对应的AIDL文件就在app/src/main/aidl/com/example/example/service/MService.aidl。需要注意的是在Android Studio中AIDL的文件在编译后如果再进行修改的话,修改部分不会再次被编译,所以只能删除原有文件后重新创建才可以,这点可能在后续的Studio版本中改进,至于Eclipse的情况因为我不用就不清楚了。

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
54
55
56
57
58
// AIDL example: MServiceAidl.aidl
interface MServiceAidl {
    // 定义相应的接口
    void sayHello(String name);
}
 
// AIDL example: MService.java
public class MService extends Service {
    @Override
    public IBinder onBind() {
        return binder;
    }
 
    private void doSayHello(String name) {
        Log.i("MService", "hello " + name);
    }
 
    private final MServiceAidl.Stub binder = new MServiceAidl.Stub() {
        public sayHello(String name) {
            doSayHello(name);
        }
    }
}
 
// AIDL example: MActivity.java
public class MActivity extends BaseActivity {
    @Override
    public void onStart() {
        super.onStart();
        bindService(new Intent(this, MService.class), mServiceConnection, Context.BIND_AUTO_CREATE);
    }
 
    @Override
    public void onStop() {
        super.onStop();
        if (isMServiceBound) {
            unbindService(mServiceConnection);
            isMServiceBound = false;
        }
    }
 
    private boolean isMServiceBound = false;
    private MServiceAidl mService;
    privaet ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = new Messenger(service);
            isMServiceBound = true;
            mService.sayHello("world");
        }
 
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
            isMServiceBound = false;
        }
    }
}