在使用httpclient 工具类发送https请求时会出现在不构造ssl安全套接字的情况下,有的https可以直接发出去,而有些会报错
现象描述:
原因是jdk1.6对HTTPS处理不够完善,升级jdk1.7及以上版本可解决,以下是我自己分析过程
测试代码如下:
public static void main(String[] args) throws Exception {
// URL url = new URL("https://wx.zhimo.co");
// String ="https://wx.zhimo.co/mslive/lives/video/check";
// URL url = new URL("https://p.bokecc.com");
// URL url = new URL("https://wx.zhimo.co/mslive/lives/video/check");
long start = System.currentTimeMillis();
URL url = new URL("https://www.boxuegu.com");
URLConnection con = url.openConnection();
con.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8"));
String s;
while ((s = reader.readLine()) != null) {
System.out.println(s);
}
reader.close();
long end = System.currentTimeMillis();
System.out.println(end-start);
}
而在jdk 自带的HttpsURLConnection 构造ssl情况下也会出现发送https 失败的情况:代码如下:
public static String httpsRequestPost(String authplayurl, HashMap<String, String> param, int timeout) throws Exception {
InputStream instream = null;
BufferedReader br = null;
try {
//创建信任管理器TrustManager,其包含可信任材料TestTrustManager
TrustManager[] tm = { new CCTrustManager() };
//构造SSL环境,请求协议的标准名称为TLS
SSLContext sc = SSLContext.getInstance("TLS");
// SSLContext sc = SSLContext.getInstance("SSL");
//初始化SSLContext,指定信任管理器TrustManager
sc.init(null, tm, new SecureRandom());
URL url = new URL(authplayurl);
HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
httpsConn.setSSLSocketFactory(sc.getSocketFactory());
httpsConn.setConnectTimeout(timeout * 1000);
//对服务器证书经常改变做兼容(存在HTTPS中间人攻击漏洞)
// httpsConn.setHostnameVerifier(new TrustAnyHostnameVerifier());
httpsConn.setHostnameVerifier(new StrictHostnameVerifier());
// HostnameVerifier AllowAllHostnameVerifier
httpsConn.setRequestMethod("POST");
//设置是否使用缓存
httpsConn.setUseCaches(false);
//设置是否添加参数
httpsConn.setDoOutput(true);
httpsConn.getURL();
//参数处理
StringBuffer buffer = new StringBuffer();
for(Entry<String,String> entry : param.entrySet()){
buffer.append("&").append(entry.getKey()).append("=").append(entry.getValue());
}
String buf = buffer.toString().isEmpty() ? "" : buffer.substring(1);
OutputStream os = httpsConn.getOutputStream();
os.write(buf.getBytes("ISO-8859-1"));
os.flush();
os.close();
//获取https请求的输入流
instream = httpsConn.getInputStream();
br = new BufferedReader(new InputStreamReader(instream,"ISO-8859-1"));
String responseContent = "";
String tmp = "";
while ((tmp = br.readLine()) != null) {
responseContent += tmp + "\r\n";
}
if (!responseContent.equals("")) {
responseContent = responseContent.substring(0, responseContent.lastIndexOf("\r\n"));
}
Certificate[] certs = httpsConn.getServerCertificates(); //会拿到完整的证书链
X509Certificate cert = (X509Certificate)certs[0]; //cert[0]是证书链的最下层
System.out.println("序号:" + cert.getSerialNumber());
System.out.println("颁发给:" + cert.getSubjectDN().getName());
System.out.println("颁发者:" + cert.getIssuerDN().getName());
System.out.println("起始:" + cert.getNotBefore());
System.out.println("过期:" + cert.getNotAfter());
System.out.println("算法:" + cert.getSigAlgName());
System.out.println("指纹:" + getThumbPrint(cert));
return new String(responseContent.getBytes("ISO-8859-1"), "UTF-8");
} catch (KeyManagementException e) {
logger.error("", e);
} catch (NoSuchAlgorithmException e) {
logger.error("", e);
} catch (IOException e) {
logger.error("", e);
} finally {
try {
if (instream != null) {
instream.close();
}
if (br != null) {
br.close();
}
} catch (IOException e) {
logger.error("", e);
}
}
return null;
}
private static String getThumbPrint(X509Certificate cert) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] der = cert.getEncoded();
md.update(der);
byte[] digest = md.digest();
return bytesToHexString(digest);
}
private static String bytesToHexString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
public static String httpsRequestGet(String authplayurl, HashMap<String, String> param, int timeout) throws Exception {
InputStream instream = null;
BufferedReader br = null;
try {
//创建信任管理器TrustManager,其包含可信任材料TestTrustManager
TrustManager[] tm = { new CCTrustManager() };
//构造SSL环境,请求协议的标准名称为TLS
SSLContext sc = SSLContext.getInstance("TLS");
// SSLContext sc = SSLContext.getInstance("SSL");
//初始化SSLContext,指定信任管理器TrustManager
sc.init(null, tm, new SecureRandom());
URL url = new URL(authplayurl);
HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
httpsConn.setSSLSocketFactory(sc.getSocketFactory());
httpsConn.setConnectTimeout(timeout * 1000);
//对服务器证书经常改变做兼容(存在HTTPS中间人攻击漏洞)
// httpsConn.setHostnameVerifier(new TrustAnyHostnameVerifier());
httpsConn.setHostnameVerifier(new StrictHostnameVerifier());
// HostnameVerifier AllowAllHostnameVerifier
httpsConn.setRequestMethod("POST");
//设置是否使用缓存
httpsConn.setUseCaches(false);
//设置是否添加参数
httpsConn.setDoOutput(true);
httpsConn.getURL();
//参数处理
StringBuffer buffer = new StringBuffer();
for(Entry<String,String> entry : param.entrySet()){
buffer.append("&").append(entry.getKey()).append("=").append(entry.getValue());
}
String buf = buffer.toString().isEmpty() ? "" : buffer.substring(1);
OutputStream os = httpsConn.getOutputStream();
os.write(buf.getBytes("ISO-8859-1"));
os.flush();
os.close();
//获取https请求的输入流
instream = httpsConn.getInputStream();
br = new BufferedReader(new InputStreamReader(instream,"ISO-8859-1"));
String responseContent = "";
String tmp = "";
while ((tmp = br.readLine()) != null) {
responseContent += tmp + "\r\n";
}
if (!responseContent.equals("")) {
responseContent = responseContent.substring(0, responseContent.lastIndexOf("\r\n"));
}
Certificate[] certs = httpsConn.getServerCertificates(); //会拿到完整的证书链
X509Certificate cert = (X509Certificate)certs[0]; //cert[0]是证书链的最下层
System.out.println("序号:" + cert.getSerialNumber());
System.out.println("颁发给:" + cert.getSubjectDN().getName());
System.out.println("颁发者:" + cert.getIssuerDN().getName());
System.out.println("起始:" + cert.getNotBefore());
System.out.println("过期:" + cert.getNotAfter());
System.out.println("算法:" + cert.getSigAlgName());
System.out.println("指纹:" + getThumbPrint(cert));
return new String(responseContent.getBytes("ISO-8859-1"), "UTF-8");
} catch (KeyManagementException e) {
logger.error("", e);
} catch (NoSuchAlgorithmException e) {
logger.error("", e);
} catch (IOException e) {
logger.error("", e);
} finally {
try {
if (instream != null) {
instream.close();
}
if (br != null) {
br.close();
}
} catch (IOException e) {
logger.error("", e);
}
}
return null;
}
经过几天的查资料发现,根本原因是HostnameVerifier host校验的时候出的问题, 代码中拿到的证书的host跟url的不一致导致的域名验证一直不通过,
而host校验失败的原因是jdk 1.6 不支持SNI (Server Name Indication)的校验,
1.jdk 1.7以后新特性 安全/加密
椭圆曲线加密算法 (ECC),提供了一个可移植的标准椭圆曲线加密算法实现,所有的 Java 应用都可以使用椭圆曲线加密算法。
JSSE(SSL/TLS)
在证书链认证中设置关闭弱加密算法,比如 MD2 算法已经被证实不太安全。
增加对 TLS(Transport Layer Security) 1.1 和 1.2 的支持,它们对应的规范分别是 RFC 4346 和 RFC 5246。
SNI(Server Name Indication) 支持,其规范定义在 RFC 4366。
TLS 密钥重新协商机制,RFC 5746。
附上自己查的大神的文章
SNI
https://blog.csdn.net/makenothing/article/details/53292335
https主机名校验
https://www.aliyun.com/jiaocheng/547639.html
忽略hostname 校验的隐患
http://www.droidsec.cn/android-https%E4%B8%AD%E9%97%B4%E4%BA%BA%E5%8A%AB%E6%8C%81%E6%BC%8F%E6%B4%9E%E6%B5%85%E6%9E%90/