2015年3月19日 星期四

用JSP伺服器瀏覽遠端網站 HttpURLConnection


用JSP伺服器瀏覽遠端網站 HttpURLConnection

伺服器之間的溝通一直以來有很多的方法,像是使用SOAP來連結 web services(例如在 JSP 伺服器上加裝Axis server),但最簡單常用的還是使用 http(s) 通訊協定直接溝通,溝通的內容則可以是網頁、XML封包格式或是任意相互約定的字串形式,此處的範例,等於把JSP伺服器當作一個瀏覽器,直接瀏覽遠端的網站。

這個範例主要示範如何使用Java的HttpURLConnection元件來瀏覽遠端網站並同時上傳資料(Form 的 Post 功能),並說明遇到SSL加密傳輸狀況時,如何處理憑證的安全問題。

把JSP伺服器當作瀏覽器 HttpURLConnection

下面這個函式,可以直接從JSP伺服器上使用http(s)通訊協定,透過網路傳送一段文字到另外一個網頁伺服器(遠端不一定是JSP,也可以是ASP、PHP等),使用時須傳入遠端網址,決定是否要同時上傳資料(Form 的 Post 功能),以及是否要將從遠端抓回來網頁存檔或是存入變數中直接應用,須特別注意的,此函式將https加密通訊協定的憑證不論是否安全皆視為安全,正式專案不會這樣做,解決方法可參考本文後面的段落。

從JSP伺服器上透過網路傳送一段文字到另外一個遠端伺服器 /Module/Jf/UrlConnect.jsp

<%!
// 程式碼來源 : http://playjsp.blogspot.com ,作者:Steven,不限使用請註明出處。
/*
呼叫方法範例

//配置要上傳之內容的存放記憶體空間
StringBuffer postBuffer = new StringBuffer();
//配置傳回結果內容的存放記憶體空間
StringBuffer resultBuffer = new StringBuffer();
//上傳之內容,若不須上傳,將這段刪除即可
postBuffer.append(
java.net.URLEncoder.encode("a1", "UTF-8") + "=" + java.net.URLEncoder.encode("a1_value", "UTF-8")
+ "&" + java.net.URLEncoder.encode("&a2", "UTF-8") + "=" + java.net.URLEncoder.encode("a2_value", "UTF-8")
);
//瀏覽遠端網頁
UrlConnect(postBuffer, "https://www.yahoo.com", resultBuffer, "");
//印出傳回結果內容來看
out.print(resultBuffer.toString());
*/
//傳入的參數
//postBuffer : 上傳之內容,若為 null 或是記憶體長度為 0 , 則使用 GET 連線,反之使用 POST 連線
//urlAddress : 遠端網址
//resultBuffer : 傳回結果之內容不存檔時,就寫入此記憶區
//saveToFileName : 檔案路徑名稱,將傳回結果存檔,若傳入空字串則不存檔並將回傳結果寫入 resultBuffer 記憶區中

public void UrlConnect(StringBuffer postBuffer, String urlAddress, StringBuffer resultBuffer, String saveToFileName) {

//儲存遠端連結傳回內容的記憶體空間(將他清空)
if (resultBuffer.length() != 0) resultBuffer.setLength(0);

//如果遠端連結是 https 加密連線, 則強迫信任該網域憑證以免連結失敗,
//如此可省略將該網域憑證匯入伺服器上的 Java 信任憑證清單中
//正式專案如此使用會有安全虞慮,請斟酌使用
if ((urlAddress.length() > 6) && (urlAddress.substring(0,6).equals("https:"))) {

javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[]{

new javax.net.ssl.X509TrustManager() {

public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public boolean isServerTrusted(java.security.cert.X509Certificate[] certs) {
return true;
}
public boolean isClientTrusted(java.security.cert.X509Certificate[] certs) {
return true;
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException {
return;
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) throws java.security.cert.CertificateException {
return;
}

}

};

javax.net.ssl.HostnameVerifier hv = new javax.net.ssl.HostnameVerifier() {

public boolean verify(String urlHostName, javax.net.ssl.SSLSession session) {

return true; //System.out.println("Warning: URL Host: " + urlHostName + " vs. " + session.getPeerHost()); 兩個如果不同原應該出錯

}

};

try {

javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);//sc.init(null, trustAllCerts, new java.security.SecureRandom());
javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(hv);

} catch (Exception e) {

resultBuffer.append("[CA:"+e+"]");

}

}


try {

//呼叫 java 的 URL 連線元件
java.net.URL url = new java.net.URL(urlAddress);
java.net.HttpURLConnection urlconnection = (java.net.HttpURLConnection)url.openConnection();
//需要上傳資料就填入true,否則填入false
urlconnection.setDoInput(true);
//不要使用暫存功能
urlconnection.setUseCaches(false);
//設定連結逾時長短(單位是千分之一秒)
urlconnection.setConnectTimeout(5000);

//決定遠端連線使用 POST 或是 GET
if ((postBuffer != null) && (postBuffer.length() != 0)) {

urlconnection.setRequestMethod("POST");
urlconnection.setDoOutput(true);
char[] chars = (postBuffer.toString()).toCharArray(); //POST DATA
java.io.OutputStreamWriter osw = null;//write post data
try {

osw = new java.io.OutputStreamWriter(urlconnection.getOutputStream());
osw.write(chars, 0, chars.length);
osw.flush();

} catch (Exception e) {

resultBuffer.append("[POST:"+e+"]");

} finally {

if (osw != null) {

osw.close();

}

}

} else {

urlconnection.setRequestMethod("GET");

}

//不須存檔,將遠端傳回內容寫入 resultBuffer 記憶區中
if (saveToFileName.equals("")) {

java.io.BufferedReader bufferedreader = null;
try {

bufferedreader = new java.io.BufferedReader(new java.io.InputStreamReader(urlconnection.getInputStream()));
for(String s1 = null; (s1 = bufferedreader.readLine()) != null;) resultBuffer.append(s1 + "\n");

} catch (Exception e) {

resultBuffer.append("[HTML:"+e+"]");

} finally {

if (bufferedreader != null) {

bufferedreader.close();//bufferedreader = null;

}

}

//須存檔,將遠端傳回內容存檔
} else {

java.io.InputStream is = null;
int len;
byte[] buf = new byte[1024];
java.io.File TempFile = null;
java.io.FileOutputStream fos = null;
try {//save return file

is = urlconnection.getInputStream();
if ((len = is.read(buf)) > 0) {

if ((new java.io.File(saveToFileName)).exists()) (new java.io.File(saveToFileName)).delete();
TempFile = new java.io.File(saveToFileName);
try {

fos = new java.io.FileOutputStream(TempFile);
fos.write(buf, 0, len);
while ((len = is.read(buf)) > 0) fos.write(buf, 0, len);
fos.flush();

} catch (Exception e) {

resultBuffer.append("[FILE WRITE:"+e+"]");

} finally {

if (fos != null) {

fos.close();

}

}

}

} catch (Exception e) {

resultBuffer.append("[FILE READ:"+e+"]");

} finally {

if (is != null) {

is.close();

}

}

}

} catch(java.net.SocketTimeoutException sockettimeoutexception) {

resultBuffer.append("[time out:"+sockettimeoutexception+"]");

} catch(java.net.MalformedURLException malformedurlexception) {

resultBuffer.append("[URL format:"+malformedurlexception+"]");

} catch(java.io.IOException ioexception) {

resultBuffer.append("[URL io:"+ioexception+"]");

} catch(Exception e) {

resultBuffer.append("[except:"+e+"]");

}

}
%>

呼叫範例如下UrlConnectSample.jsp,內容正如上式開頭的呼叫範例說明,至於呼叫UrlConnect時傳入的參數說明則如下示:

postBuffer : 上傳之內容,若為 null 或是記憶體長度為 0 , 則使用 GET 連線,反之使用 POST 連線
urlAddress : 遠端網址
resultBuffer : 傳回結果之內容不存檔時,就寫入此記憶區
saveToFileName : 檔案路徑名稱,將傳回結果存檔,若傳入空字串則不存檔並將回傳結果寫入 resultBuffer 記憶區中

呼叫範例 UrlConnectSample.jsp

<%@ include file="/Module/Jf/UrlConnect.jsp" %><%

//配置要上傳之內容的存放記憶體空間
StringBuffer postBuffer = new StringBuffer();

//配置傳回結果內容的存放記憶體空間
StringBuffer resultBuffer = new StringBuffer();

//上傳之內容,若不須上傳,將這段刪除即可
postBuffer.append(
java.net.URLEncoder.encode("a1", "UTF-8") + "=" + java.net.URLEncoder.encode("a1_value", "UTF-8")
+ "&" + java.net.URLEncoder.encode("&a2", "UTF-8") + "=" + java.net.URLEncoder.encode("a2_value", "UTF-8")
);

//瀏覽遠端網頁
UrlConnect(postBuffer, "https://www.yahoo.com", resultBuffer, "");

//印出傳回結果內容來看
out.print(resultBuffer.toString());

%>

執行結果很有趣,如下圖,瀏覽器網址為127.0.0.1時,卻出現yahoo的首頁,如果這時候,Client端是Windows的話,只要將例如C:\Windows\System32\drivers\etc\hosts檔案中加入一條 127.0.0.1 yahoo.com.tw,就可以在瀏覽器輸入網址yahoo.com.tw,此時當然連到的是假的yahoo首頁,若是Mac作業系統,則可以到 /private/etc/hosts檔案修改網域名稱對應的IP位址,程式開發者帶常常會利用這樣的做法來增加系統開發的便利性,但此法也常被駭客當作竊取個資的手法之一。

事先將SSL連線時的遠端憑證(公鑰)匯入Java信任憑證清單

上面的函式UrlConnect等於在SSL連線時強迫信任遠端憑證,正式專案會將if ((urlAddress.length() > 6) && (urlAddress.substring(0,6).equals("https:"))) {......}那一段程式碼移除,如此一來,只要加密連線https,此函式必定會出錯誤訊息,解決方法很簡單,我們既然將伺服器當作瀏覽器用,所以只要將該網站的公鑰憑證匯入伺服器上的JRE 憑證信任清單 cacerts檔案中即可,範例如下:

1.先使用 IE 連到 https://xxx.xxx.xxx (遠端網址)
2.滑鼠右鍵按視窗右下角的鑰鎖圖案兩下
3.選擇[匯入憑證]選項
4. 再使用 IE是窗指令[工具 \ 網際網路選項 \ 內容 \ 憑證] 找到該網域名稱的憑證,匯出成 DER 編碼的 .cer 檔案
5.在JSP伺服器上使用命令提示字元,匯入憑證到 Java 之 JRE 憑證信任清單 cacerts檔案中,範例如下,jdk與jre路徑請自行決定。
"C:\Program Files\Java\jdk\bin\keytool" -import -alias tcbca -file "C:\Program Files\Tomcat\webapps\ROOT\ca\tcbbank.cer" -keystore "C:\Program Files\Java\jdk\jre\lib\security\cacerts" -storepass changeit -trustcacerts

之所以我們先用IE來下載和匯出憑證,是因為目前Java尚未開放程式碼來匯出https一開始連線時所收到遠端伺服器的憑證公鑰,我們這裡用台灣合庫銀行作範例,Windows系統大概大家都很容易依照上面的方法自行完成此步驟,此處就使用越來越多人愛用的 Mac 作業系統(等同Linux作業系統)配合 Chrome 瀏覽器做一次吧,道理和上述完全相同:

如上圖,在Chrome瀏覽器中輸入網址 https://ars.tcb-bank.com.tw/school/Page/Main.htm,用滑鼠按一下網址前方出現的鑰鎖圖案,再按一下[憑證資訊]超連結。

如上圖,可以看到ars.tcb-bank.com.tw這個網域的https連線加密憑證,是經由TWCA加簽出來的,一般企業通常不會自己簽自己的憑證,因為自己沒有公信力,簽出來的憑證也沒有公信力,跟憑證不加簽是一樣的沒公信力,一般瀏覽器連到這些沒有公信力的網站,一定會跳出錯誤信息詢問使用者是否要信任該憑證,像是近年來很熱門的GoDaddy,就可以完全透過網路快速幫企業加簽憑證,只要網路刷卡即可支付加簽費用,使用起來非常快速方便,所以我們只要將TWCA用來加簽別人的憑證公鑰,匯入JRE的信任憑證清單 cacerts檔案即可。

如上圖,按一下Chrome瀏覽器右上方那個三條線的設定鈕,再挑選[設定]選單,在出現的畫面中按下[顯示進階設定...]超連結。

如上圖,找到[管理憑證]按鈕,按下後出現視窗如下圖。

此時只要將所有找到的TWCA的憑證,全都利用滑鼠右鍵將他們匯出成.cer檔案,再利用前述第5小步驟的方法,使用命令提示字元(Linux和Mac上叫做終端機之類的),匯入憑證到 Java 之 JRE 憑證信任清單 cacerts檔案中即可,同樣的jdk與jre路徑請自行決定。

 

 

沒有留言:

張貼留言