1 概述
OkHttp
配置HTTPS
访问,核心为以下三个部分:
sslSocketFactory
HostnameVerifier
X509TrustManager
第一个是套接字工厂,第二个用来验证主机名,第三个是证书信任器管理类。通过OkHttp
实现HTTPS
访问需要自己实现以上三部分,另外还简单提及了服务器端的部署,用的是Tomcat9
,最后是一些常见问题的可能解决方案。
2 OkHttp
介绍
OkHttp
是一款开源的处理网络请求的轻量级框架,有Square
公司贡献,用于替代HttpUrlConnection
与Apache HttpClient
,优点有:
- 共享
Socket
,HTTP/2
支持所有连接到同一个主机的请求共享Socket
- 连接池可以减少请求延迟
- 缓存响应数据减少重复的网络请求
- 自动处理
gzip
压缩
3 准备工作
- 一台服务器
- 一个域名
- 一个证书
4 OkHttp
部分
4.1 暴力方案
public static String test() {
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(createSSLSocketFactory(), new TrustAllCerts())
.hostnameVerifier(new TrustAllHostnameVerifier()).build();
String url = "https://xxxxxxx"; //修改成自己的url
Request request = new Request.Builder().url(url).build();
Call call = build.newCall(request);
Response response = call.execute();
if(response.body() != null)
{
String result = response.body().string();
//处理result
}
}
private static class TrustAllCerts implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[0];}
}
private static class TrustAllHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) { return true; }
}
private static SSLSocketFactory createSSLSocketFactory() {
SSLSocketFactory ssfFactory = null;
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[]{new TrustAllCerts()}, new SecureRandom());
ssfFactory = sc.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return ssfFactory;
}
这是一种暴力的方案,看类名就知道了,信任所有的证书与主机:
public boolean verify(String hostname, SSLSession session) { return true; }
这个方法直接返回true
,也就是信任所有的主机。
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
这里两个check
函数没有做任何的工作,表示接受任意的客户端与服务端的证书。这样写的话相当于是使用了一个没用的TrustManager
,这样还不如不加密,不推荐使用。
4.2 推荐方案
从两方面入手修改,一是从X509TrustManager
入手,二是从HostnameVerifier
入手。
4.2.1 HostnameVerifier
先说个简单的,这里主要是验证主机名,简单的话,可以如下实现:
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
if("www.test.com".equals(hostname)){
return true;
}
else {
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify(hostname, session);
}
}
};
这里验证主机名是www.test.com
就返回true
(也可以使用服务器IP
进行验证),实现得比较简单,业务复杂的话可以结合配置中心,黑/白名单等动态校验。
4.2.2 X509TrustManager
接着是X509TrustManager
的处理,这里其实有两种方式,一种是以流的方式添加信任证书(来源):
private static X509TrustManager trustManagerForCertificates(InputStream in)
throws GeneralSecurityException
{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
char[] password = "password".toCharArray(); // 这里可以使用任意密码
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}
// Use it to build an X509 trust manager.
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager))
{
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
}
返回一个信任由输入流读取的证书的信任管理器,若证书没有被签名则抛出SSLHandsakeException
,证书建议使用第三方签名的而不是自签名的(比如使用OpenSSL
生成),特别是在生产环境中,例子的注释也提到:
完整代码见文末。这里把工具类的方法实现成了静态,调用时可以直接:
OKHTTP.send("https://xxxxx");
另一种方式是直接自定义一个TrustManager
,重写里面的三个方法:
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain,String authType) throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] chain,String authType) throws CertificateException {
for (X509Certificate cert : chain) {
// Make sure that it hasn't expired.
cert.checkValidity();
// Verify the certificate's public key chain.
try {
cert.verify(((X509Certificate) ca).getPublicKey());
} 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];
}
}
}, null);
第一个方法为
@Override
public void checkClientTrusted(X509Certificate[] chain,String authType) throws CertificateException {}
该方法检查客户端的证书,由于不需要对客户端进行认证,默认即可。
第二个方法为
@Override
public void checkServerTrusted(X509Certificate[] chain,String authType)
该方法检查服务器的证书,若不信任该证书则抛出异常,通过自己实现该方法可以信任任何自己指定的证书,不做任何处理的话,不会抛出任何异常,相当于信任所有证书。这里检查了证书是否过期以及证书的签名是否匹配。
第三个方法为
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
返回受信任的X509
证书数组。
这种方法笔者没有试过,仅供参考。
5 服务器部署
服务器用的是Tomcat
,简单介绍一下部署。
5.1 上传工程
后端处理用的Spring Boot
的工程,就不演示了,使用打包后上传到webapps
下即可。
5.2 Tomcat
配置
重点说一下Tomcat
的配置,首先需要一个域名,修改conf/server.xml
文件,找到默认的名叫localhost
的Host
:
然后直接复制Host
标签,把name
修改成自己的域名即可。
然后是证书的配置,笔者的证书在某某云上购买的,这里提供了几种格式的证书下载:
Tomcat
的是两个文件,一个是pfx
文件,一个是密码文件,把pfx
文件上传到服务器的Tomcat
后,继续修改server.xml
,搜索8443
找到如下位置(Tomcat 9.0.33
):
一些Tomcat8
的高版本提供了HTTP/2
的实现,默认使用apr
实现的,这里使用的是HTTP/1.1
,使用HTTP/2
需要额外安装Apr
、Apr-util
以及Tomcat-native
,因此这里采用HTTP/1.1
实现。
修改如下:
添加了scheme
、secure
、keystoreFile
、keystoreType
、keystorePass
、clientAuth
、sslProtocol
配置,同时去掉里面的<SSLHostConfig>
,keystoreFile
是刚才的pfx
文件,采用绝对路径,keystorePass
是密码。
另外默认的端口为8443
,这里修改成了8123
。
如果想要更安全的话可以手动指定TLS
的版本:
<Connector ...
sslProtocol="TLS" sslEnabledProtocols="TLSv1.3"
>
- 1
- 2
- 3
- 1
- 2
- 3
重启Tomcat
后输入
https://www.test.com:port
- 1
- 1
进行测试
这样就成功了。
6 验证与源码
这个因为没有完整的Demo很难做验证,具体来说前端用的OkHttp
核心都介绍了,后端的话服务器Tomcat
也介绍了,用Spring Boot
做个Demo应该不难。
这里只给出了工具类OKHTTP
的源码:
package xxx.xxx;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import javax.net.ssl.*;
import java.io.*;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
public class OKHTTP {
private static OkHttpClient client;
private static X509TrustManager trustManager;
static
{
try
{
//这里是服务器的证书文件,笔者查看了其他的教程,使用的是getAssets().open(),那是AS的工程
//这里是Maven工程,证书文件放在了src/main/resources下
//可以以pem或crt结尾,具体可以向购买证书的服务商查询.
trustManager = trustManagerForCertificates(new FileInputStream("src/main/resources/server.crt"));
client = new OkHttpClient.Builder()
.sslSocketFactory(createSSLSocketFactory(), trustManager)
.hostnameVerifier((hostname, sslSession) -> {
//验证主机名
if("www.test.com".equals(hostname))
{
return true;
}
else
{
HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
return verifier.verify(hostname,sslSession);
}
}).build();
}
catch (GeneralSecurityException | FileNotFoundException e)
{
e.printStackTrace();
}
}
public static String send(String url)
{
Request request = new Request.Builder().url(url).build();
//如果想要加上get/post请求的话再.build前添加即可
try (Response response = client.newCall(request).execute())
{
ResponseBody body = response.body();
return body == null ? null : body.string();
}
catch (IOException e)
{
e.printStackTrace();
return null;
}
}
//以下代码为别人的轮子
private static X509TrustManager trustManagerForCertificates(InputStream in)
throws GeneralSecurityException
{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
char[] password = "password".toCharArray(); // 这里可以使用任意密码
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}
// Use it to build an X509 trust manager.
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager))
{
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
}
private static KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); // 这里添加自定义的密码,默认
InputStream in = null; // By convention, 'null' creates an empty key store.
keyStore.load(in, password);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static SSLSocketFactory createSSLSocketFactory() {
SSLSocketFactory ssfFactory = null;
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[]{trustManager}, new SecureRandom());
ssfFactory = sc.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return ssfFactory;
}
}
7 常见问题
7.1 Tomcat HTTPS
无法访问
- 证书文件错误,不过这个可能性比较少
- 配置错误,请检查配置文件是否正确,可以
ps -ef | grep tomcat
查看Tomcat
是否开启以及查看logs/catalina.out
日志 - 端口错误,访问的端口需要与
<Connector>
中的端口对应 - 安全组/防火墙问题,云服务器的话需要在安全组配置中开启相应端口,同时应查看有没有把某个
IP
列入黑名单导致无法访问。防火墙的话这里主要指iptables
,如果没有开启的话不需要理会,如果开启的话需要开放对应端口
7.2 OkHttp HTTPS
无法访问
- 无法读取证书文件:需要把证书文件放在工程对应路径下读取,比如
Android Studio
中放在assets
下然后使用getAssets().open("xxx.xxx")
获取,Maven
工程的话放在resources
下直接使用FileInputStream
获取 singed fields invalid
:
证书文件格式错误,使用.crt/.pem
等证书
Signature does not match
:这个有可能是使用OpenSSL
自生成证书在验证的时候出现的异常,可能的解决办法是转换证书的格式,如果不行就重新生成一次证书