1. 程式人生 > >如何在不重啟 JVM 的情況下重新載入證書檔案?

如何在不重啟 JVM 的情況下重新載入證書檔案?

某種情況下需要動態下載證書檔案後使用 SSL 訪問某服務,但由於 JVM 使用預設的證書訪問新服務,該證書在 JVM 啟動的時候載入,那時還沒有新服務的證書,所以會報 ValidatorException,詳情:

16:44:27,338 ERROR [HttpModelLoader] sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 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(Unknown Source) at sun.security.ssl.SSLSocketImpl.fatal(Unknown Source) at sun.security.ssl.Handshaker.fatalSE(Unknown Source) at sun.security.ssl.Handshaker.fatalSE(Unknown Source) at sun.security.ssl.ClientHandshaker.serverCertificate(Unknown Source) at sun.security.ssl.ClientHandshaker.processMessage(Unknown Source) at sun.security.ssl.Handshaker.processLoop(Unknown Source) at sun.security.ssl.Handshaker.process_record(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 org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:394) at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:353) at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:141) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:353) at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:380) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82) at com.defonds.als.gui.model.HttpModelLoader.requestByHttpClient4(HttpModelLoader.java:172) at com.defonds.als.gui.model.HttpModelLoader.doBusinessAction(HttpModelLoader.java:94) at com.defonds.als.gui.model.EJBModelLoader.doBusinessEvent(EJBModelLoader.java:69) at com.defonds.als.gui.model.ModelLoader.doBusiness(ModelLoader.java:193) at com.defonds.als.gui.model.ModelLoader.doBusiness(ModelLoader.java:96) at com.defonds.als.gui.model.BusinessEventHelper.callBizService(BusinessEventHelper.java:302) at com.defonds.als.gui.model.BusinessEventHelper.doBusiness(BusinessEventHelper.java:186) at com.defonds.als.gui.model.BusinessEventHelper.doBusiness(BusinessEventHelper.java:166) at com.defonds.als.gui.model.BusinessEventHelper.doBusiness(BusinessEventHelper.java:160) at com.defonds.als.gui.model.BusinessEventHelper.doBusiness(BusinessEventHelper.java:155) at com.defonds.als.gui.model.BusinessEventHelper.doBusiness(BusinessEventHelper.java:363) at com.defonds.als.gui.base.Login.init(Login.java:188) at com.defonds.als.gui.base.Login.(Login.java:122) at com.defonds.als.gui.base.Login.getInstance(Login.java:130) at com.defonds.als.Startup.(Startup.java:114) at com.defonds.als.Startup.main(Startup.java:287) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at jbolt.deployment.smartyclient.JBoltSmartyClientStartup.main(JBoltSmartyClientStartup.java:121) Caused by: 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.validator.PKIXValidator.doBuild(Unknown Source) at sun.security.validator.PKIXValidator.engineValidate(Unknown Source) at sun.security.validator.Validator.validate(Unknown Source) at sun.security.ssl.X509TrustManagerImpl.validate(Unknown Source) at sun.security.ssl.X509TrustManagerImpl.checkTrusted(Unknown Source) at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(Unknown Source) ... 40 more Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(Unknown Source) at java.security.cert.CertPathBuilder.build(Unknown Source) ... 46 more
必須重啟 JVM,然後才可以訪問新的服務。那麼有沒有辦法在不重啟 JVM 的情況下重新載入新的證書並訪問新的服務? 解決辦法是自行建立一個 SSLSocket 工廠,並在訪問新服務之前將其賦給 HttpsURLConnection 物件。就這麼簡單。示例程式碼如下:
		URL localURL = new URL(urlPath);
		URLConnection connection = localURL.openConnection();
		HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
		if (connection instanceof HttpsURLConnection) {
			SSLSocketFactory sslSocketFactory = getSSLFactoryInstance();
		    ((HttpsURLConnection) httpURLConnection).setSSLSocketFactory(sslSocketFactory);
		}
		httpURLConnection.setDoOutput(true);
		httpURLConnection.setRequestMethod("POST");
		httpURLConnection.setRequestProperty("Content-Type", "application/octet-stream");
		httpURLConnection.setRequestProperty("Accept-Encoding", "chunck");
		httpURLConnection.setConnectTimeout(3000);
		//httpURLConnection.setReadTimeout(18000);
		
		outputStream = httpURLConnection.getOutputStream();
		baos = new ByteArrayOutputStream(8196);
		ObjectOutputStream oos = new ObjectOutputStream(baos);
		oos.writeObject(e);
		oos.flush();
		oos.close();
		outputStream.write(baos.toByteArray());
		outputStream.flush();
		
		if (HttpStatus.SC_OK == httpURLConnection.getResponseCode()) {

呼叫到的 getSSLFactoryInstance 方法:

    private static SSLSocketFactory selfSSLSocketFactoryInstance = null;
    
    private static synchronized SSLSocketFactory getSSLFactoryInstance() throws ALSAppException {
    	if (null == selfSSLSocketFactoryInstance) {
    		KeyStore keyStore = null;
    		TrustManagerFactory tmf = null;
    		SSLContext ctx = null;
    		InputStream is = null;
    		String strJnlpHome = System.getProperty("jnlpx.home");
    		try {
    			keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    			if (StringUtil.isNotEmpty(strJnlpHome)) {
    				is = fullStream(strJnlpHome + "/../lib/security/cacerts");
    			}
    			keyStore.load(is, "changeit".toCharArray());
    			tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        	    tmf.init(keyStore);
        	    ctx = SSLContext.getInstance("TLSv1");
        	    ctx.init(null, tmf.getTrustManagers(), null);
        	    selfSSLSocketFactoryInstance = ctx.getSocketFactory();
    		} catch (Exception e) {
        	    throw new ALSAppException(ExceptionKey.RUN_TIME_EXCEPTION, e);
      	  }
    	}
    	return selfSSLSocketFactoryInstance;
    }

呼叫到的 fullStream 方法:

    private static InputStream fullStream(String fname) throws IOException {
        FileInputStream fis = new FileInputStream(fname);
        DataInputStream dis = new DataInputStream(fis);
        byte[] bytes = new byte[dis.available()];
        dis.readFully(bytes);
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        return bais;
    }

還有一種解決方案是實現 X509TrustManager 介面,以下為網際網路上流行的一種寫法:

TrustManager tm = new X509TrustManager() {
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }

    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
};

但是很明顯以上寫法忽略了任何 SSL 證書校驗,雖然走的還是 https 協議即密文傳輸,但是在中間人劫持面前失去了 SSL 應有的保護作用,所以還是要謹慎使用。起碼的 server 端校驗、證書域名校驗還是要有的,不能偷懶。

參考資料