2018.2.3晚更新 添加关于虚拟机参数jdk.http.auth.tunneling.disabledSchemes的说明
2018.2.3 凌晨 修改:添加了连接代理,以及配置连接代理的用户名密码的代码
2017.12.22 完成
完全参照以下文章,进行了自己一些整理:
java获取https网站证书,附带调用https:webservice接口
代码功能说明:
根据域名自动下载https服务端发送过来的证书并保存成文件
本来代码可以正常工作,但是在某些机器在上连接需要鉴权的机器时会出错。
一直报 java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.0 407 Unauthorized"
尝试了各种设置代理的方法都无效,抓包发现http请求的头部中都没有携带Proxy-Authorization这个选项。在经历了苦苦一天的搜索之后,终于在stackoverflow上找到了答案:
某些虚拟机的lib/net.properties会有如下配置:
#jdk.http.auth.tunneling.disabledSchemes=
jdk.http.auth.proxying.disabledSchemes=Basic
这个配置会禁止proxy使用用户名密码这种鉴权方式。
解决方法有两种:
1.直接修改此配置文件为:
jdk.http.auth.tunneling.disabledSchemes=
#jdk.http.auth.proxying.disabledSchemes=Basic
2.在启动虚拟机的时候加上参数:
java -jdk.http.auth.tunneling.disabledSchemes=
stackoverflow链接:
https://stackoverflow.com/questions/41505219/unable-to-tunnel-through-proxy-proxy-returns-http-1-1-407-via-https
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Authenticator;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URL;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
public class Test_downloadHttpsCert {
static String proxyHost = "192.168.0.104";
static int proxyPort = 808;
static String proxyUser = "testuser";
static String proxyPassword = "testpw1";
public static void setAuthenticator() {
Authenticator authenticator = new Authenticator() {
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(proxyUser, proxyPassword.toCharArray());
}
};
Authenticator.setDefault(authenticator);
}
public static void main(String[] args) throws Exception {
String host = "www.baidu.com";
int port = 443;
File file = new File(System.getProperty("java.home") + File.separator + "lib" + File.separator + "security/cacerts");
String keyStorePassword = "changeit";
// 新建一个信任管理工厂,信任仓库为jre的cacerts文件
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(new FileInputStream(file), keyStorePassword.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
System.out.println(tmf.getTrustManagers().length);
// 获取信任管理类的第一个(调试代码时发现也只有一个)
X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
// 改写第一个,从而能够拿到服务器发送过来的证书
SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory factory = context.getSocketFactory();
// httpUrlConnection写法
{
SocketAddress proxyInetAddress = new InetSocketAddress(proxyHost, proxyPort);
Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyInetAddress);
URL url = new URI("https", null, host, port, "", null, null).toURL();
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(proxy);
connection.setConnectTimeout(10 * 1000);
connection.setSSLSocketFactory(factory);
connection.setRequestMethod("GET");
//第一种设置代理密码的方法 (测试无效)
// System.setProperty("https.proxyUser", proxyUser);
// System.setProperty("https.proxyPassword", proxyPassword);
//第二种写法 测试无效(抓包发现根本就不会带这个header)
// String key = "Proxy-Authorization";
// String base64 = Base64.getEncoder().encodeToString((proxyUser + ":" + proxyPassword).getBytes());
// String value = "Basic " + base64;
// connection.setRequestProperty(key, value);
//第三种写法 测试有效(如果还有错请继续看文章后面介绍)
setAuthenticator();
try{
connection.connect();
System.out.println("no error,certificate is already trusted.");
} catch(SSLException e){
System.out.println("catch SSLException.");
}
}
// socket 写法
// {
// SSLSocket socket;
// boolean useProxy = true;
// if(useProxy) {//设置代理
// SocketAddress proxyInetAddress = new
// InetSocketAddress(proxyHost,proxyPort);
// Proxy proxy = new Proxy(Proxy.Type.HTTP,proxyInetAddress);
// Socket orginSocket = new Socket(proxy);
//
// SocketAddress server = new InetSocketAddress(host,port);
//
// setAuthenticator();//密码认证必须放在连接之前
//
// //这句话非常重要,之前就是因为没写这句话,花了一两个小时的时间才找
// //到这个函数加上去(不过还是有些成就感,网上没找到相关代码,这句话是自己摸索出来的)
// orginSocket.connect(server);
//
// socket = (SSLSocket) factory.createSocket(orginSocket, host, port,
// true);
// }else{
// socket = (SSLSocket)factory.createSocket(host, port);
// }
// socket.setSoTimeout(10000);
// try {
// //握手的过程中服务器会发送证书过来
// socket.startHandshake();
// socket.close();
// //如果证书没有被信任,上面握手的代码会抛出异常
// System.out.println("No errors, certificate is already trusted");
// } catch (SSLException e) {
// //e.printStackTrace(System.out);
// System.out.println("SSLException.begin download cacerts.");
// }
// }
// 完成握手之后,
X509Certificate[] chain = tm.chain;
if (chain == null) {
throw new RuntimeException("Could not obtain server certificate chain");
}
System.out.println("received " + chain.length + " certificate(s) from server:");
MessageDigest sha1 = MessageDigest.getInstance("SHA1");
MessageDigest md5 = MessageDigest.getInstance("MD5");
for (int i = 0; i < chain.length; i++) {
X509Certificate cert = chain[i];
System.out.println(" " + (i + 1) + " Subject " + cert.getSubjectDN());
System.out.println(" Issuer " + cert.getIssuerDN());
sha1.update(cert.getEncoded());
System.out.println(" sha1 " + toHexString(sha1.digest()));
md5.update(cert.getEncoded());
System.out.println(" md5 " + toHexString(md5.digest()));
System.out.println();
}
// 只导入服务器发来的第一个证书。当然可以导入多个
int k = 0;
X509Certificate cert = chain[k];
String alias = host + "-" + (k + 1);
ks.setCertificateEntry(alias, cert);
// 将keystore中的证书条目输出到文件中
OutputStream out = new FileOutputStream("jssecacerts");
ks.store(out, keyStorePassword.toCharArray());
out.close();
System.out.println("Added certificate to keystore 'jssecacerts',alias:" + alias);
}
private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();
private static String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int b : bytes) {
b &= 0xff;
sb.append(HEXDIGITS[b >> 4]);
sb.append(HEXDIGITS[b & 15]);
}
return sb.toString();
}
private static class SavingTrustManager implements X509TrustManager {
private final X509TrustManager tm;
private X509Certificate[] chain;
SavingTrustManager(X509TrustManager tm) {
this.tm = tm;
}
public X509Certificate[] getAcceptedIssuers() {
throw new UnsupportedOperationException();
}
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
throw new UnsupportedOperationException();
}
// 将服务器端发来的证书保存下来
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
this.chain = chain;
tm.checkServerTrusted(chain, authType);
}
}
}
遇到的bug及解决方法:
是在本机开ccproxy进行验证的
1.
Exception in thread "main" java.net.SocketException: Software caused connection abort: recv failed
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(Unknown Source)
at java.net.SocketInputStream.read(Unknown Source)
at java.net.SocketInputStream.read(Unknown Source)
at sun.security.ssl.InputRecord.readFully(Unknown Source)
at sun.security.ssl.InputRecord.read(Unknown Source)
at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(Unknown Source)
at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
at Test_downloadHttpsCert.main(Test_downloadHttpsCert.java:74)
这是由于代理的目的ip设置的不对,不知道啥原因配127.0.0.1会报这个错。而且无线局域网的ip也会报错,配以太网适配器的ip就好了(代理开的是0.0.0.0)。
。。。。。(写这段文字的时候又回去试了一下,发现每个ip都可以,无语了.....还是懂得太少了呀)