Android 应用实现 Https 双向认证

为什么需要双向认证

Https保证的是信道的安全,即客户端和服务端通信报文的安全。但是无法保证中间人攻击,所以双向认证解决的问题就是防止中间人攻击。

中间人攻击(Man-in-the-MiddleAttack)简称(MITM),是一种“间接”的入侵攻击,这种攻击模式是通过各种技术手段将受入侵者控制的一台计算机虚拟放置在网络连接中的两台通信计算机之间,这台计算机就称为“中间人”。若没有开启双向认证,中间人可以拦截客户端发送的请求,然后篡改信息再发送到服务端;中间人也可以拦截服务端返回的信息,再发送到客户端。所以使用Https的单向认证或双向认证能够有效防止中间人攻击。

注:无论Ca证书还是自签证书都需要双向认证。

双向认证原理

1、服务端认证客户端原理

客户端有自己的bks证书auth_client.bks,并将导出的auth_client_pub.cer证书导入到服务端证书auth_server.keystore中,这样服务端就将客户端证书添加到信任列表中,从而能够让带有该auth_client_pub.cer证书信息的客户端访问服务。

2、客户端认证服务端原理

服务端有自己的证书(ca颁发的或者是自己创建的)auth_server.keystore,并导出auth_server_pub.cer证书,将该证书导入到客户端证书

auth_truststore.jks中,注意:这里不是导入到auth_client.jks中,而是导入生成另一个证书auth_truststore.jks,最后再将jks证书转化成bks证书。

实现过程

一、服务端证书

创建服务端证书

keytool -genkeypair -alias auth_server -keyalg RSA -validity 36500 -keypass auth_server -storepass auth_server -keystore /Users/renzhongrui/android/certs/auth_server.keystore

导出服务端证书公钥

keytool -export -alias auth_server -file /Users/renzhongrui/android/certs/auth_server_pub.cer -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server

二、客户端证书

创建客户端证书(andoird不能用keystore格式的密钥库,所以先生成jks格式,再用Portecle工具转成bks格式)

keytool -genkeypair -alias auth_client -keyalg RSA -validity 36500 -keypass auth_client -storepass auth_client -keystore /Users/renzhongrui/android/certs/auth_client.jks

导出客户端证书公钥

keytool -export -alias auth_client -file /Users/renzhongrui/android/certs/auth_client_pub.cer -keystore /Users/renzhongrui/android/certs/auth_client.jks -storepass auth_client 

三、证书交换

将客户端证书导入服务端keystore中,再将服务端证书导入客户端auth_truststore中, 一个keystore可以导入多个证书,生成证书列表。

将客户端公钥导入到服务端keystore证书中,使得服务端能够信任客户端。

keytool -import -v -alias auth_client -file /Users/renzhongrui/android/certs/auth_client_pub.cer -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server

生成客户端信任证书库auth_truststore.jks,即将服务端公钥导入到客户端jks证书中,使得客户端能够信任服务端。

keytool -import -v -alias auth_server -file /Users/renzhongrui/android/certs/auth_server_pub.cer -keystore /Users/renzhongrui/android/certs/auth_truststore.jks -storepass auth_truststore

最后验证一下,查看证书库中的所有证书

keytool -list -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server

keytool -list -keystore /Users/renzhongrui/android/certs/auth_truststore.jks -storepass auth_truststore

四、证书转换

下载portecle.jar(https://sourceforge.net/projects/portecle/),解压后运行jar包:
java -jar portecle.jar

1、点击File菜单选择Open Keystore File,选择创建好的auth_client.jks或auth_truststore.jks证书,输入密码。

2、选中导入的证书,点击Tools菜单,选择Change Keystore Type,再选择BKS类型,再次输入密码,确认之后,会显示成功。

3、最后点击File菜单,选择Save Keystore File As,将证书保存的指定路径。

五、配置服务

证书准备好之后,就可以进行集成测试了,服务使用Spring Boot创建或者使用Nginx代理。

使用Spring Boot服务

1、添加配置

server:
  port: 443
  server:
    tomcat:
      uri-encoding: UTF-8
开启https,配置跟证书对应
  ssl:
    key-store: classpath:auth_server.keystore
    key-store-type: JKS
    key-store-password: auth_server
    key-password: auth_server
    key-alias: auth_server
    enabled: true
    #是否需要进行认证
    client-auth: need
    protocol: TLS # 默认
    trust-store: classpath:auth_server.keystore
    trust-store-password: auth_server
    trust-store-type: JKS

2、添加代码,这里配置80端口重定向到443,也可以改成别的端口。

public class PackApplication implements WebMvcConfigurer {


    public static void main(String[] args) {
        SpringApplication.run(PackApplication.class, args);
    }


    @Bean
    public Connector connector(){
        Connector connector=new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(80);
        connector.setSecure(false);
        connector.setRedirectPort(443);
        return connector;
    }


    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory(Connector connector){
        TomcatServletWebServerFactory tomcat=new TomcatServletWebServerFactory(){
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint=new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection=new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(connector);
        return tomcat;
    }
}

使用Nginx服务配置

Nginx配置与Spring Boot服务配置略有不同。

server {
        listen       443;
        server_name  192.168.200.101; # 代理服务IP
        ssl on; # 开启Https


        ssl_certificate      /usr/local/nginx/conf/https/auth_server.cer; # auth_server.keystore导出的cer证书
        ssl_certificate_key  /usr/local/nginx/conf/https/auth_server.key; # auth_server.keystore导出的私钥
        ssl_client_certificate /usr/local/nginx/conf/https/auth_client.cer; # auth_client.keystore导出的cer

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
        ssl_verify_client optional; # 配置校验客户端策略,设置成optional时候可以开启白名单接口


        ssl_protocols TLSv1.1 TLSv1.2;


        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  off;


        location / { # 需要双向验证https的接口
            if ($ssl_client_verify != SUCCESS) {
                 return 401;
            }
            proxy_pass http://192.168.200.101:8008;
            proxy_connect_timeout 600;
            proxy_read_timeout 600;
        }


        location /aarm/downloadUpdateFile { # 获取证书版本和下载证书接口,不需要验证Https
            proxy_pass http://192.169.200.101:8008;
            proxy_connect_timeout 600;
            proxy_read_timeout 600;
        }
    }

六、配置客户端

在客户端app中使用OkHttp来进行网络访问,所以需要配置OkHttp来进行证书认证。

1、将上面创建的auth_client.bks和auth_truststore.bks证书放到assets目录下

2、初始化OkHttp

OkHttpClient okHttpClient = new OkHttpClient.Builder()
  .connectTimeout(10, TimeUnit.SECONDS)
  .readTimeout(10, TimeUnit.SECONDS)
  .sslSocketFactory(Https.getSSLCertifcation(getApplicationContext()))//获取SSLSocketFactory
  .hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName验证器
  .build();

重点需要看一下Https类的实现:

public class Https {
    private final static String CLIENT_PRI_KEY = "auth_client.bks";
    private final static String TRUSTSTORE_PUB_KEY = "auth_truststore.bks";
    private final static String CLIENT_BKS_PASSWORD = "auth_client";
    private final static String TRUSTSTORE_BKS_PASSWORD = "auth_truststore";
    private final static String KEYSTORE_TYPE = "BKS";
    private final static String PROTOCOL_TYPE = "TLS";
    private final static String CERTIFICATE_FORMAT = "X509";

    public static SSLSocketFactory getSSLCertifcation(Context context) {
        SSLSocketFactory sslSocketFactory = null;
        try {
            // 服务器端需要验证的客户端证书,其实就是客户端的keystore
            KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);// 客户端信任的服务器端证书
            KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE);//读取证书
            InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);
            InputStream tsIn = context.getAssets().open(TRUSTSTORE_PUB_KEY);//加载证书
            keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());
            trustStore.load(tsIn, TRUSTSTORE_BKS_PASSWORD.toCharArray());
            ksIn.close();
            tsIn.close();
            //初始化SSLContext
            SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE);
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CERTIFICATE_FORMAT);
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CERTIFICATE_FORMAT);
            trustManagerFactory.init(trustStore);
            keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
            sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
            sslSocketFactory = sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sslSocketFactory;
    }
}

还有一个UnSafeHostnameVerifier类

private class UnSafeHostnameVerifier implements HostnameVerifier {
  @Override
  public boolean verify(String hostname, SSLSession session) {
    return true;
  }
}

此时再进行网络请求,就能够访问到带有双向认证的服务端接口了。当然一般网上的博客到这就结束了,但是这样真的就完事了吗,其实真正的设计才刚开始,如果只是了解原理,读到这里就可以了,下面才是真实应用场景。

真实场景实现

原理还是那个原理,就看怎么合理的使用了。在真实开发环境中,需要解决几个问题:

auth_client.bks和auth_truststore.bks是需要动态下发的
不是所有的接口都需要进行双向认证

动态下发auth_client.bks和auth_truststore.bks
1、auth_client.bks和auth_truststore.bks的制作需要在本地工具完成,然后通过管理端上传到服务器,并且改变证书的版本号;

2、客户端需要访问证书版本,来判断是否需要更新证书,如果需要更新则下载证书。

这里会引出两个问题:

1、请求版本号的接口和下载证书的接口不能进行双向认证,否则无法下发证书。

2、不进行双向认证的接口是不安全的,所以,请求版本号的接口的返回值是需要加密的;

针对第一个问题处理方式

服务端需要配置白名单,将请求版本号的接口和下载证书的接口过滤掉;

客户端OkHttp首次初始化不能进行双向认证,等下载完证书之后,需要再次进行OkHttp初始化;

针对第二个问题处理方式

需要本地工具创建RSA公私钥对,用于请求版本号接口的加解密;

服务端使用私钥对报文加密,客户端保存公钥,并使用公钥对报文解密。

客户端使用公钥解密后的报文格式:

{
    "version":1,
    "authType":2,
    "clientBksPath":"https://localhost/downloadUpdateFile?fileName=auth_client.bks",
    "trustBksPath":"https://localhost/downloadUpdateFile?fileName=auth_truststore.bks",
    "authKey":"auth_client"
}

version: 表示每一次更换证书的版本;
authType:0 表示不开启认证,1 表示开启单向认证,2 表示开启双向认证
clientBksPath:auth_client.bks下载路径
trustBksPath:auth_truststore.bks下载路径
authKey:auth_client.bks证书密码
客户端每次启动都要获取服务端证书版本,并将证书信息存储到本地文件或者数据库中,通过对比服务端证书版本和数据库中版本来判断是否需要证书更新。

注:这样设计的好处是当证书过期时,能够动态下发证书,但会引出一个问题,客户端要安全的存储公钥信息,一般做法是将公钥存储到so文件里,再配合应用加固手段进行保护,不过这个就不是通信安全的问题了,而是apk安全的问题。

其他证书操作
1、查看keystore证书公钥

keytool -list -rfc --keystore release.keystore | openssl x509 -inform pem -pubkey

2、查看keystore证书私钥

先转成pfx格式

keytool -v -importkeystore -srckeystore release.keystore -srcstoretype jks -srcstorepass 123456 -destkeystore keystore/release.pfx -deststoretype pkcs12 -deststorepass 123456 -destkeypass 123456

再查看证书私钥

openssl pkcs12 -in release.pfx -nocerts -nodes

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容