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路徑請自行決定。

 

 

2015年3月17日 星期二

資料庫取流水號


資料庫取流水號

取流水號在大量存取的專案上,一值是個令很多人頭痛的問題,不是怕取到重複的號碼,就是怕資料庫鎖定造成效率變差,事實上,只要了解不同方法的使用特性和限制,就不用怕不小心用錯方法,以下介紹幾個常用的方法,並舉例說明他們該何時使用。

將Table欄位設定為自動流水號

大部分的資料庫都提供將Table欄位設定為自動流水號,以下是MSSQL的範例語法,在建立Table的時候,將流水號的欄位Sell_Id設定為IDENTITY(1,1),代表存入資料時會從1開始自動取號,每次累加1。

CREATE TABLE [dbo].[Ticket_Sell_Info](
[Sell_Id] [int] IDENTITY(1,1) NOT NULL,
[Ticket_Price] [numeric] (18,6)NOT NULL,
[Maintain_Date] [datetime] NOT NULL
CONSTRAINT [PK_Ticket_Sell_Info] PRIMARY KEY CLUSTERED
(
[Sell_Id] ASC
)
) ON [PRIMARY]

因為流水號欄位的內容是遊資料庫自動產生,所以在存資料進入此Table時,不須也不能指定流水號欄位Sell_Id的資料,範例如下:

String SQL = "INSERT INTO Ticket_Sell_Info (Ticket_Price,Maintain_Date) VALUES (100,getdate()) ";

statement.executeUpdate(SQL); //statement 宣告此處為簡化範例故省略

這樣的做法很方便,理論上不用擔心會取到重複的流水號,但是很多資料庫管理師會視狀況不使用此方法,主要在於這樣的設計,流水號欄位只能讀取且無法寫入或修改,會讓資料庫後續管理在某些狀況下較為不便,例如將此筆資料移到另外一個Table再移回來,流水號就會因為再次重新取號而變了。

使用SQL語法MAX+1取流水號

假設上面的Ticket_Sell_Info在建立時,Sell_Id欄位不加上IDENTITY(1,1)的描述,則新增一筆資料時,因為該欄位指定 NOT NULL屬性(且同時為PrimaryKey欄位),代表Sell_Id欄位一定要存一些東西進去,且存入的資料不能和已存在Table中現有的資料重複,我們常常會使用下面的方法取號。

String SQL = "INSERT INTO Ticket_Sell_Info (Sell_Id,Ticket_Price,Maintain_Date) SELECT ISNULL(MAX(Sell_Id)+1,1), 100,getdate() FROM Ticket_Sell_Info";

statement.executeUpdate(SQL); //statement 宣告此處為簡化範例故省略

這樣的MAX+1流水號取法也相當方便,其中ISNULL函式只是為了讓資料庫中還沒有任何資料時,也就是第一筆資料產生時寫入1,以免寫入null值而產生錯誤,一般常用在系統設定上,主要是因為系統設定不會有大量人員同時執行寫入的動作,但是如過是個售票系統,同一時間只要有超過數百人同時買票,將因資料庫硬體效能與多功能力,導致多筆寫入的資料抓到相同的流水號,此時只有第一個寫入的會成功,其他流水號重複寫入的皆會失敗。

使用Transaction啟動Lock功能取流水號

如果前面使用MAX+1的方法,在取號前先用Transaction啟動Lock功能來確保同一時間只有一個執行緒可以取號,這樣的方法看似可行,但如果同時間數百人或是千人以上同時取號,每個取號都要排隊,資料庫不僅會塞車還可能因為塞太多導致記憶體不足等各種原因而掛掉,當這個Table裡的資料量過多例如上百萬筆時,狀況更是雪上加霜,附帶一提,通常我們會設計一個系統假設預計可以使用50年的話,會同時設計Table內的資料量50年後不會超過100萬筆,否則可以動態產生新的Table來儲存,例如動態依不同年度產生Ticket_Sell_Info_[年度]之類的Table,或甚至每場售票就動態產生一個的Table來儲存,例如Ticket_Sell_Info_[場次流水號]。

如果要使用Transaction的lock功能來取號,會建議另外產生一個通常只有一筆(或少數幾筆)資料的Table來儲存目前的流水號取到第幾號,因為Table中只有一筆資料,所以取號相當快速,較不擔心數百人同時取號會造成塞車,取出號碼之後,再將取到的號碼存入前面的Ticket_Sell_Info表單中。

儲存目前流水號取到第幾號的Table建立範例如下,建立完Table就先存入一筆資料Current_Sell_Id代表目前流水號取到第0號:

CREATE TABLE [dbo].[Current_Id_Info](
[Current_Sell_Id] [int] NOT NULL,
CONSTRAINT [PK_Current_Id_Info] PRIMARY KEY CLUSTERED
(
[Current_Sell_Id] ASC
)
) ON [PRIMARY]

INSERT INTO Current_Id_Info (Current_Sell_Id) VALUES (0)

用Transaction啟動Lock功能從Current_Id_Info表單中取號,並將取到的流水號儲存到Ticket_Sell_Info表單的範例如下,範例中,我們將針對Current_Id_Info表單更新流水號(流水號+1)與取號的SQL語法放在同一個連線交易中完成,如此才能千百倍的減少Table被鎖定的時間:

<%@ page import="java.sql.*" %>
<%

String SQL = "";
String Current_Sell_Id = "0";
try {

//取消自動commit,則接下來存取的Table會被上鎖並獨佔存取,其他人的存取必須排隊,直到恢復自動commit為止
connection.setAutoCommit(false); //connection 宣告此處為簡化範例故省略

//以下兩個語法合併在同一個連線交易中完成,才能減少Table被鎖定的時間
SQL="UPDATE Current_Id_Info SET Current_Sell_Id = Current_Sell_Id + 1; "
+"SELECT Current_Sell_Id FROM Current_Id_Info"
;
statement.execute(SQL); //statement 宣告此處為簡化範例故省略
statement.getMoreResults();
ResultSet rs = statement.getResultSet();
if (rs.next()) {

Current_Sell_Id = rs.getString("Current_Sell_Id");

}

} catch (Exception e) {

out.print("ERROR"+e);

} finally {

connection.setAutoCommit(true);

}

//取到流水號且解除Table鎖定之後,可以慢慢的拿來任意使用了
String SQL = "INSERT INTO Ticket_Sell_Info (Sell_Id,Ticket_Price,Maintain_Date) VALUES ("+Current_Sell_Id+", 100,getdate())";
statement.executeUpdate(SQL); //statement 宣告此處為簡化範例故省略

%>

 

使用JAVA產生UID當作流水號

如果仍嫌上面使用Transaction啟動Lock功能取流水號的方法不夠快速,同時成千上萬人存取有造成資料庫鎖死之疑慮,則你可以考慮直接用JAVA產生一組行遍全球皆不會重複的UID長字串當作流水號,等於放棄使用資料庫取流水號。

<%@ page import="java.sql.*" %>
<%

java.util.UUID uuid = java.util.UUID.randomUUID();
String Current_Sell_Id = uuid.toString();

//使用Java取到流水號之後再拿來存入資料庫
String SQL = "INSERT INTO Ticket_Sell_Info (Sell_Id,Ticket_Price,Maintain_Date) VALUES ('"+Current_Sell_Id+"', 100,getdate())";
statement.executeUpdate(SQL); //statement 宣告此處為簡化範例故省略

%>

產生的UID範例如36a00559-550b-4672-aad0-67ff238f9c3e,長度有36個字,因為是長字串流水號,所以前面建立Ticket_Sell_Info表單時,Sell_Id要改成字串欄位如下:

CREATE TABLE [dbo].[Ticket_Sell_Info](
[Sell_Id] [char](36) NOT NULL,
[Ticket_Price] [numeric] (18,6)NOT NULL,
[Maintain_Date] [datetime] NOT NULL
CONSTRAINT [PK_Ticket_Sell_Info] PRIMARY KEY CLUSTERED
(
[Sell_Id] ASC
)
) ON [PRIMARY]

使用這個方法的好處,除了不用浪費資料庫的資源來取流水號,理論上連不同企業之間的資料交換,也不會產生流水號重複的問題,另外,使用此方法產生的流水號因為是一個相當長的字串,所以網路上可以Google一堆縮短流水號長度的演算方法,但如果你沒有確定的把握和對該方法深入的了解,建議儘量不要使用那些方法,舉例來說,筆者就曾經使用了其中某些方法而在微調JSP伺服器時間後發生重複取號的窘境,Java還有其他產生UID的方法,像是使用java.rmi.server等方法都不建議使用,這些較短的UID都可能在某些特定狀況下造成流水號重複。

2015年3月13日 星期五

JSP伺服器之間溝通時使用公私鑰進行加密或確認彼此身份


JSP伺服器之間溝通時使用公私鑰進行加密或確認彼此身份

產生一組公私鑰

JSP伺服器之間相互溝通時使用公私鑰加密或認證相當容易,在這個範例中,我們假設有兩台伺服器A和B,則需要建立兩組金鑰,一組給A伺服器,一組給B伺服器,每一組金鑰都包含一支公鑰和一支私鑰,A伺服器的私鑰自己保管不能外流,公鑰則複製一份給B伺服器,同樣的B伺服器的私鑰自己保管不能外流,公鑰則複製一份給A伺服器。

發話的時候使用對方的公鑰加密,收話的時候使用自已的私鑰解密

當B伺服器要對A伺服器發話進行溝通時,發話的內容在B伺服器上可全部或是重點欄位(以便加速)使用A伺服器的公鑰來加密,加密的內容從B伺服器傳送到A伺服器時,將會在A伺服器上使用A伺服器的私鑰來解密,所以任何伺服器如果沒有A伺服器的公鑰,就沒辦法對A伺服器發話,且若A伺服器上沒有A伺服器的私鑰就無法解密這些傳話內容。

同樣的道理,A伺服器須使用B伺服器的公鑰才能對B伺服器發話,B伺服器須使用B伺服器的私鑰來解密收到的通話內容,如此即可加密彼此的溝通,進一步互相驗證彼此,防止溝通的內容在網路上被竊聽。

綜合以上可以得到一個結論,就是發話的時候使用對方的公鑰加密,收話的時候使用自已的私鑰解密。

建立公私鑰的網頁範例

將以下兩支JSP程式碼各放在A和B兩個JSP伺服器的例如Module/Ca路徑下,所以如果您在Linux上使用Tomcat作為JSP應用程式伺服器,則完整的路徑例如為/usr/Tomcat/webapps/ROOT/Module/Ca。

在此範例中,我們須先在AB伺服器上均建立路徑/usr/cert,來存放產生的公私鑰,為了簡化程式碼,我們將下面的函式keyPairGen()獨立出來,以便用來產生一組公私鑰。

產生一組公私鑰的函式 /Module/Ca/keyPairGen.jsp

<%!
// 程式碼來源 : http://playjsp.blogspot.com ,作者:Steven,不限使用請註明出處。
// 提供公私鑰存放路徑、驗算法種類與公私鑰記憶體大小來產生一組公私鑰
public boolean keyPairGen(String Pub_File_Name_In, String Pri_File_Name_In, String Algorithm_In, int keySize){

try {

//產生公私鑰
java.security.KeyPairGenerator kpg = java.security.KeyPairGenerator.getInstance(Algorithm_In);
kpg.initialize(keySize);
java.security.KeyPair kp = kpg.generateKeyPair();
java.security.PublicKey pubk = kp.getPublic();
java.security.PrivateKey prik = kp.getPrivate();

//將公鑰存檔
java.io.FileOutputStream fos_pub = new java.io.FileOutputStream(Pub_File_Name_In);
fos_pub.write(pubk.getEncoded());
fos_pub.flush();
fos_pub.close();

//將私鑰存檔
java.io.FileOutputStream fos_pri = new java.io.FileOutputStream(Pri_File_Name_In);
fos_pri.write(prik.getEncoded());
fos_pri.flush();
fos_pri.close();

//} catch (NoSuchAlgorithmException exc) {
//} catch (java.io.IOException e) {
} catch (Exception e) {

out.print(e);
return false;

}
return true;

}
%>

而下面的GenkeyExample.jsp是一個JSP網頁,直接呼叫上面的方法來產生一組公私鑰。

呼叫範例網頁 /Module/Ca/GenkeyExample.jsp

<%@ page language="java" contentType="text/html" %>
<%@ include file="/Module/Ca/keyPairGen.jsp" %>
<%

// 程式碼來源 : http://playjsp.blogspot.com ,作者:Steven,不限使用請註明出處。
//檔名如果是中文,請自行處理編碼轉換
String File_Name = request.getParameter("File_Name");

//公私鑰的演算法,其他也可以輸入像是NONE/PKCS1PADDING/EC DSA,
//RSA, MD5 or SHA-1 ,
//DiffieHellman (1024) DSA (1024) RSA (1024, 2048) 等等
String Algorithm_In = "RSA";

//公私鑰加密長度,一般1024即可,數值越大越安全,但運算速度會慢些
int keySize = 2048;

//公私鑰檔案存放路徑如果不存在的話,就先建立該路徑
String Pub_File_Name_In = "/usr/cert/pubKey_"+File_Name+".key";
String Pri_File_Name_In = "/usr/cert/priKey_"+File_Name+".key";
java.io.File file1 = new java.io.File("/usr/cert");
if (!file1.exists()) file1.mkdirs();

//產生公私鑰檔案
if (!(new java.io.File(Pub_File_Name_In).exists())) {

keyPairGen(Pub_File_Name_In, Pri_File_Name_In, Algorithm_In, keySize);

}

%>

則在A伺服器上使用瀏覽器執行例如http://127.0.0.1/Module/Ca/GenkeyExample.jsp?File_Name=A,就可以產生A伺服器的公私鑰共兩個檔案,分別為公鑰/usr/cert/pubKey_A.key與私鑰/usr/cert/priKey_A.key。

同樣的在B伺服器上使用瀏覽器執行例如http://127.0.0.1/Module/Ca/GenkeyExample.jsp?File_Name=B,就可以產生B伺服器的公私鑰共兩個檔案,分別為公鑰/usr/cert/pubKey_B.key與私鑰/usr/cert/priKey_B.key。

將A伺服器的公鑰/usr/cert/pubKey_A.key複製一份到B伺服器的/usr/cer路徑下,私鑰不必也不應複製過去。
將B伺服器的公鑰/usr/cert/pubKey_B.key複製一份到A伺服器上/usr/cer路徑下,私鑰不必也不應複製過去。

(你也可以在同一台伺服器上產生所有伺服器的公私鑰,再依上面的範例來佈署公私鑰)

A伺服器對B伺服器發話加密範例

接著,我們要在A伺服器上,使用B伺服器的公鑰來加密一段文字,然後轉成Base64編碼才能透過網路傳送到B伺服器上,為了簡化程式碼,我們將此動作拆成幾個函式來完成如下:

keyReadByte() 讀出鑰匙檔案的內容。
keyEncrypt2Base64() 利用鑰匙內容將一段文字加密並轉成Base64格式。
UrlConnect() 從JSP伺服器上透過網路傳送一段文字到另外一個伺服器。

讀出鑰匙檔案的內容 /Module/Ca/keyReadByte.jsp

<%!
// 程式碼來源 : http://playjsp.blogspot.com ,作者:Steven,不限使用請註明出處。
// 傳入鑰匙檔案路徑,傳回鑰匙檔案內容
public byte[] keyReadByte(String File_Name_In){

byte[] fileBytes = null;
try{

java.io.File keyFile = new java.io.File(File_Name_In);
java.io.FileInputStream fis = new java.io.FileInputStream(keyFile);
java.io.DataInputStream dis = new java.io.DataInputStream(fis);
fileBytes = new byte[(int)keyFile.length()];
dis.readFully(fileBytes);

} catch (Exception e) {

return null;//out.print(e);

}
return fileBytes;

}
%>

 

利用鑰匙內容將一段文字加密並轉成Base64格式 /Module/Ca/keyEncrypt2Base64.jsp

<%!
// 程式碼來源 : http://playjsp.blogspot.com ,作者:Steven,不限使用請註明出處。
//利用鑰匙檔案將一段文字加密並轉成Base64格式
//傳入要加密的文字、鑰匙檔案內容與演算法種類,傳回被加密的文字
public String keyEncrypt2Base64(String Str_In, byte[] publicfile_Bytes, String Algorithm_In){

String base64str = "";
try {

//transfer byte into key
java.security.KeyFactory kf = java.security.KeyFactory.getInstance(Algorithm_In);
java.security.PublicKey pk = kf.generatePublic(new java.security.spec.X509EncodedKeySpec(publicfile_Bytes));

//加密 cipher 並轉成 Base64
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(Algorithm_In);
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, pk);
base64str = new sun.misc.BASE64Encoder().encodeBuffer(cipher.doFinal(Str_In.getBytes()));

} catch(Exception e) {

return "";

}
return base64str;

}
%>

 

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

請到以下網址 http://playjsp.blogspot.com/2015/03/jsp-httpurlconnection.html 頁面搜尋[/Module/Jf/UrlConnect.jsp]字串,並複製其程式碼。

好了,現在可以一次呼叫上面三個函式,從A伺服器傳送一段加密文字到B伺服器了,範例網頁如下,只要在A伺服上的瀏覽器,執行例如http://127.0.0.1/Module/Ca/SendExample.jsp即可,當然B伺服器上接收的程式要先安置好(參考本文下一段),此處網頁的執行才不會出錯。

傳送範例網頁 /Module/Ca/SendExample.jsp

<%@ page language="java" import="java.io.*" contentType="text/html" %>
<%@ include file="/Module/Ca/keyReadByte.jsp" %>
<%@ include file="/Module/Ca/keyEncrypt2Base64.jsp" %>
<%@ include file="/Module/Jf/UrlConnect.jsp" %>
<%

//B伺服器的公鑰檔案路徑
String Pub_File_Name = "/usr/cert/pubKey_B.key";
//抓現在哦系統時間當作等等要加密的內容
String accessKey = Long.toString(System.currentTimeMillis());
//將上面的 accessKey 內容加密並轉成 Base64 格式
String base64encString = keyEncrypt2Base64(accessKey, keyReadByte(Pub_File_Name), "RSA");

//用來存放傳回結果的記憶體
StringBuffer resultBuffer = new StringBuffer();

//要上傳的內容
StringBuffer postBuffer = new StringBuffer();
postBuffer.append(
//base64encString 是加密過的內容
java.net.URLEncoder.encode("accessKey", "UTF-8") + "=" + java.net.URLEncoder.encode(base64encString, "UTF-8")
//這個是沒有加密的內容
+ "&" + java.net.URLEncoder.encode("Server_Name", "UTF-8") + "=" + java.net.URLEncoder.encode("A", "UTF-8")
);

//將傳送內容印出來
out.print("<P>傳送給B伺服器的認證碼為["+accessKey+"]");
out.print("<P>加密後為["+base64encString+"]");

//開始傳送,下面 192.168.2.254 是B伺服器的網址
UrlConnect(postBuffer, "http://192.168.2.254/Module/Ca/AnswerExample.jsp", resultBuffer, "");

//將回傳內容印出來
out.print("<P>B伺服器的回覆 : " + resultBuffer.toString());

%>

B伺服器收話解密範例

同樣為了簡化程式,我們在此處使用 keyDecryptFromBase64() 函式來負責將收到的 Base64 格式字串解密如下:

將收到的 Base64 格式字串解密 /Module/Ca/keyDecryptFromBase64.jsp

<%!
// 程式碼來源 : http://playjsp.blogspot.com ,作者:Steven,不限使用請註明出處。
//利用鑰匙檔案將一段已經加密且轉成Base64格式的文字解密
public String keyDecryptFromBase64(String base64str, byte[] fileBytes_pri, String Algorithm_In){

String Str_Out = "";
try {

//由 Base64 格式轉回原先的格式
byte[] inpBytes = new sun.misc.BASE64Decoder().decodeBuffer(base64str);
//transfer byte into Private key
java.security.KeyFactory kf = java.security.KeyFactory.getInstance(Algorithm_In);
java.security.PrivateKey pk = kf.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(fileBytes_pri));
//用鑰匙解密
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(Algorithm_In);
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, pk);
Str_Out = new String(cipher.doFinal(inpBytes));

} catch( Exception e ){

return "";

}
return Str_Out;

}
%>

B伺服器收話解密的JSP網頁程式AnswerExample.jsp如下,範例中直接呼叫上面的函式,以便解密A伺服器送來的資料:

呼叫範立網頁 /Module/Ca/AnswerExample.jsp

<%@ page language="java" contentType="text/html" %>
<%@ include file="/Module/Ca/keyReadByte.jsp" %>
<%@ include file="/Module/Ca/keyDecryptFromBase64.jsp" %>
<%

//接收A傳來的內容,若是中文請自行轉碼
String accessKey = request.getParameter("accessKey");
String Server_Name = request.getParameter("Server_Name");

//B伺服器自己私鑰的存放路徑
String Pri_File_Name = "/usr/cert/priKey_B.key";

//使用B伺服器自己的私鑰解密
String accessKey_Ori = keyDecryptFromBase64(accessKey, keyReadByte(Pri_File_Name), "RSA");
String ResultStr = "remote server name is ["+Server_Name+"], accessKey is ["+accessKey_Ori+"]";

//至於 ResultStr 要不要先用 A 伺服器的公鑰加密再傳給 A,就看使用者自己的應用了,此處簡單略過
out.print(ResultStr);

%>

範例執行結果

前面 http://127.0.0.1/Module/Ca/SendExample.jsp 執行結果如下,相同的道理,B伺服器對A伺服器回話時,也要先使用A伺服器的公鑰來加密對話內容,A伺服器收到後使用A伺服器的私鑰來解密,此處範例簡單省略此步驟。