在典型的 HTTPS 使用场景中,会使用一个包含公钥及与其匹配的私钥的证书配置服务器。作为 SSL 客户端与服务器握手的一部分,服务器将通过使用公钥加密签署其证书来证明自己具有私钥。
不过,任何人都可以生成他们自己的证书和私钥,因此,一个简单的握手只能说明服务器知道与证书公钥匹配的私钥,除此之外什么都证明不了。解决此问题的一个方法是让客户端拥有其信任的一个或多个证书集。如果证书不在此集合中,则不会信任服务器。
但这个简单的方法有几个缺点。服务器应能够随时间的推移升级到更强的密钥(“密钥旋转”),使用新的公钥替换证书中的公钥。遗憾的是,客户端应用现在必须根据服务器配置发生的变化进行更新。如果服务器不在应用开发者的控制下(例如,如果服务器是一个第三方网络服务),则很容易出现问题。如果应用必须与网络浏览器或电子邮件应用等任意服务器通信,那么,此方法也会带来问题。
为弥补这些缺点,通常使用来自知名颁发者(证书颁发机构(CA))发放的证书配置服务器。主机平台一般包含其信任的知名 CA 的列表。从 Android 4.2 开始,Android 目前包含在每个版本中更新的 100 多个 CA。CA 具有一个证书和一个私钥,这点与服务器相似。为服务器发放证书时,CA 使用其私钥签署服务器证书。然后,客户端可以验证该服务器是否具有平台已知的 CA 发放的证书。
不过,在解决一些问题的同时,使用 CA 也会引发其他问题。因为 CA 为许多服务器发放证书,因此,您仍需要某种方式来确保您与您需要的服务器通信。为解决这个问题,CA 发放的证书通过类似 gmail.com 等具体名称或 *.google.com 等通配型主机集识别服务器。
以下示例会让这些概念更具体。下面的代码段来自命令行,openssl
工具的 s_client
命令将查看维基百科( Wikipedia) 的服务器证书信息。它指定端口 443,因为此端口是 HTTPS的默认端口。此命令将 openssl s_client
的输出发送到 openssl x509
,后者将根据 X.509 标准 格式化与证书有关的信息。具体而言,此命令获取subject和issuer信息,分别包含服务器名称信息 和 可认证 CA 的颁发结构。如下:
$ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuer subject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.org issuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA
你会看到证书是由 RapidSSL CA 为与 *.wikipedia.org 匹配的服务器发放的。
1. 访问知名CA发放证书的Https服务器
URL url = new URL("https://wikipedia.org"); URLConnection urlConnection = url.openConnection(); InputStream in = urlConnection.getInputStream(); copyInputStreamToOutputStream(in, System.out);
private void requestFromServer(final Context context, String https_url) throws NoSuchAlgorithmException, KeyManagementException, IOException { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { //校验服务器证书 if (chain == null) { throw new IllegalArgumentException("Check Server X509Certificates is null"); } if (chain.length < 0) { throw new IllegalArgumentException("Check Server X509Certificates is empty"); } for (X509Certificate cert : chain) { cert.checkValidity(); try { String certName = "abc.crt"; //一般将下载的证书放到项目中的assets目录下 InputStream certInput = new BufferedInputStream(context.getAssets().open(certName)); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); X509Certificate serverCert = (X509Certificate) certificateFactory.generateCertificate(certInput); cert.verify(serverCert.getPublicKey()); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } catch (SignatureException e) { e.printStackTrace(); } } } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } }; SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, null); HostnameVerifier hostnameVerifier = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { //校验服务器证书的域名是否相符(若不校验服务器证书域名则直接return true) HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier(); Boolean result = hv.verify("*.xxx.com", session); return result; } }; URL url = new URL(https_url); HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory()); httpsURLConnection.setHostnameVerifier(hostnameVerifier); // httpsURLConnection.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //信任所有主机(注意:此处SSLSocketFactory与sslContext.getSocketFactory()返回的是不同的类) InputStream in = httpsURLConnection.getInputStream(); //网络返回的数据处理... }
KeyStore
,然后用后者创建和初始化
TrustManager
。
TrustManager
是系统用于从服务器验证证书的工具,可以使用一个或多个 CA 从
KeyStore
创建,而创建的
TrustManager
将仅信任这些 CA。
如果是新的
TrustManager
,此示例将初始化一个新的
SSLContext
,后者可以提供一个
SSLSocketFactory
,您可以通过
HttpsURLConnection
用它来替换默认的
SSLSocketFactory
。这样一来,网络请求时将使用您的 CA 验证证书,
系统能够验证您的服务器证书是否来自值得信任的颁发者。
/** * 获取校验服务器端证书的SocketFactory * * @param context * @param certName 保存在assets路径下的服务器端证书文件名,如abc.crt * @return */ public static javax.net.ssl.SSLSocketFactory getSocketFactory(Context context, String certName) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException { InputStream certInput = new BufferedInputStream(context.getAssets().open(certName)); //以X.509格式获取证书 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); Certificate cert = certificateFactory.generateCertificate(certInput); //生成一个包含服务器端证书的KeyStore String keyStoreType = KeyStore.getDefaultType(); KeyStore keyStore = KeyStore.getInstance(keyStoreType); keyStore.load(null, null); keyStore.setCertificateEntry("cert", cert); //用包含服务器端证书的KeyStore生成一个TrustManager String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm); trustManagerFactory.init(keyStore); //生成一个使用我们TrustManager的SSLContext SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagerFactory.getTrustManagers(), null); return sslContext.getSocketFactory(); } public static void test(final Context context) { new Thread() { @Override public void run() { super.run(); String https_url = "https://www.xxx.com"; try { URL url = new URL(https_url); HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); httpsURLConnection.setSSLSocketFactory(getSocketFactory(context, "abc.cer")); InputStream in = httpsURLConnection.getInputStream(); ... } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (KeyStoreException e) { e.printStackTrace(); } catch (KeyManagementException e) { e.printStackTrace(); } } }.start(); }
private void requestFromServer(final Context context, String https_url) throws NoSuchAlgorithmException, KeyManagementException, IOException { X509TrustManager[] trustAllCerts = new X509TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {//不校验客户端证书
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//不校验服务器证书
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, null);
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
//不校验服务器证书的域名
return true;
}
};
OkHttpClient okHttpClient = new OkHttpClient.Builder().hostnameVerifier(hostnameVerifier)
.sslSocketFactory(sslContext.getSocketFactory(), trustAllCerts[0]).build();
Request request = new Request.Builder().url(https_url).build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
public class MyWebViewClient extends WebViewClient { ... @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { handler.proceed(); } }