一.背景
前两篇对Http和Https的原理进行了介绍,接下来是在Android代码中如何具体配置;同时再强调说明一下如果使用权威ca机构申请(购买)的证书客户端也要进行验证。
二.涉及的点
2.2 使用权威CA机构实现https的时候,只有服务端的秘钥证书,而app端没有公钥证书(Android系统会检验服务端合法性),但是还是要“手动”验证服务端合法性(公司请第三方检测机构检测后要求必须有手动校验过程),此时如何处理。
2.3 使用自签名证书的时候如何校验服务端合法性
2.4 各种网络请求框架中如何配置(Httpclient,HttpsUrlConnection,Okhttp,Glide,WebView)
3.1首先,生成服务端证书存储库文件(证书库,ycb_server.jks)
keytool
-genkey
-alias ycb_server 别名
-keypass 123456 密码
-keyalg RSA 加密方式
-keysize 1024 加密密钥长度(可以不加这一行,默认2048)
-validity 3650 证书有效期(单位:天)
-keystore E:/yuncaibianhttps/ycb_server.jks 证书存放的具体路径
-storepass 123456 查看jks信息的密码
按照提示的各种问题填写信息,点击回车后无提示,自行查看相应的文件夹中有无生成(这里注意提前新建要保存的目录文件夹如我的是E:/yuncaibianhttps,不然会报io异常);具体如下图
3.2其次,从服务端证书存储库文件中导出客户端需要的公钥证书。
如下图所示:
通过上面的操作就产生了我们需要的服务端配置需要的ycb_server.jks,和客户端需要的ycb_server.cer。如下图所示:
3.3再有,从cer文件中导出公钥字符串(备用)
keytool -printcert -rfc -file E:/yuncaibianhttps/ycb_server.cer
我的生成如下:
----- -----
++
+
+
++
+++
----- -----
在代码中使用时候稍作处理如下:
private String PUB_KEY = "-----BEGIN CERTIFICATE-----\n" +
"MIICUjCCAbugAwIBAgIEBt35sTANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJjbjEQMA4GA1UE\n" +
"CBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEMMAoGA1UEChMDY25yMQwwCgYDVQQLEwNjbnIx\n" +
"DTALBgNVBAMTBGdvbmcwHhcNMTgwMzA2MDcwMTQzWhcNMjgwMzAzMDcwMTQzWjBcMQswCQYDVQQG\n" +
"EwJjbjEQMA4GA1UECBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEMMAoGA1UEChMDY25yMQww\n" +
"CgYDVQQLEwNjbnIxDTALBgNVBAMTBGdvbmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAI31\n" +
"yMvNkmdFLyDr4uI35lMZwZf9lqt/q0MHpIaMP7j4evtVMqs9SnAJwEprOK35+z5xIgqpD+ioy5Xf\n" +
"e1g2crM0r22qU229WOT4OfSk12bcobWw9Dr7Hqy5hjdXDtJ7hwg+c4mYE4WWZOH6REkR58c0LCoe\n" +
"4Y1g0iZOpXwiTjF7AgMBAAGjITAfMB0GA1UdDgQWBBTDpVe72BYQOuvexvW+WpDt5XrpPDANBgkq\n" +
"hkiG9w0BAQsFAAOBgQB0KhMRb7LhFB/8jWRa9owvSF9rEPmx8BzcsrMDQQEU+XV1dBhrr+YgADcl\n" +
"wgEHZjCHair5rTId888bdSN+OXYIMat5jRyH2MW+5ybRQYCCijQ6jjyK1TV+/hDNZSYtEk9RW9vC\n" +
"I7WMhdclqcbnzT6COg1cJHgrMhLlMz8bsoDZWQ==\n" +
"-----END CERTIFICATE-----";
如下图:
关于怎么配置服务端,这里不做赘述网上很多了自行百度。
四.代码中配置https
4.1使用CA机构证书的时候(Httpclient中)
下面是云采编app实际生产中,由于只有服务端有秘钥证书,客户端没有,如果想要手动校验那么只能采用下面的做法:自己获取服务端的公钥字符串,然后保存,然后供后面验证用(依据:我们测试用的服务器肯定是我们自己的服务器,公钥字符串肯定是没问题的)。
4.1.1 如下代码中打印出服务端返回的证书信息获取公钥字符串PUB_KEY ,如下代码:
下面是HttpClient实体类
private final DefaultHttpClient httpClient;
//构造一个支持https的HttpClient
public RdHttpClient() {
BasicHttpParams httpParams = new BasicHttpParams();
...省略代码:http基本请求配置...
SchemeRegistry schemeRegistry = new SchemeRegistry();
...省略代码:支持http...
//配置自己的SSLSocketFactory
SSLSocketFactory sf = null;
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore
.getDefaultType());
trustStore.load(null, null);
sf = new MySSLSocketFactory(trustStore);//自己的SSLSocketFactory
sf.setHostnameVerifier(MySSLSocketFactory.STRICT_HOSTNAME_VERIFIER);//关键点:严格校验服务器域名
} catch (Exception e) {
e.printStackTrace();
}
//支持https,端口号为443
schemeRegistry.register(new Scheme("https", sf, 443));
ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(
httpParams, schemeRegistry);
httpClient = new DefaultHttpClient(cm, httpParams);
...省略代码...
}
下面是自己实现的SSLSocketFactory 类
public class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
RSAPublicKey pubkey = (RSAPublicKey) chain[0].getPublicKey();
String encoded = new BigInteger(1, pubkey.getEncoded()).toString(16);
Log.e("PUB_KEY",encoded);
}
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
}
打印log如下(当然字符串不全)
03-07 11:24:28.672 12103-12256/com.cnr.broadcastCollect E/PUB_KEY: 30820122300d06092a864886f70d01010105000382010f003082010a0282010100c9292d7
4.1.2 获取PUB_KEY 之后写死在MySSLSocketFactory 中或别的地方。
4.1.3 使用刚才获取的PUB_KEY 进行服务端合法性校验,只需将上面的MySSLSocketFactory 中的重写方法中补充完整。校验代码如下:
private final String PUB_KEY = "30820122300d06092a864886f70d01010105000382010f003082010a0282010100c9......"
public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (chain == null) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null");
}
if (!(chain.length > 0)) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}
if (!(null != authType && authType.equalsIgnoreCase("ECDHE_RSA"))) {
throw new CertificateException("checkServerTrusted: AuthType is not ECDHE_RSA");
}
for (X509Certificate cert:chain){
try {
cert.checkValidity();
} catch (Exception e) {
e.printStackTrace();
}
}
RSAPublicKey pubkey = (RSAPublicKey) chain[0].getPublicKey();
String encoded = new BigInteger(1, pubkey.getEncoded()).toString(16);
final boolean expected = PUB_KEY.equalsIgnoreCase(encoded);
if (!expected) {
throw new CertificateException("checkServerTrusted: Expected public key: " + PUB_KEY + ", got public key:" + encoded);
}
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}
通过上面的代码可以看出主要进行了如下校验:
1. 对服务端返回的证书做判空处理
2. 校验加密算法种类
3. 检查证书是否在有效期内
4. 校验公钥字符串是否相同
下面是通过Fiddler抓包工具得到的如下图:
题外话:上图是一个登陆接口,用的post请求;从中我们也可以知道即使使用了https,使用简单的抓包工具都能获取到数据信息,所以想要真正的保密的地方还是要加密处理的,无论是对称加密还是非对称加密。
4.2分析一下几个现有HostnameVerifier,在默认的SSLSocketFactory类中
public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
= new AllowAllHostnameVerifier();
public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
= new BrowserCompatHostnameVerifier();
public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
= new StrictHostnameVerifier();
这里只分析StrictHostnameVerifier,前面两个简单,只是重写方法的传参差异
4.2.1STRICT_HOSTNAME_VERIFIER 分析
如果按照4.1.1中RdHttpClient中sf.setHostnameVerifier(MySSLSocketFactory.STRICT_HOSTNAME_VERIFIER);这样的写法会出现一种现象就是:只有在app进行第一次网络请求的时候才进行服务器端合法性校验,后面的网络请求都不会进行校验(除非将app从后台杀死)。下面来看一下现象;
测试的方法是在MySSLSocketFactory的TrustManager回调函数checkServerTrusted中打log,如下面的代码:
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
Log.e("checkServerTrusted","checkServerTrusted");
...省略代码...
}
log代码如下
03-07 14:27:57.446 11056-11705/com.cnr.broadcastCollect E/HttpClient?getInstance: HttpClient getInstance
03-07 14:27:57.576 11056-11705/com.cnr.broadcastCollect E/checkServerTrusted: checkServerTrusted
03-07 14:27:57.714 11056-11705/com.cnr.broadcastCollect E/tag: js {"result":{"sex":"","channelName":"中国之声","channelId":"","dutyTitle":"","roleName":"节目编辑","userRole":"","departmentName":"早间节目部","departmentType":"","id":"","authority":"11111111111111111111111111111","token":"","roleType":"3","userName":"","departmentId":"","userPhonePath":"","loginName":""},"error":{"message":"","code":"0"}}
03-07 14:28:04.778 11056-11705/com.cnr.broadcastCollect E/HttpClient?getInstance: HttpClient getInstance
03-07 14:28:04.994 11056-11705/com.cnr.broadcastCollect E/tag: js {"totalNum":1499,"result":[{"taskId":"",ck":"","}]
03-07 14:28:08.377 11056-11705/com.cnr.broadcastCollect E/HttpClient?getInstance: HttpClient getInstance
03-07 14:28:09.717 11056-11705/com.cnr.broadcastCollect E/tag: js {"totalNum":453,"result":[{"taskId":"",,"createDate":"2018-03-07 10:10:29"}]
我们发现只有在登陆的时候(第一次请求)的时候验证了,而后面未验证(未调用checkServerTrusted)方法。
4.2.2从源码中找一下原因
StrictHostnameVerifier 类如下:
/**
下面注释再说名使用这种HostnameVerifier,就只在第一次链接的时候检查服务端的合法性,即造成上面的现象
*
* The hostname must match either the first CN, or any of the subject-alts.
* A wildcard can occur in the CN, and in any of the subject-alts. The
* one divergence from IE6 is how we only check the first CN. IE6 allows
* a match against any of the CNs present. We decided to follow in
* Sun Java 1.4's footsteps and only check the first CN. (If you need
* to check all the CN's, feel free to write your own implementation!).
*
* 下面的注释说明严格控制域名匹配,和BrowserCompatHostnameVerifier形成对比,后者宽泛:允许子域名匹配也可以认为域名相同,确认合法
* A wildcard such as "*.foo.com" matches only subdomains in the same
* level, for example "a.foo.com". It does not match deeper subdomains
* such as "a.b.foo.com".
*/
@Deprecated
public class StrictHostnameVerifier extends AbstractVerifier {
public final void verify(
final String host,
final String[] cns,
final String[] subjectAlts) throws SSLException {
verify(host, cns, subjectAlts, true);
}
@Override
public final String toString() {
return "STRICT";
}
}
AbstractVerifier 类
public abstract class AbstractVerifier implements X509HostnameVerifier {
public final void verify(final String host, final String[] cns,
final String[] subjectAlts,
final boolean strictWithSubDomains)
throws SSLException {
LinkedList names = new LinkedList();
if(cns != null && cns.length > 0 && cns[0] != null) {
names.add(cns[0]);
}
if(subjectAlts != null) {
for (String subjectAlt : subjectAlts) {
if (subjectAlt != null) {
names.add(subjectAlt);
}
}
}
if(names.isEmpty()) {
String msg = "Certificate for <" + host + "> doesn't contain CN or DNS subjectAlt";
throw new SSLException(msg);
}
StringBuffer buf = new StringBuffer();
String hostName = host.trim().toLowerCase(Locale.ENGLISH);
boolean match = false;
for(Iterator it = names.iterator(); it.hasNext();) {
String cn = it.next();
cn = cn.toLowerCase(Locale.ENGLISH);
buf.append(" <");
buf.append(cn);
buf.append('>');
if(it.hasNext()) {
buf.append(" OR");
}
boolean doWildcard = cn.startsWith("*.") &&
cn.indexOf('.', 2) != -1 &&
acceptableCountryWildcard(cn) &&
!isIPv4Address(host);
if(doWildcard) {
match = hostName.endsWith(cn.substring(1));
if(match && strictWithSubDomains) {
match = countDots(hostName) == countDots(cn);
}
} else {
match = hostName.equals(cn);
}
if(match) {
break;
}
}
if(!match) {
throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf);
}
}
}
从上面可以看出如果有需要想每次请求都验证服务端合法性那么只能直接实现X509HostnameVerifier接口自己处理验证过程。
4.3使用自签名证书配置客户端
大体思路有两种:第一种自己实现SSLSocketFactory 关键是重写校验证书链TrustManager中的方法checkServerTrusted,自己手工检验服务端合法性;第二种利用预留在客户端的公钥文件生成证书添加到系统的被信任列表中,让系统按照默认的校验方式校验。(个人推荐第一种)
在“三”中已经生成了服务端和客户端的相应证书库和证书,这里只说Android端的配置,服务端的配置自行百度。由于4.1中没有公钥证书,所以采取了上述的折中方法;
4.3.1先写第一种方案:自己手工校验:
将公钥证书放在工程的assets文件夹中或者用3.3中取出的公钥字符串都是可以的。
RdHttpclient不用动,MySSLSocketFactory中代码稍作改动如下:
public class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
Log.e("checkServerTrusted","checkServerTrusted");
if (chain == null) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null");
}
if (!(chain.length > 0)) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}
if (!(null != authType && authType.equalsIgnoreCase("ECDHE_RSA"))) {
throw new CertificateException("checkServerTrusted: AuthType is not ECDHE_RSA");
}
for (X509Certificate cert:chain){
try {
cert.checkValidity();
cert.verify(getCert().getPublicKey());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
private X509Certificate getCert(){
X509Certificate serverCert = null;
try {
InputStream certificate = App.getInstance().getResources().getAssets().open("ycb_server.cer");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
serverCert = (X509Certificate) certificateFactory.generateCertificate(certificate);
} catch (Exception e) {
e.printStackTrace();
}
return serverCert;
}
private X509Certificate getCertFromString(){
X509Certificate serverCert = null;
try {
InputStream certificate = new ByteArrayInputStream(PUB_KEY_CERT.getBytes());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
serverCert = (X509Certificate) certificateFactory.generateCertificate(certificate);
} catch (Exception e) {
e.printStackTrace();
}
return serverCert;
}
private final String PUB_KEY_CERT = "-----BEGIN CERTIFICATE-----\n" +
"MIICUjCCAbugAwIBAgIEBt35sTANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJjbjEQMA4GA1UE\n" +
"CBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEMMAoGA1UEChMDY25yMQwwCgYDVQQLEwNjbnIx\n" +
"HHHHBgNVBAMTBGdvbmcwHhcNMTgwMzA2MDcwMTQzWhcNMjgwMzAzMDcwMTQzWjBcMQswCQYDVQQG\n" +
"HHHHbjEQMA4GA1UECBMHYmVpamluZzEQMA4GA1UEKKKKYmVpamluZzEMMAoGA1UEChMDY25yMQww\n" +
"HHHHVQQLEwNjbnIxDTALBgNVBAMTBGdvbmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAI31\n" +
"yMvNkmdFLyDr4uI35lMZwZf9lqt/q0MHpIaMP7j4evtVMqs9SnAJwEprOK35+z5xIgqpD+ioy5Xf\n" +
"e1g2crM0r22qU229WOT4OfSk12bcobWw9Dr7Hqy5KKKKDtJ7hwg+c4mYE4WWZOH6REkR58c0LCoe\n" +
"4Y1g0iZOpXwiTjF7AgMBAAGjITAfMB0GA1UdDgQWBBTDpVe72BYQOuvexvW+WpDt5XrpPDANBgkq\n" +
"hkiG9w0BAQsFAAOBgQB0KhMRb7LhFB/8jWRa9owvSF9rEPmx8BzcsrMDQQEU+XV1dBhrr+YgADcl\n" +
"wgEHZjCHair5rTId888bdSN+OXYIMat5jRyH2MW+5ybRQYCCijQ6jjyK1TV+/hDNZSYtEk9RW9vC\n" +
"I7WMhdclqcbnzT6COg1cJHgrMhLlMz8bsoDZWQ==\n" +
"-----END CERTIFICATE-----";
}
同时还可以采用自己验证域名的方式,如下面的代码;但是我不推荐这种做法,因为采用自带的严格检验的StrictHostnameVerifier 里面写的更加的全面一些。
sf.setHostnameVerifier(new AbstractVerifier() {
@Override
public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
}
});
4.3.2 第二种利用预留在客户端的公钥文件生成证书添加到系统的被信任列表中,让系统按照默认的校验方式校验。
这里偷懒请查看使用HttpsURLConnection,里面有一部分讲如何配置;
同时okhttp的配置请看洪洋大神的okhttp中配置https,讲的很详细,是在不行有全套的代码。
五.总结
从网上找东西,信息本来就多,找到了还不能用这是最让人闹心的。以后记录的东西一定是我自己项目中用过的或者真正实践过的,因为知道原理和真的用到生产中还是有不小差距的。