Android+Nginx一步步配置https单向/双向认证请求

  最近想实现一个旧项目https的防抓包功能,重新学习并且配置了下https相关通信知识,参考了不少文章,有些文章比较旧不全或者有误,走了不少弯路和坑,所以整理出来方便自己巩固以及供大家参考,指出不足或者有误之处互相学习。
  本文的服务端环境:

Debian 9.8
Nginx

原创文章,欢迎转载,转载请注明:ifish.site
作者:JaydenZhou

一、需要的前置知识点

  本文需要的前置知识点,这也是我刚接触时候绕得很晕的问题,自己前后端流程走一遍后,就清晰多了。

网络相关的知识点:
https通信,CA发证机构,自签发机构,公钥,私钥,数字证书,数字签名、相关cer,pem,scr,key等关键字定义。

Android相关的知识点:
如何发起网络请求,如何设置单向/双向认证ssl,如何把一些数字证书转成Android识别的bks格式证书等。

后端相关知识点:
Liunx基本知识;如何域名解析(或直接ip)访问;https的CA证书/自签名证书如何生成;如何配置nginx中的https访问;

  https的通信,是从非对称加密(RSA)到对称加密(AES)的一个过程,其中数字证书扮演着重要作用。 借助文章:https://juejin.im/post/5c9cbf1df265da60f6731f0a 里面所讲,

数字证书  = 公钥 + 签名 + 申请者和颁发者的信息
签名 = 私钥 + 信息摘要(hash处理过的不可逆的明文信息)

类比于我们的身份证(数字证书) = 证件号(公钥) + 公安盖章(签名) + 个人姓名/发证公安局(申请者和颁发者的信息)。
私钥一般以.key结尾,用它才能跟对应的数字证书(公钥)互相解密。

二、单向/双向认证的应用场景在哪里呢?

单向认证: 这里有个简单的理解,凡是你可以直接访问的网站(比如我的域名: https://ifish.site ); 直接请求的https的api等,都是单向认证,因为它只需要client端能够解密出server端的数字证书(分CA和自签发的),操作系统或者浏览器一般都内置了一堆相关CA的证书,所以可以直接访问;若是自签发的,浏览器会提示该证书不受信任。适用场景是站点访问,非高机密数据传输。
双向认证: 顾名思义,就是在单向基础上,添加上了服务端要校验客户端的公钥,客户端要自己保存着自己的私钥来加密,客户端发过来的请求要用该私钥来加密后,才能跟服务器进行完整通信。适用场景:企业间对应机密api接口的数据传输。

三、证书的生成

  配置好Linux环境后,首先我们要生成对应的证书,两种方法如下:

方法一:用CA签的证书(有收费 or 免费),这样浏览器就不会显示不信任提示,前提是要有合法的境内实名域名,然后比如在阿里云服务器管理后台界面进行证书的申请。

方法二:用openssl在自己服务器上,制作自签发的证书,浏览器也可以访问,但是会有不信任提示。

方法一按照对应服务商的提示来操作就行,这里讲下openssl来生成的方法,先抛出一个我自己现在也疑惑的问题:
为何要生成root根CA证书,然后再发布二级server和client证书? 是为了一个根证书可以直接管理多个二级证书?
本文为了简化演示https单向/双向认证,只需要生成对应的 server 和 client 相关证书就行,避免文件太多导致像我这样的新手造成的困惑和配置出错。
1.生成服务端key:

openssl genrsa -out server-key.key 1024

2.生成服务端证书请求文件(这步很关键,弹出信息填写提示时候,“Common Name”一定要填写你自己的域名,其他的可以直接回车):

openssl req -new -out server-req.csr -key server-key.key

比如我的域名是 ifish.site
name

3.生成服务端证书cer:

openssl x509 -req -in server-req.csr -out server-cert.cer -signkey server-key.key  -CAcreateserial -days 3650

4.生成客户端key(同上面方法一样):

openssl genrsa -out client-key.key 1024

5.生成服务端证书请求文件(Common Name最好一致):

openssl req -new -out client-req.csr -key client-key.key

6.生成客户端证书cer:

openssl x509 -req -in client-req.csr -out client-cert.cer -signkey client-key.key -CAcreateserial -days 3650

7.生成客户端带密码的p12证书(这步很重要,双向认证的话,浏览器访问时候要导入该证书才行;Android请求的时候也需要把它转成bks来请求双向认证):

openssl pkcs12 -export -clcerts -in client-cert.cer -inkey client-key.key -out client.p12

四、nginx配置

1.将生成的证书,为了方便管理,建议放到nginx相同目录下,比如我是放到“/usr/local/nginx/conf/ssl_cust” 里面;
2.打开对应的 conf 里面要https访问的域名,如果之前有配过443端口,那么只需要指定下对应的证书即可,其中

单向认证是:
ssl_certificate      ssl_cust/server-cert.cer;
ssl_certificate_key  ssl_cust/server-key.key;
双向认证是:
ssl_certificate      ssl_cust/server-cert.cer;
ssl_certificate_key  ssl_cust/server-key.key;
ssl_client_certificate   ssl_cust/client-cert.cer;
ssl_verify_client    on;

3.若完全没有配过https的话,可以参考我的配置:

server
    {
        listen       443 ssl;
        server_name  ifish.site www.ifish.site;
        ssl on;
        ssl_certificate      ssl_cust/server-cert.cer;
        ssl_certificate_key  ssl_cust/server-key.key;
# 双向认证一般不开启, #是注释掉
#        ssl_client_certificate   ssl_cust/client-cert.cer;
#        ssl_verify_client    on;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
    }

4.保存关闭后,执行以下命令让nginx重启生效:

sudo nginx -s reload

5.如此我们配置就生效了,可以用浏览器来验证下,如果只是单向认证,直接浏览器输入域名来访问即可,会有不安全提示;
若是双向认证,直接访问会出现 400 Bad Requst, 需要我们手动添加证书,这里是Chrome下的截图,我们需要添加之前我们生成的 client.p12 文件。
Android+Nginx一步步配置https单向/双向认证请求_第1张图片

五、Android代码请求

  终于到Android请求的代码写法了,为了简化Demo的独立访问,这里引入了 xUtils库 (https://github.com/wyouflf/xUtils3 ),当然你也可以自己手写或者用比如Retrofit、OkHttp等网络库。
单向认证的写法:
  其实单向认证,用xUtils的话可以不需要设置setSslSocketFactory内容,因为库代码DefaultParamsBuilder.java里面判断了如果没有设置自定义ssl,那么就直接用操作系统自带的进行返回,从而来访问https。
  我们这里为了演示下具体代码,所以自定义一个ssl的构造出来,操作如下:
把服务端server-cert.cer证书放到 assets 目录下,然后创建一个 SSLHelper.java 的辅助类:

public static SSLSocketFactory getSSLSingleFactory(Context context) {
    try {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null);
        InputStream is = context.getAssets().open("server-cert.cer");
        keyStore.setCertificateEntry("0", certificateFactory.generateCertificate(is));
        if(is != null) {
            is.close();
        }
        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 (CertificateException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (KeyManagementException e) {
        e.printStackTrace();
    }
    return null;
}

对应的Activity请求里的代码是:

RequestParams params = new RequestParams("https://ifish.site");
params.setSslSocketFactory(SSLHelper.getSSLSingleFactory(this));
// 因为是自签不受权威机构认证,所以绕过不检查域名ssl。
params.setHostnameVerifier(new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
});

x.http().get(params, new Callback.CommonCallback() {
    @Override
    public void onSuccess(String result) {
        Log.d(TAG, "onSuccess...result = " + result);
    }
    @Override
    public void onError(Throwable ex, boolean isOnCallback) {
        Log.d(TAG, "onError...ex = " + ex.getMessage());
    }
    @Override
    public void onCancelled(CancelledException cex) { }
    @Override
    public void onFinished() { }
});

双向认证的写法:
双向认证要求的是客户端也需要持有一份自己的私钥key、服务端要有一份客户端的公钥证书。但是由于Android系统限制,我们需要把client.p12转成client.bks格式,才能被访问到。介绍一个转化工具,叫做“Portecle”,亲测可用的下载和使用链接如下:https://blog.csdn.net/zhangyong125/article/details/50402183,也可以自己去官网免费下载,如果是Linux系统, 可以用 java -jar protecle.jar 来运行,然后按照上文链接的使用方法,把对应的 client.p12转成client.bks格式。
对应的 SSLHelper.java添加一个双向认证方法:

public static SSLSocketFactory getSSLDoubleFactory(Context context) {
    try {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null);
        InputStream is = context.getAssets().open("server-cert.cer");
        keyStore.setCertificateEntry("0", certificateFactory.generateCertificate(is));
        if(is != null) {
            is.close();
        }
        SSLContext sslContext = SSLContext.getInstance("TLS");
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        // 初始化双向客户端keyStore
        KeyStore clientKeyStore = KeyStore.getInstance("BKS");
        clientKeyStore.load(context.getAssets().open("client.bks"), "123456".toCharArray());
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(clientKeyStore, "123456".toCharArray());
        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
        return sslContext.getSocketFactory();
    } catch (CertificateException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (UnrecoverableKeyException e) {
        e.printStackTrace();
    } catch (KeyManagementException e) {
        e.printStackTrace();
    }
    return null;
}

然后Activity里面,params.setSslSocketFactory(SSLHelper.getSSLSingleFactory(this)); 换成 params.setSslSocketFactory(SSLHelper.getSSLDoubleFactory(this)); 即可。

六、总结

疑难点:
1.涉及的知识点比较多,很多不是Android本身的东西;
2.https单向/双向原理不太好理解,可以看该文https://juejin.im/post/5c9cbf1df265da60f6731f0a;
3.仅验证单双向认证来说,没必要生成根CA证书,部分文章生成太多证书会导致配置上容易乱。

调试验证技巧:
  原Android网络请求框架庞大,对应的域名是生产环境的域名,绝对不能随便动后台的生产环境配置。因此需要自己的一台服务器,自己搭建一个简单的后台Demo https请求,以及Android的Demo网络请求app,从而方便Debug。

参考:
https://www.zhihu.com/question/29620953 – SSL中,公钥、私钥、证书的后缀名都是些啥?
https://juejin.im/post/5c9cbf1df265da60f6731f0a – 扯一扯HTTPS单向认证、双向认证、抓包原理、反抓包策略
https://zhuanlan.zhihu.com/p/60392573 – 为了抓包某app,我折腾了10天,原来他是用SSL Pinning防抓包的
https://www.cnblogs.com/yelao/p/9486882.html – Nginx https 双向认证
https://blog.csdn.net/jsc702325/article/details/76724010 – Android 用自签名证书实现https请求
https://www.cnblogs.com/guogangj/p/4118605.html – 那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)
https://blog.csdn.net/zhangyong125/article/details/50402183 – P12证书转BKS证书

你可能感兴趣的:(Android模块,网络)