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都可以,无语了.....还是懂得太少了呀)



Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐