HTTPS证书校验
HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在HTTP 的基础下加入SSL,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间)。这个系统提供了身份验证与加密通讯方法。它被广泛用于万维网上安全敏感的通讯,例如交易支付等方面。
关于HTTPS证书的知识,水还是很深的本文先介绍一下我们常用的问题。
证书校验
默认的证书校验
在Java中要访问Https链接时,会用到一个关键类HttpsURLConnection;参见如下实现代码:
URL serverUrl = new URL("https://www.xxx.com");
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) serverUrl.openConnection();
int responseCode = httpsURLConnection.getResponseCode();
在取得httpsURLConnection的时候和正常浏览器访问一样,仍然会验证服务端的证书是否被信任(权威机构发行或者被权威机构签名);如果服务端证书不被信任,则默认的实现就会有问题。当证书过期时,默认的证书校验也。一般来说,用SunJSSE
会抛如下异常信息:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
SunJSSE
(Sun的Java安全套接扩展 Java Secure Socket Extension)是sun公司实现Internet安全通信的一系列包的集合。它是一个SSL和TLS的纯Java实现,可以透明地提供数据加密、服务器认证、信息完整性等功能,JSSE是一个开放的标准,不只是Sun公司才能实现一个SunJSSE,事实上其他公司有自己实现的JSSE,然后通过JCA就可以在JVM中使用(暂时还没研究过,一般都是用sun实现的)。
在深入了解JSSE之前,需要了解一个有关Java安全的概念:客户端的TrustStore文件。客户端的TrustStore文件中保存着被客户端所信任的服务器的证书信息。客户端在进行SSL连接时,JSSE将根据这个文件中的证书决定是否信任服务器端的证书。
JSSE中,有一个信任管理器类负责决定是否信任远端的证书,这个类有如下的处理规则:
⑴ 果系统属性javax.net.sll.trustStore指定了TrustStore文件,那么信任管理器就去jre安装路径下的lib/security/目录中寻找并使用这个文件来检查证书。
⑵ 果该系统属性没有指定TrustStore文件,它就会去jre安装路径下寻找默认的TrustStore文件,这个文件的相对路径为:lib/security/jssecacerts。
⑶ 如果 jssecacerts不存在,但是cacerts存在(它随J2SDK一起发行,含有数量有限的可信任的基本证书),那么这个默认的TrustStore文件就是cacerts。
当那遇到上面证书校验不通过的这种情况,怎么处理呢?有以下两种方案:
- 将证书导入到TrustStore文件中
Java提供了命令行工具keytool用于创建证书或者把证书从其它文件中导入到Java自己的TrustStore文件中。把证书从其它文件导入到TrustStore文件中的命令行格式为:
keytool -import -file src_cer_file –keystore dest_cer_store
其中,src_cer_file为存有证书信息的源文件名,dest_cer_store为目标TrustStore文件。
这种方式不灵活,对于移动应用来说基本无效,不可能让每一位用户手动安装一下证书。
-
实现自己的证书信任管理器类,比如X509TrustManager
实现自己的证书信任管理器类就是下面要说的自定义证书校验,这种方式灵活,但需要小心。
自定义证书校验
自定义证书的校验一般是实现X509TrustManager接口,该接口有三个方法需要实现。
- checkClientTrusted,该方法检查客户端的证书,无返回值,若不信任该证书则抛出异常。
public void checkClientTrusted(X509Certificate[] arg0, String arg1){
}
- checkServerTrusted,该方法检查服务器的证书,无返回值,若不信任该证书同样抛出异常。
public void checkServerTrusted(X509Certificate[] chain, String authType){
}
- getAcceptedIssuers,返回受信任的X509证书数组
public X509Certificate[] getAcceptedIssuers() {
}
自定义部分规则校验
/**
* 自定义部分规则校验,主要是针对服务器返回证书信息进行校验
*/
public class TrustCerManager implements X509TrustManager {
private Certificate[] mCertificates;
private String[] localPublicKeyStrs;
/**
* 构造方法,传入本地信任的证书cer文件所获取的Certificate信息。根据Certificate获取公钥信息
*/
public TrustCerManager(Certificate[] argCers) {
mCertificates = argCers;
localPublicKeyStrs = new String[argCers.length];
for (int i = 0; i < mCertificates.length; i++) {
Certificate cer = mCertificates[i];
PublicKey publicKey = cer.getPublicKey();
// 将公钥信息解析出来
localPublicKeyStrs[i] = byte2Base64(publicKey.getEncoded());
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (null != localPublicKeyStrs) {
int passCount = 0;
for (String publicKeyStr : localPublicKeyStrs) {
if (null != chain) {
// 遍历服务端证书链的证书信息
for (X509Certificate c : chain) {
// 获取服务端证书链的证书公钥
PublicKey netPublicKey = c.getPublicKey();
String netPublicKeyStr = byte2Base64(netPublicKey.getEncoded());
// 记录是否存在服务端证书链上的证书公钥与本地信任的相同
if (publicKeyStr.equals(netPublicKeyStr)) {
passCount++;
}
log.info("PublicKey={}", netPublicKey.toString());
}
}
}
log.info("passCount={}", passCount);
if (0 == passCount) {
// 没有一个比配上的证书公钥,抛出异常,若不信任该证书
throw new CertificateException();
}
} else {
System.out.println("CertificateException");
throw new CertificateException();
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
/**
* 字节数组转Base64编码
*
* @param bytes
* @return
*/
public static String byte2Base64(byte[] bytes) {
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(bytes);
}
/**
* Base64编码转字节数组
*
* @param base64Key
* @return
* @throws IOException
*/
public static byte[] base642Byte(String base64Key) throws IOException {
BASE64Decoder decoder = new BASE64Decoder();
return decoder.decodeBuffer(base64Key);
}
}
根据cer文件获取Certificate
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate cert = cf.generateCertificate(new FileInputStream("my.cer"));
这种方式可以解决证书过期问题,尤其移动端APP端的校验,当某一天证书过期了,可能保证客户端还能访问,也提高了证书的校验速度。
自定义完全信任证书
完全信任证书是指信任证书所有服务端的证书,无论它是否过期,是否经过认证。一般这种情况用在信任的域上面。
public class TrustAllManager implements X509TrustManager {
/**
* 该方法检查客户端的证书,若不信任该证书则抛出异常。由于我们不需要对客户端进行认证,因此我们只需要执行默认的信任管理器的这个方法。JSSE中,默认的信任管理器类为TrustManager。
*
* @param arg0
* @param arg1
* @throws CertificateException
*/
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
/**
* 该方法检查服务器的证书,若不信任该证书同样抛出异常。通过自己实现该方法,可以使之信任我们指定的任何证书。在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。
*
* @param chain
* @param authType
* @throws CertificateException
*/
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
/**
* 返回受信任的X509证书数组
*
* @return
*/
@Override
public X509Certificate[] getAcceptedIssuers() {
X509Certificate[] x509Certificates = new X509Certificate[0];
return x509Certificates;
}
}
X509TrustManager的使用
通常我们使用http请求除了HttpURLConnection外,用的第三方库有:OkHttp和HttpClient。下面分别介绍一下这三种式设置TrustManager的方法。
/**
* 根据自定义的X509TrustManager构建一个SSLSocketFactory
*/
public static SSLSocketFactory createAllSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
TrustManager[] trustManagers = new TrustManager[]{new TrustAllManager()};
SSLContext context = SSLContext.getInstance("SSL");
context.init(null, trustManagers, new SecureRandom());
return context.getSocketFactory();
}
createAllSSLSocketFactory方法下面三种方式都是使用到。
HttpURLConnection
URL serverUrl = new URL(url);
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) serverUrl.openConnection();
// 设置SSLSocketFactory
httpsURLConnection.setSSLSocketFactory(createAllSSLSocketFactory());
int responseCode = httpsURLConnection.getResponseCode();
OkHttp
OkHttpClient.Builder builder = new OkHttpClient.Builder();
// 设置hostnameVerifier
builder.hostnameVerifier((s, sslSession) -> true);
// 设置SSLSocketFactory
builder.sslSocketFactory(createAllSSLSocketFactory(), new TrustAllManager());
Request request = new Request.Builder()
.url(url)
.build();
Response response = builder.build().newCall(request).execute();
HttpClient
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(createAllSSLSocketFactory(), NoopHostnameVerifier.INSTANCE);
CloseableHttpClient client = HttpClients.custom()
// 设置SSLSocketFactory
.setSSLSocketFactory(sslsf)
.build();
//发送get请求
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
/**请求发送成功,并得到响应**/
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
/**读取服务器返回过来的json字符串数据**/
String strResult = EntityUtils.toString(response.getEntity());
System.out.println(strResult);
}