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.jks 和 D:\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 改成你的服务器地址。
参考文章:
Android Https相关完全解析 当OkHttp遇到Https
Creating self-signed certificates for use on Android
Demo下载:
原创随笔,转载注明出处。
http://www.cnblogs.com/wlfcolin/p/5198835.html