Tomcat双向Https验证搭建,亲自实现与主流浏览器、Android/iOS移动客户端超安全通信(转)

紧接着《Tomcat单向Https验证搭建,亲自实现与主流浏览器、Android/iOS移动客户端安全通信》,此处演示下更安全的双向Https认证的通信机制,为了清晰明了,以下进行单独描述,你不需要去看《Tomcat单向Https验证搭建,亲自实现与主流浏览器、Android/iOS移动客户端安全通信》一样可以完全理解。

 

众所周知,iOS9已经开始在联网方面默认强制使用Https替换原来的Http请求了,虽然Http和Https各有各的优势,但是总得来说,到了现在这个安全的信息时代,开发者已经离不开Https了。

网上有很多搭建Https的教程,但是比较零散,Web浏览器端和移动端具体部署也不是特别明确,如果真的用于项目中,还需要折腾一番,本人直接来个项目级别的Demo。

 

在开始之前,我总结一下keytool这个证书工具需要处理的几种常见后缀格式的意义:

jsk/keystore, 表示一个密钥库,里面可以包含多个密钥条目(证书),密钥条目(证书)还可以分私有的和信任的等,私有的一般包括私钥、公钥和密钥条目信息,信任的一般包 括公钥和密钥条目信息(公钥证书)。打开密钥库需要一个密码,同时打开每个私有密钥条目也需要一个密码(但一般建议将打开私有密钥条目的密码设置跟打开密钥库密码相同,省的弄乱了,以下我的Demo演示是设置相同的),做过给安卓apk签名打包的一定能体会到这个。

csr/certreq,证书请求文件,你把这个提交给CA,CA会给你颁发cer格式的含有公钥和密钥条目信息的证书(公钥证书)给你。

cer, 用于存储某个密钥条目(证书)的公钥文件,一般你提交了csr/certreq给CA后,CA会颁发给你,你也可以通过自签名的CA颁发,如果你已经有密 钥条目(证书)在密钥库里,也可以从jsk/keystore中的某个密钥条目(证书)导出其公钥和密钥条目内容的证书(公钥证书)。

p12,表示一个密钥库,跟jsk/keystore类似,但一般用于客户端,表示客户端密钥库,比如IE/火狐浏览器可以直接导入。另外不同于jsk/keystore的是,一般密钥条目(证书)的打开密码跟密钥库打开密码一样。

bks,表示一个密钥库,跟p12类似,一般用于Android客户端,下面的Demo示例在Android客户端则需要用到,可以直接从p12格式转换而来。

综上,其实最简单的理解就是密钥库就相当于SQL数据库,各种密钥条目(证书)就相当于SQL数据库表 ,一个SQL数据库表其实跟其它的表又有父子(外键)关系的,这种关系叫做密钥条目(证书)的密钥链。为了描述更加方便,以下将“密钥库”描述词叫做《证书库》,“密钥条目”描述词叫做《证书》,将“cer格式的公钥和密钥条目内容的证书”叫做《公钥证书》。

 

接下来开始演示Demo示例:

1、生成服务器端证书库和证书:(生成服务器端证书库和证书可以有多种方式,推荐通过走第三方CA方式,这样生成的证书以后更具有保障性和安全性(尤其是对Web客户端,可以启动“绿色地址栏/安全锁 地址栏显示单位名称 EV国际认证标识”等等))

1-1-1、方式一、使用keytool,生成自签名的CA证书和自签名的server证书(下面生成的CA是自签名的,当然下面生成的server也是自签名的,这些证书在浏览器上使用绝对不会出现绿条):

复制代码
1.生成自签名CA:keytool -genkey -v -alias ca -keyalg RSA -keystore D:\ca_cert_lib.jks -validity 3650
2.生成服务器证书:keytool -genkey -v -alias server -keyalg RSA -keystore D:\server_cert_lib.jks -validity 365

注意证书名叫ca定义为自签名的CA证书,证书名叫server定义为服务器证书,它们分别保存在证书库路径为 D:\ca_cert_lib.jksD:\server_cert_lib.jks 中
之所以要分自签名的CA证书server服务器证书,是因为正常情况下我们的server服务器证书是需要向第三方CA申请的,第三方CA会用它的根证书给你生成一份公钥证书(这个过程叫做第三方CA给你签名),而此处就是要自导自演展示自签名的CA给server证书签名这个过程
复制代码

1-1-2、用自签名的CA给server签上CA的签名(server本身也是自签名的,下面要做的相当于将server的自签名换成CA的签名,也许你会问CA的签名是谁的,CA也可以是别人的,比如如果沃通愿意给你的CA签名的话,那么CA的颁发者就是沃通,我这里的Demo演示没有权威机构给它签名,所以我这个CA就是自己给自己签名的,这个CA其实就是ROOT证书,只不过不会被任何客户端信任(如:浏览器等)而已,即用我这个CA签发的所有server服务器证书在任何浏览器上绝对不会出现绿条):

在给server签名之前,查看一下当前证书库情况,它们的确都是各自给自己签名的:
keytool -list -v -keystore D:\ca_cert_lib.jks
keytool -list -v -keystore D:\server_cert_lib.jks

复制代码
现在使用自签名CA给server签名(如果你要沃通CA给你server签名,就把下面的csr交给沃通):
1.生成server的证书请求文件:keytool -certreq -alias server -keystore D:\server_cert_lib.jks > D:\server.csr (linux上:keytool -certreq -alias server -keystore <路径>/server_cert_lib.jks | tee <路径>/server.csr)
2.使用自签名的CA对server的证书请求文件进行签名颁发服务器server.cer公钥证书:keytool -gencert -alias ca -keystore D:\ca_cert_lib.jks -infile D:\server.csr -outfile D:\server.cer
3.生成自签名CA的公钥文件:keytool -export -alias ca -keystore D:\ca_cert_lib.jks -rfc -file D:\ca.cer

此时可以先查看以下ca.cer和server.cer公钥证书具体内容(注意ca.cer是自签名CA的公钥文件,其颁发者还是它自己,而server.cer是server服务器的公钥文件,其颁发者是自签名的CA,两者是有本质区别的,下面安装回复后可以看到这个区别),不过其实他们都是个Base64过的字符串:
keytool -printcert -rfc -file D:\ca.cer
keytool -printcert -rfc -file D:\server.cer
复制代码

复制代码
安装证书回复(回复这个翻译也许不太好,反正这个意思就是:将CA颁发的cer公钥证书安装到server服务器端证书库,前提条件是CA的cer公钥证书也需要先被安装):
1.先安装CA的公钥证书(这步不可以少,否则下面的证书回复没法安装):keytool -importcert -alias ca -keystore D:\server_cert_lib.jks -file D:\ca.cer
2.安装server的公钥证书(安装证书回复(被CA签名过的)):keytool -importcert -alias server -keystore D:\server_cert_lib.jks -file D:\server.cer

此时再查看下服务器server证书:keytool -list -v -keystore D:\server_cert_lib.jks -alias server

这时发现这个server证书变化挺大的,一是证书连变长了,变成2了,这个server证书附带了上一级证书SELF CA ROOT CERT的信息,其次是server的发布者变成了SELF CA ROOT CERT,这也就是说明成功的使用自签名的CA给server签名成功了
复制代码

 

1-2-1、方式二、通过权威CA(第三方SSL证书机构)生成,如通过沃通生成免费/收费服务器端证书库和证书,CA生成的证书更具有保障性,最直观的表现是客户端用Web浏览器访问该Https网站时会有绿色标识(当然要显示越华丽就得给权威CA交更多的钱)如github:,以下演示使用沃通申请免费的DV SSL证书。

1-2-1、登陆沃通,申请一个免费的DV SSL证书。

1-2-2、申请需要先绑定域名

1-2-3、申请完后需要验证域名,验证域名这个事就自己去搞定吧

1-2-4、上面用自签名的CA给server证书签名已经提到了如何生成csr文件,此处通过提交证书申请文件csr申请的步骤略。以下演示在线生成,即本次讲的通过沃通CA自动生成公钥证书,顺便把server服务器证书库也一并生成好并将公钥证书导入到这个证书库,此处输入的密码实际上既是server服务器证书库密码也是server服务器证书(此种方式生成的证书名字叫做1,这个1对应上面自签名CA导入的server证书)的密码

输入密码生成证书之后就可以下载沃通CA颁布给你server服务器端用的证书库和证书了,然后部署到对应的服务器程序中,本案例部署到tomcat,为了保持统一性和直观性此处将沃通CA颁发的证书库名andy5.me.jks改名为server_cert_lib.jks

  

一般通过此时生成的证书名字(alias)叫做:1,对应自签名CA方式中的server证书

因此你拿到了上面的沃通颁发的证书后,你还可以继续颁发给别人,这些你颁发的证书都是可信任的,因为沃通上面的根证书一定是可信任的,不然沃通本身就是不可以信任的。

 

2、由于是双向认证,所以同理,需要生成客户端证书库和证书:(同样使用keytool生成)

keytool -genkey -v -alias client -keyalg RSA -storetype PKCS12 -keystore D:\client_cert_lib.p12 -validity 90

注意证书名叫做client,保存的证书库路径为 D:\client_cert_lib.p12


3、客户端和服务器端证书库和证书都生成好了后,将客户端证书导入到服务器端证书库中:(这个过程叫做让服务器端证书库信任指定的客户端证书,信任只需要信任其公钥证书就行了,即cer格式)

3-1、先从p12格式的客户端证书库导出cer格式的client证书公钥

keytool -export -alias client -keystore D:\client_cert_lib.p12 -storetype PKCS12 -rfc -file D:\client.cer

3-2、将cer格式的client证书公钥导入到服务器端证书库里

keytool -import -alias client -v -file D:\client.cer -keystore D:\server_cert_lib.jks

3-3、导入后检查一下服务端证书库当前包含的证书情况,看看有没有将客户端证书导入成功

此时客户端证书名在服务器证书库里叫client,使用命令查看一下,此时发现有client证书,不过类型是trustedCertEntry,即简单理解就是通过证书公钥导入的证书,而ca和server证书的类型都是PrivateKeyEntry
keytool -list -v -keystore D:\server_cert_lib.jks

到了此步,实际上ca没什么用了,理论上可以从服务器端证书库移除掉(沃通颁发的就只有名字叫1这个server证书)

3-4、在tomcat的安装目录/conf/server.xml中配置和启用以下port为8443的Connector(可以理解为给服务器端安装服务器端证书库需要信任的客户端证书

复制代码
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="D:\\server_cert_lib.jks" keystorePass="s123456"
truststoreFile="D:\\server_cert_lib.jks" truststorePass="s123456"
/>
复制代码

 

4、同样的,做双向认证还需要同时将服务器端证书导入到客户端证书库里:(这个过程叫做让客户端证书库信任指定的服务器端证书,需要server的公钥证书即可)

4-1、先从jks格式的服务器端证书库导出cer格式的服务器端公钥证书

keytool -keystore D:\server_cert_lib.jks -export -alias server -file D:\server.cer

4-2、将cer格式的服务器端证书导入到客户端证书库(这个实现不能用keytool导入了,而是要根据具体的各个客户端平台进行实现,此步骤可以理解为给客户端安装客户端证书库需要信任的服务器端server公钥证书

4-2-1、Web浏览器实现:使用浏览器安装客户端证书库和信任的服务器端server证书

4-2-1-1、IE浏览器

1、安装客户端证书库:Windows下直接双击client_cert_lib.p12,输入客户端证书库密码后,一直下一步安装客户端证书库

2、安装服务器端公钥证书:Windows下直接双击server.cer,然后在证书存储——将所有证书放入下列存储,选择受信任的根证书颁发机构

4-2-1-2、火狐浏览器

1、导入client_cert_lib.p12

2、导入server.cer,火狐没办法直接将来自自签名CA颁发的server.cer公钥证书导入到证书机构中,因为火狐会检查你的服务器公钥证书 server.cer的最高一级颁发者是不是权威机构(权威第三方CA),不是的话,不会通过的,比如你在火狐选项——高级——证书——查看证书——服务 器/证书机构:导入server.cer,会提示无法导入,因此解决方法是不用导入server.cer,而是在服务器选项卡中添加列外地址(显然并不安全),如果来自 权威CA颁发的server.cer,则可以直接导入。

 

4-2-2、Android客户端实现

4-2-2-1、先将p12格式的客户端证书库转成bks格式的证书库,因为Android上只能用bks的证书库(bcprov-jdk16-1.45.jar在这里下载,或后面的Demo也带有)

keytool -importkeystore -srckeystore D:\client_cert_lib.p12 -srcstoretype pkcs12 -destkeystore D:\client_cert_lib.bks -deststoretype bks -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath D:\bcprov-jdk16-1.45.jar

4-2-2-2、将服务器端公钥证书server.cer客户端证书库client_cert_lib.bks放入安卓assert目录,然后使用HttpsUtil.getSslSocketFactory进行初始化(该工具类的方法具体实现见后面的Demo代码)

复制代码
private void initHttpsEngine(boolean isSelfCa) {
        try {
            // 初始化服务器端公钥证书,得到SSLSocketFactory
            SSLSocketFactory sslSocketFactory = HttpsUtil.getSslSocketFactory(new InputStream[]{getAssets().open
                    ("server.cer")}, getAssets().open("client_cert_lib.bks"), "c123456");
            OkHttpClient.Builder builder = new OkHttpClient.Builder();

            if (isSelfCa) {
                /**
                 * 注意;如果你的server.cer是来自自签名CA颁发的,那么就要设置下面的customVerifier,主要是为了解决报以下异常,
                 * 即跳过Hostname www.andy5.me在CA上的验证,如果你的server.cer是来自第三方SSL权威机构颁发的,不用设置这个customVerifier
                 *
                 * javax.net.ssl.SSLPeerUnverifiedException: Hostname www.andy5.me not verified:
                 * certificate: sha1/EnrjjhNxjvuDkO/rJqPmJ9XaIMs=
                 * DN: CN=Andy Wu(www.andy5.me),OU=Andy5 Server,O=www.andy5.me,L=Guangzhou,ST=Guangdong,C=CN
                 * subjectAltNames: []
                 */
                HostnameVerifier customVerifier = new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        // 指定SERVER_URL一定可以通过
                        if (SERVER_URL.equalsIgnoreCase(hostname)) {
                            return true;
                        } else {
                            // 使用默认的OkHostnameVerifier进行验证
                            return OkHostnameVerifier.INSTANCE.verify(hostname, session);
                        }
                    }
                };
                mOkHttpClient = builder.sslSocketFactory(sslSocketFactory).connectTimeout(30, TimeUnit.SECONDS)
                        .hostnameVerifier(customVerifier).build();
            } else {
                mOkHttpClient = builder.sslSocketFactory(sslSocketFactory).connectTimeout(30, TimeUnit.SECONDS).build();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
复制代码

4-2-2-3、可以在任何地方对该服务器发起Https请求了,如果是自签名CA签发的服务器端server证书,需要忽略域名验证才能正常通信(具体看Demo代码),显然也是不安全的。

复制代码
    public void testHttps(View v) {

        if (mOkHttpClient == null) {
            Toast.makeText(getApplicationContext(), "请先初始化客户端密钥库和服务器端公钥!", Toast.LENGTH_SHORT).show();
            return;
        }

        mTvResult.setText("正在从 " + HTTPS_SERVER_URL + " 获取数据....");
        mWvResult.loadData("", "text/html", "UTF-8");

        Request request = new Request.Builder().url(HTTPS_SERVER_URL).build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, final IOException e) {
                e.printStackTrace();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mTvResult.setText("从 " + HTTPS_SERVER_URL + " 获取数据失败!\n" + e);
                    }
                });
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String html = response.body().string();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mTvResult.setText("从 " + HTTPS_SERVER_URL + " 获取数据成功!");
                        mWvResult.loadData(html, "text/html", "UTF-8");
                    }
                });
            }
        });
    }
复制代码

4-2-2-4、成功请求界面如下,软件环境:MIUI 6(Android 4.4.2) + AS 1.5.1

4-2-3、iOS客户端实现

4-2-3-1、将服务器端公钥证书server.cer和客户端证书库client_cert_lib.p12放入根目录,然后使用HttpsUtil.configHTTPSessionManager配置AFNetworking安全选项(该工具类的方法具体实现见后面的Demo代码)

复制代码
- (IBAction)testHttps:(UIButton *)sender {
    
    AFHTTPSessionManager *manager =[AFHTTPSessionManager manager];
    
    NSArray *serverCersNames = [[NSArray alloc] initWithObjects:@"server.cer", nil];
    [HttpsUtil configHTTPSessionManager:manager serverCers:serverCersNames clientP12:@"client_cert_lib.p12" clientP12Password:@"c123456" isSelfCa:true];// 使用自签名CA给服务器server证书签名的isSelfCa为true,第三方权威CA签名的isSelfCa为false,当设置isSelfCa为false时,需要注释掉Info.plist中整个NSAppTransportSecurity节点的配置
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    manager.requestSerializer.timeoutInterval = 30.0f;
    
    [_tvResult setText:[NSString stringWithFormat:@"正在从%@获取数据....",HTTPS_SERVER_URL]];
    [_wvResult loadHTMLString:@"" baseURL:nil];
    
    [manager GET:HTTPS_SERVER_URL parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
        //
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSString *result = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding];
        NSLog(@"获取数据成功\n%@",result);
        [_tvResult setText:[NSString stringWithFormat:@"从%@获取数据成功!",HTTPS_SERVER_URL]];
        [_wvResult loadHTMLString:result baseURL:nil];
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"获取数据失败\n%@",error);
        [_tvResult setText:[NSString stringWithFormat:@"从%@获取数据失败!\n%@",HTTPS_SERVER_URL,error]];
    }];
}
复制代码

4-2-3-2、成功请求界面如下,软件环境:iOS 9.2.1 + Xcode 7.2.1

 

5、此时,客户端(Web浏览器、Android、iOS)就可以向服务器端发起Https双向认证请求了,详细效果见Demo(为了兼容演示自签名CA,Demo使用的是自签名服务器证书)。

总结来说,真正最后需要使用的文件就是3个:server_cert_lib.jks、server.cer、client_cert_lib.p12/client_cert_lib.bks,其中server_cert_lib.jks放到tomcat,server.cer、client_cert_lib.p12/client_cert_lib.bks放到客户端(Web浏览器、Android、iOS)。

注意:在使用自签名CA签名的server.cer的情况下,无论是主流浏览器,还是Android和iOS移动客户端,按照正常的Https安全认证流程,都会收到证书错误提示或者错误(不过可以通过忽略一些验证达到能正常通信的效果,但这并不是安全的,其它具体参考Demo代码,以下截图来自Android中使用OkHttp时忽略hostname验证达到能正常通信的效果和iOS中AFNetworking通过设置类似允许不信任证书和忽略域名验证来实现),故自签名CA只适用于测试用,实际开发产品对外发布,你务必得把你的服务器server证书交给第三方CA(权威SSL证书机构)进行签名,否则,正常情况 下在移动客户端正常安全验证是不会通过的(使用一些忽略验证手段通过的,那就意味着失去了安全性,如果这样Https本身相比于Http就没有任何优势了)。

 

6、最后补充说明:

关于自己内网测试,我所有使用的www.andy5.me这个域名是可以改成IP的,一样可以测试成功的,当然我测试的时候是用nat123做了映射,主要是为了更方便和更接近真实环境,如果是你自己测试,务必将客户端 SERVER_URL 和 HTTPS_SERVER_URL 改成你的服务器地址。

 

参考文章:

用Tomcat服务器配置https双向认证过程实战

Android Https相关完全解析 当OkHttp遇到Https

Creating self-signed certificates for use on Android

JDK中的证书生成和管理工具keytool

HTTPS双向认证的原理与实现 (Nginx + iOS)

 

Demo下载:

双向Https验证Demo相关文件.7z

 

原创随笔,转载注明出处。

http://www.cnblogs.com/wlfcolin/p/5198835.html

 

posted @ 2016-03-01 19:53  沧海一滴  阅读(222)  评论(0编辑  收藏  举报