62b198349200341b6d72c53a95be1a40.jpeg

一般情况下公司都是通过CA机构来购买SSL证书,但是这种证书费用普遍比较贵,所以在debug环境下可以考虑使用自签名证书。

这篇内容将介绍Android如何使用自签名证书,主要分为以下4个步骤:

  1. 创建服务端SSL自签名证书

  2. 下载并配置 Tomcat 服务器

  3. Android端导入SSL证书

  4. 同时支持自签名证书和系统证书

43c6b70b39b6873019b4cc2fd1b5eef5.gif

1 创建服务端SSL自签名证书

通过工具Keytool,可以使用如下命令快速生成Java服务器能够识别的jks格式证书:

keytool -genkey -alias my_server -keyalg RSA -keystore my_server.jks -validity 3600 -storepass 123456

执行以上命令后,会弹出一些咨询信息,可以根据实际情况填写或者随意填写也OK。如下所示:

What is your first and last name?
  [Unknown]:  Danny
What is the name of your organizational unit?
  [Unknown]:  Null
What is the name of your organization?
  [Unknown]:  Null
What is the name of your City or Locality?
  [Unknown]:  SH
What is the name of your State or Province?
  [Unknown]:  SH
What is the two-letter country code for this unit?
  [Unknown]:  CN
Is CN=Danny, OU=Null, O=Null, L=SH, ST=SH, C=CN correct?
  [no]:  yes

Enter key password for <my_server>
    (RETURN if same as keystore password):
Re-enter new password:

注意最后需要输入密码123456。执行成功之后,就可以在当前目录看到一个新生成的服务端SSL证书:my_server.jks 。

1765b25d76f9e65c3854aa1105d3a7c1.gif

2 下载并配置 Tomcat 服务器

创建好服务端使用的SSL证书之后,接下来就需要将其配置到服务端的配置项里,这里我们使用Tomcat搭建本地服务用来演示。

2.1 下载 Tomcat (Mac电脑版)

浏览器中输入Tomcat官方下载链接:

https://tomcat.apache.org/download-90.cgi

选中 core zip 包进行下载,如下图:

8b7fa642e1e5f120ede0cc3157f080b3.png

解压下载之后的zip包,并重命名为Tomcat,然后在Terminal中将其移动/Library目录,接下来进入 /Library/Tomcat/bin 目录,使用如下命令启动Tomcat服务器:

sudo sh startup.sh

上述命令需求admin权限,执行成功之后,就可以验证Tomcat是否成功打开。在浏览器中输入 localhost:8080,如果出现如下截图内容,则说明Tomcat启动成功。

422514b40eb4e72f2b59fe309baa52f5.png

2.2 Tomcat配置SSL证书

下载好Tomcat并启动成功之后,接下来就需要配置SSL证书了。进入Tomcat/conf/目录,编辑 server.xml 配置文件,在 <Service> 标签中添加如下 <Connector> 标签:

<?xml version="1.0" encoding="UTF-8"?>
<Server>
  <Service>
   ...
    <Connector
    SSLEnabled="true"
    acceptCount="100"
    clientAuth="false"
    disableUploadTimeout="true"
    enableLookups="true"
    keystoreFile="/Users/Danny.Jiang/Desktop/certificate/android_cert/my_server.jks"
    keystorePass="123456"
    maxSpareThreads="75"
    maxThreads="200"
    minSpareThreads="5"
    port="8181"
    protocol="org.apache.coyote.http11.Http11NioProtocol"
    scheme="https"
    secure="true"
    sslProtocol="TLS" />
  </Service>
</Server>

添加以上配置之后,重新在浏览器中输入 https://localhost:8181/ 就会看到如下warning信息:

32e016f8f74571ff6faa524728617041.png

看到上述warning信息,就说明服务端的SSL证书配置成功了。

f0d07bac5d89d45ae6cc771f8157dbda.gif

3 Android端导入SSL证书

3.1 导出Android端SSL证书

使用如下命令,从上面创建的服务端证书server.jks中导出客户端证书:

keytool -export -alias my_server -file my_client.cer -keystore my_server.jks -storepass 123456

上述命令执行成功之后,将生成my_client.cer证书文件,这个就是Android端使用的自签名SSL证书。

3.2 将证书导入Android项目

创建Android项目SelfSignedCertificateDemo,如下

8e5a12554f54c254e03cdc1d53549422.png

为了增加对比效果,我们创建2个Button控件,分别用来获取baidu和本地Tomcat服务器的数据,如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="getBaidu"
        android:text="获取baidu首页数据" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="getTomcat"
        android:text="获取Tomcat服务器数据" />

</LinearLayout>

getBaidu和getTomcat这2个方法具体如下:

private String BAIDU_URL = "https://www.baidu.com/";
private String TOMCAT_URL = "https://192.168.1.105:8181/";
private static OkHttpClient mOkHttpClient;

public void getBaidu(View view) {
    Request request = new Request.Builder()
            .url(BAIDU_URL)
            .build();
    mOkHttpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            Log.i("TAG", "getBaidu onFailure: " + e.getMessage());
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            Log.i("TAG", "getBaidu response: " + response.body().string());
        }
    });
}

public void getTomcat(View view) {
    Request request = new Request.Builder()
            .url(TOMCAT_URL)
            .build();
    mOkHttpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            Log.i("TAG", "getTomcat onFailure: " + e.getMessage());
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            Log.i("TAG", "getTomcat response: " + response.body().string());
        }
    });
}

上述代码中的TOMCAT_URL需要改为自己电脑的IP地址。默认情况下上述2个方法的执行结果如下:

getBaidu response: <!DOCTYPE html>
    <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
                    </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>

getTomcat onFailure: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

可以看出getTomcat请求报错,原因是客户端验证服务端SSL证书失败。最简单的办法就是强制客户端让不检查所有的SSL证书,做法如下:

1 创建自定义X509TrustManager和HostnameVerifier

/**
 * 创建信任所有证书的TrustManager
 * @return
 */
private static X509TrustManager createTrustAllTrustManager() {
    return new X509TrustManager() {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    };
}

//实现信任所有域名的HostnameVerifier接口
private static class TrustAllHostnameVerifier implements HostnameVerifier {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        //域名校验,默认都通过
        return true;
    }
}

上面自定义X509TrustManager中的checkClientTreusted和checkServerTrusted都是空实现,也就是不检查客户端和服务端的SSL证书信息。另外在自定义HostnameVerifier中的verify方法返回true,默认信任所有域名,否则在请求时会报如下错误:

Hostname XXX not verified:

2 根据自定义X509TrustManager创建OkHttpClient

将之前创建的空实现X509TrustManager传入createSSLClient方法,如下

private static OkHttpClient createSSLClient(X509TrustManager x509TrustManager){
    OkHttpClient.Builder builder = new OkHttpClient.Builder()
            .connectTimeout(60, TimeUnit.SECONDS)
            .sslSocketFactory(createSSLSocketFactory(x509TrustManager),x509TrustManager)
            .hostnameVerifier(new TrustAllHostnameVerifier());
    return builder.build();
}

private static SSLSocketFactory createSSLSocketFactory(TrustManager trustManager) {
    SSLSocketFactory ssfFactory = null;
    try {
        SSLContext sc = SSLContext.getInstance("TLS");
        sc.init(null, new TrustManager[]{trustManager}, new SecureRandom());
        ssfFactory = sc.getSocketFactory();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ssfFactory;
}

通过上述方法,即可创建出不检查所有SSL证书的OkHttpClient对象。再次执行getBaidu和getTomcat方法,执行结果如下:

getBaidu response: 
    <!DOCTYPE html>
    <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
                    </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>

getTomcat response: 

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8" />
            <title>Apache Tomcat/8.5.72</title>
            <link href="favicon.ico" rel="icon" type="image/x-icon" />
            <link href="tomcat.css" rel="stylesheet" type="text/css" />
        </head>

        <body>
            <div id="wrapper">
                <div id="navigation" class="curved container">
                    <span id="nav-home"><a href="https://tomcat.apache.org/">Home</a></span>
                    <span id="nav-hosts"><a href="/docs/">Documentation</a></span>
                    <span id="nav-config"><a href="/docs/config/">Configuration</a></span>
                    <span id="nav-examples"><a href="/examples/">Examples</a></span>
                    <span id="nav-wiki"><a href="https://wiki.apache.org/tomcat/FrontPage">Wiki</a></span>
                    <span id="nav-lists"><a href="https://tomcat.apache.org/lists.html">Mailing Lists</a></span>
                    <span id="nav-help"><a href="https://tomcat.apache.org/findhelp.html">Find Help</a></span>
                    <br class="separator" />
                </div>
    。。。
    </html>

可以看出,百度和Tomcat服务器的数据都能成功获取。但是这种方式存在极大的安全漏洞。因为并没有做任何SSL证书的校验,很容易被MITM(Man In The Middle)攻击。

比较好的优化方式当然是在客户端使用自签名SSL证书,验证服务器的身份合法之后,再进行后续的数据传输操作。通过以下步骤将客户端证书my_client.cer导入到项目中:

1 将my_client.cer保存在assets文件夹中

创建assets目录,并将my_client.cer保存到此目录下,如下:

2464e903f0c98ee356b2ba0cf3be131e.png

保存好后,通过如下方式将证书转换为InputStream格式:

private InputStream getInputStreamFromAsset(){
    InputStream inputStream = null;
    try {
        inputStream = getAssets().open("my_clent.cer");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return inputStream;
}

2 创建只信任自签名证书的X509TrustManager

将转换后的InputStream传入以下方法,创建自定义X509TrustManager

/**
     * 创建只信任指定证书的TrustManager
     * @param inputStream:证书输入流
     * @return
     */
    @Nullable
    private static X509TrustManager createTrustCustomTrustManager(InputStream inputStream) {
        try {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);

            Certificate certificate = certificateFactory.generateCertificate(inputStream);
            //将证书放入keystore中
            String certificateAlias = "ca";
            keyStore.setCertificateEntry(certificateAlias, certificate);
            if (inputStream != null) {
                inputStream.close();
            }

            TrustManagerFactory trustManagerFactory = TrustManagerFactory.
                    getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);
            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                throw new IllegalStateException("Unexpected default trust managers:"
                        + Arrays.toString(trustManagers));
            }
            return (X509TrustManager) trustManagers[0];
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

以上方法将自签名证书保存到Java对象KeyStore中,并最终创建只信任自签名证书的X509TrustManager对象。重新将此对象传给上文中的createSSLClient方法后,就是一个加载自签名SSL证书的OkHttpClient对象了。

再次执行getBaidu和getTomcat方法,执行结果如下:

getBaidu onFailure: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.


getTomcat response: 

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8" />
            <title>Apache Tomcat/8.5.72</title>
            <link href="favicon.ico" rel="icon" type="image/x-icon" />
            <link href="tomcat.css" rel="stylesheet" type="text/css" />
        </head>

        <body>
        ...
        </body>
    </html>

以上结果显示获取baidu数据失败,而获取Tomcat数据成功。

log 显示结果正好跟最初的默认结果相反,这是因为当前所有的https请求都使用自签名证书去校验服务器身份。因为Tomcat配置了本地证书所以能够成功验明正身;但是baidu并没有配置我们的自签名证书,也就无法正确验明身份了。

b2b691632fd3d43ccdd914ad670f7554.gif

支持自签名证书和系统证书

为了能让getBaidu正常获取数据,并且getTomcat也在安全的环境下获取数据,我们需要在这个X509TrustManager中再添加对系统自带SSL证书的信任。具体如下:

/**
 * 创建既信任自签名证书又信任系统自带证书的TrustManager
 */
private static X509TrustManager createTrustCustomAndDefaultTrustManager(InputStream inputStream) {
    try {
        // 获取信任系统自带证书的TrustManager
        final X509TrustManager systemTrustManager = getSystemTrustManager();
        // 获取信任自签名证书的TrustManager
        final X509TrustManager selfTrustManager = createTrustCustomTrustManager(inputStream);

        return new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                systemTrustManager.checkClientTrusted(chain, authType);
            }
            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                try {
                    // 默认使用信任自签名证书的TrustManager验证服务端身份
                    selfTrustManager.checkServerTrusted(chain, authType);
                } catch (CertificateException e) {
                    // 此处使用系统自带SSL证书验证服务端身份
                    systemTrustManager.checkServerTrusted(chain, authType);
                }
            }
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return systemTrustManager.getAcceptedIssuers();
            }
        };
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

/**
 * 创建信任系统自带证书的TrustManager
 */
private static X509TrustManager getSystemTrustManager() throws NoSuchAlgorithmException, KeyStoreException {
    TrustManagerFactory tmf = TrustManagerFactory
                .getInstance(TrustManagerFactory.getDefaultAlgorithm());

    tmf.init((KeyStore) null);
    for (TrustManager tm : tmf.getTrustManagers()) {
        if (tm instanceof X509TrustManager) {
            return (X509TrustManager) tm;
        }
    }
    return null;
}

可以看出在自定义X509TrustManager的checkServerTrusted方法中,先使用信任自签名证书的TrustManager验证服务端,如果没有验证成功,则继续使用系统默认TrustManager来继续验证。

通过以上设置之后,getBaidu 和 getTomcat这2个方法都能正确获取数据了。
对源码有需求,或者想一起探讨共同进步的,欢迎关注公众号发私信或者加微信。

f0b1301da5907ac2d6c03991ea879f06.gif

如果你喜欢本文

长按二维码关注

0291d144db8d0ba2eb19cf6912c2c071.gif

9fd43e38ac32d30c91a8151f706127eb.jpeg

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐