在上一篇 一文读懂Https原理 中,我主要介绍了Https的安全机制和实现原理,比较偏向于理论研究,可能会有点抽象。为了加深理解,今天写一篇Https的实战教程,主要介绍Https在Android中的实际应用,通过理论和实战的结合的方式相信可以帮助我们更好地理解Https。
Android对于Https的支持在系统层面上已经帮我们封装的很好了,系统会内置合法ca机构的根证书,只要服务器证书是由这些机构或者是其中间机构签发的那么系统会自动做安全校验,我们不需要做任何事情,直接请求https就可以。
但是如果服务器证书是自签名的,就需要我们对请求逻辑做一定的改造,不然你发起的所有请求都会失败,因为系统在做ssl证书校验时会认为这是一个非法请求直接阻断掉。
今天我们主要来探讨下Android中信任自签名证书如何实现。
操作系统:macOs10.14
网络请求框架:okhttps4.3.1
首先,我们需要自己生成一个自签名证书。自签名证书可以通过keytool生成,打开终端输入下面的命令即可。
keytool -genkey -alias my_server -keyalg RSA -keystore my_server.jks -validity 3600 -storepass 123456
其中-alias和-keystore是自己取的名字,后面的-storepass是自己设的密钥库密码。执行命令后会出现一些提示,可以随便输入一些值。
如果操作成功的话就会在当前目录下生成对应的xxx.jks文件。
2. 从密钥库导出证书(导出的是根证书)
keytool -export -alias my_server -file my_server.cer -keystore my_server.jks -storepass 123456
其中-keystore和-storepass需要跟刚才生成的密钥库一致。操作成功后就会在当前目录下看到xxx.cer的自签名证书了。
生成自签名证书后需要把证书配置到服务器上,所以我们还需要自己搭个简单的服务器。
mac下安装tomcat可以看下这个教程
安装成功后通过在终端运行如下命令启动服务器
catalina run
Tomcat的默认端口是8080,如果运行成功可通过http://localhost:8080访问。
tomcat的根目录在mac下一般是/usr/local/Cellar/tomcat/
找到tomcat/conf/sever.xml文件,并以文本形式打开。
在Service标签中加入:
<Connector SSLEnabled="true" acceptCount="100" clientAuth="false"
disableUploadTimeout="true" enableLookups="true" keystoreFile="/Users/administrator/Documents/blog/my_server.jks" keystorePass="123456" maxSpareThreads="75"
maxThreads="200" minSpareThreads="5" port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="https"
secure="true" sslProtocol="TLS"
/>
keystoreFile的值是刚才生成的jks文件的路径:/Users/administrator/Documents/blog/my_server.jks(填写你的路径),keystorePass值是密钥库密码:123456(填写你的密码),port是https的请求端口。保存成功后重启tomcat即可。
启动以后,打开浏览器输入https://localhost:8443/会看到证书不可信任的警告。输入地址时需要注意这时候的端口已经是8443不是8080了,8443是前面在sever.xml配置的https访问端口号,除此之外协议栏要输入https而不是http。
这里再说下为什么chrome浏览器会有这样的安全提示,因为chrome浏览器同样也会内置合法ca机构的根证书,用这些证书来校验服务器证书的合法性,因为我们的证书并不是合法ca机构签发的,所以就会报安全提示。如果对这方面还不是很了解的话可以再去看下我上一篇文章 一文读懂Https原理
直接选择打死也要进入,即可进入tomcat默认的主页,这时候左上角会显示不安全的提示。
环境搭建好了,那么现在开始步入正题,如何在Androig中信任Https自签名证书。
首先引入okhttp框架,通过okhttp进行网络请求。
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.3.1")
...
}
为了便于比较,我会发起两个https请求,一个是访问百度网址,一个是访问我刚才搭建的服务器,百度网址肯定用的是ca证书,而我搭建的服务器用的是自签名证书。
public static final String URL_CA = "https://www.baidu.com/";
public static final String URL_SELF = "https://192.168.0.103:8443/";
网络请求代码
private void getCa(){
Runnable runnable = new Runnable() {
@Override
public void run() {
String response = null;
try {
response = get(URL_CA);
} catch (IOException e) {
e.printStackTrace();
}
Log.d("ssl_test", "MainActivity: getCa: response="+response);
}
};
Thread thread = new Thread(runnable);
thread.start();
}
private void getSelf(){
Runnable runnable = new Runnable() {
@Override
public void run() {
String response = null;
try {
response = get(URL_SELF);
} catch (IOException e) {
e.printStackTrace();
}
Log.d("ssl_test", "MainActivity: getSelf: response="+response);
}
};
Thread thread = new Thread(runnable);
thread.start();
}
String get(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
Response response = MyApplication.getOkHttpClient().newCall(request).execute();
return response.body().string();
}
请求结果
百度网址能成功访问,但是我搭建的服务器访问失败了,并且报了一个SSLHandshakeException异常,这就说明ssl验证失败了,所以不能直接访问自签名证书的https。
前面说了系统只会信任内置的ca证书,所以直接访问自签名证书的https会失败。那么可以把权限放开,让系统信任所有的证书,这样自签名的https请求就能校验通过了。
这种方式会有安全风险,建议只在debug包开启,这样可以方便测试,release包千万不要这么干,不然你的应用将没有安全性可言。
下面是关键代码,思路就是创建一个信任所有https证书的okhttp客户端,通过该客户端发起的https请求会默认信任所有证书。
/**
* 创建信任所有证书的OkHttpClient
* @return
*/
public static OkHttpClient createTrushAllClient(){
return createSSLClient(createTrustAllTrustManager());
}
/**
* 创建信任所有证书的TrustManager
* @return
*/
private static X509TrustManager createTrustAllTrustManager() {
return 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];
}
};
}
private static OkHttpClient createSSLClient(X509TrustManager x509TrustManager){
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.sslSocketFactory(createSSLSocketFactory(x509TrustManager),x509TrustManager)
.hostnameVerifier(new TrustAllHostnameVerifier());
return builder.build();
}
private static SSLSocketFactory createSSLSocketFactory(TrustManager trustManager) {
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;
}
//实现信任所有域名的HostnameVerifier接口
private static class TrustAllHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
//域名校验,默认都通过
return true;
}
}
请求结果
通过日志可以看到这时候百度网址和自搭建的服务器都可以成功访问了。
虽然说信任所有证书简单方便,但是确有很大的安全问题,因为不管是系统证书还是用户导入的证书都会被信任。
这会带来什么问题呢?
我们都知道fiddler代理https的原理其实就是将服务器证书替换成自己的证书,因为fiddler证书是自签名的,所以用户需要在手机上手动信任fiddler证书才可以实现对https的抓包,因为有手动信任证书这一步,所以即使出现中间人攻击,只要用户没有手动信任其证书,也不会出现问题。那么假如我们在代码层面上直接信任所有证书,这样就不需要在手机导入fiddler证书也可以对应用实现https抓包,也就是说任何一个中间人攻击都可以成功,应用数据随时都有被窃听的风险。
其实即使是默认信任系统内置的ca根证书在理论上也是有一定安全风险的,因为ca机构有可能存在被黑客攻破的风险,如果被破解那么黑客就可以伪造合法机构签发的证书招摇撞骗。所以对于一些安全要求较高的应用可以自己指定信任的证书,这样不管是ca机构签名还是自签名的证书,只要不在信任列表里的都会请求失败。
如果要信任指定证书,首先需要把受信任的证书导入到项目中。
导入证书有两种方式,一种是通过文件方式导入,一种是以字符串形式导入。通常都比较推荐以字符串形式导入,因为可以减少包体积。
文件形式导入
1.将xxx.cer证书文件放在assets目录下
2.将证书转成流形式
private InputStream getInputStreamFromAsset(){
InputStream inputStream = null;
try {
inputStream = getAssets().open("my_server.cer");
} catch (IOException e) {
e.printStackTrace();
}
return inputStream;
}
字符串形式导入
1.通过keytool将证书转成rfc样式的字符串
keytool -printcert -rfc -file my_server.cer
转换后的字符串
-----BEGIN CERTIFICATE-----
MIIDOzCCAiOgAwIBAgIEXZVqZDANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJo
aDELMAkGA1UECBMCaGgxCzAJBgNVBAcTAmhoMQswCQYDVQQKEwJoaDELMAkGA1UE
CxMCaGgxCzAJBgNVBAMTAmhoMB4XDTIwMDQwNDEwMjEyNFoXDTMwMDIxMTEwMjEy
NFowTjELMAkGA1UEBhMCaGgxCzAJBgNVBAgTAmhoMQswCQYDVQQHEwJoaDELMAkG
A1UEChMCaGgxCzAJBgNVBAsTAmhoMQswCQYDVQQDEwJoaDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAKkCLt6IMQLDkCtKvmBkmw0sNovuiiduse64bFqW
o4HD9CTUNBDxKqY6TWi5CBaqIzINvveQK5h2vM5or60eVq0x5QSplOfru7yHPlpw
BdnpZ7ffISqUAHggpEmPf4nCKabF7D9HfTn3ZqgI/LelnxAOxzxyFfDwah6h3Xph
9ySC6uHJxrjbgsR8C84rnqw540D7MY4HE/rhDuRzTP1LH9/70LDHeUsCkMfRQ4A6
Z4CTDzYP7ATx4P35WoejihitEdBMquLZ2XYNDklRvGWJ2XkS2cteJUWzEdYVdnxV
fjYAiwu9mORtPwX9YiyoRdAnOSpiya/PnPjBlFOJZSsY5o0CAwEAAaMhMB8wHQYD
VR0OBBYEFMzRbwBzE5NrSFXRCNXgfddfLh/fMA0GCSqGSIb3DQEBCwUAA4IBAQAP
3/v7oZPx4++176BtEGhTy/xchMI1gqfxiM54j1bvjFN+90VQ3OdFStvMQWKP1tlm
EJHfL0ayLcVUsqiWRlykroWo9kEE9sOhbnZ8l9PGbPwbK5bYDXS80v+fx1e4mFxC
wrU/KLPqnG1GIdB8q+y+B8PgB8zUl5oLgaqN86jg+hc2kv5vD2n3RXvopZCO0/Mf
qODZ08gSBZeCDOXwgn1PpRijBxp7n4VG3Pb73L+A7dmA9aXGhvXDMIevDhLCc7hw
xBw8RmCyioDafdY7l4CVpvue1R6r4GJTPcQipJ2kEM4FagVwomtQvQRSoxr2ZTnY
6bIW+OcdKMaquNMgWsIA
-----END CERTIFICATE-----
2.将字符串转成流形式
/**
* 以rfc字符串形式导入证书流(推荐使用,因为不用常见assets文件占用包大小)
* @return
*/
private InputStream getInputStreamFromString(String rfc){
return new Buffer()
.writeUtf8(rfc)
.inputStream();
}
/**
* 创建信任指定证书的OkHttpClient(可以用于信任自签名证书)
* @return
*/
public static OkHttpClient createTrustCustomClient(InputStream inputStream){
return createSSLClient(createTrustCustomTrustManager(inputStream));
}
/**
* 创建只信任指定证书的TrustManager
* @param inputStream:证书输入流
* @return
*/
@Nullable
private static X509TrustManager createTrustCustomTrustManager(InputStream inputStream) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
Certificate certificate = certificateFactory.generateCertificate(inputStream);
//将证书放入keystore中
String certificateAlias = "ca";
keyStore.setCertificateEntry(certificateAlias, certificate);
if (inputStream != null) {
inputStream.close();
}
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];
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static OkHttpClient createSSLClient(X509TrustManager x509TrustManager){
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.sslSocketFactory(createSSLSocketFactory(x509TrustManager),x509TrustManager)
.hostnameVerifier(new TrustAllHostnameVerifier());
return builder.build();
}
private static SSLSocketFactory createSSLSocketFactory(TrustManager trustManager) {
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;
}
//实现信任所有域名的HostnameVerifier接口
private static class TrustAllHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
//域名校验,默认都通过
return true;
}
}
请求结果
通过日志可以发现这时候自签名证书的https请求成功了,但是百度网址请求失败了,这是因为现在我们只信任了自签名证书,所以用其他签名证书的https请求都会不受信任,就会请求失败。
关于Android信任自签名证书最后总结下几个要点
github
如果觉得文章对你有帮助,帮忙点个赞哈,您的支持是我不断创作的动力。