2017年12月19日 星期二

使用 Limbo x86 App 在 Android 上安裝 Windows


使用 Limbo x86 App 在 Android 上安裝 Windows
Run Windows on Android via Limbo x86

為什麼要在 Android 上安裝 Windows ?

當初筆者在 Android 上安裝 Windows,純粹只是想要在手機上執行 Adobe Dreamweaver 這套超好用的網頁編輯軟體,如此只要再搭配一個微型投影機,出外旅行時,就不用背著笨重的筆電到處跑,當然,這個願望很容易就達成了,所以才會產生這篇文章,但是因為 Windows 是採用 x86 或 x64 系列的 cpu,和一般使用 AMD 或 AMD64 cpu 的 Android 手機之間,存在太多的轉換,即使是少見的 X86 Android 手機,中間執行與轉換速度還是超級慢,所以筆者後續仍舊繼續使用 Android 上開源的 Turbo Edit 編輯器不算短的時間,而最後,則是採用開源的 Apache Orion 解決方案,利用手機上現成的瀏覽器充當網頁編輯器,來達到利用手機工作,在手機上撰寫網頁程式的目的。

選擇並安裝 Android上的 X86 虛擬機 - Limbo x86

動工前,在網路上爬了一堆國外的文章,已經知道要在 Android 上跑 Windows 作業系統,速度將會非常的慢,由於不願意安裝最輕量版但支援度過低的 Windows 95,所以筆者就從 Windows XP 開始玩起,方法很簡單,先在 Android 手機上的 Play Store 搜尋並安裝 Limbo x86 這套可以模擬 x86 電腦的虛擬機 App,之所以選他,主要還是因為他是由 QEMU 這套老牌的通用平台虛擬機改版過來,相信穩定度應該不錯,果然,穩定度超高,光是安裝 Windows XP 就花了我超過 8 個小時的時間,而且中途都沒有當機!看到 8 小時先不要氣餒,其實 Windows XP 在 Android 較為高端的手機中,不論是使用滑鼠還是鍵盤打字,都還算非常流暢,就是啟動程式與隨便切換一個視窗,常常要等個 10-20 分鐘而已!!! Windows 開機與關機的時間,當然也都超過 20 分鐘以上,附帶一題,筆者所使用的手機型號如下:
Sharp Z2 十核心 cpu, 4G RAM

Limbo設定畫面 - 以安裝 Windows XP 為範例

在 Play Store中找到 Limbo x86 ,完成安裝並開啟。


Limbo畫面如下,按下「Load Machine載入虛擬機」欄位「None」右方的向下箭頭選項。


選擇「New」來建立一台虛擬機。


替新的虛擬機取名字,此處使用「xp」,並按下「Create建立」按鈕,建立新的虛擬機。


虛擬機設定畫面如下圖,在開始設定之前,須說明的一點,就是不同的手機硬體配置,以下的設定不見得會完全相同,如果設定了一項手機硬體不支援的選項,通常就是虛擬機一開機就直接黑屏當掉回到此處的設定頁面,或是開機之後一直黑屏沒反應超過10分鐘以上,此時,使用者可以變換不同的設定試試看。
「Architecture架構」此處選用 x86,如果你想要安裝 64 位元的 Windows版本,且手機 cpu 有支援64位元,則可以選擇 X64。
「Machine Type架構」選用 pc。
「CPU Model中央處理器型號」選用 pentium,事實上,國外有的網站建議使用QEMU虛擬器提供的qemu32,但在筆者的手機上,只有選用 pentium的時候,開機才不會當掉,所以如果你設定完成後虛擬機無法順利開機,可能此處要試著切換不同的選項。
「CPU Core核心數量」選用1,同樣的,有國外網站建議使用4(如果你的手機有到達4顆CPU的話),但在筆者的手機上,只有選用1,虛擬機啟動的時候才不會當機。
「RAM Memory(MB)記憶體數量」選用480,這需要視你的手機有多少記憶體而定,雖然筆者的手機有 4G RAM,但選用超過480MB時,虛擬機啟動的時候必然當機。


「Storage \ Hard Disk A:第一個硬碟」先勾選此欄位左方的勾選紐,再點選右方的向下箭頭紐,利用「New」選項建立一個新的虛擬機檔案,並在出現的命名視窗中,為他取一個檔名例如「xp」,到時候我們會將 Windows XP 安裝在這個虛擬硬碟中,這個虛擬硬碟的檔案,會在手機內存的 limbo \ machines \xp 路徑下叫做 xp.qcow2。
「Removeable Storage \ CDROM光碟機」先勾選此欄位左方的勾選紐,再點選右方的向下箭頭紐,挑選 Windows XP安裝光碟的 iso 檔案,由於版權問題此處無法提供,但是讀者很容易在網路上 google 到 Windows XP的安裝序號,並下載可開機的中文或英文版 Windows XP 安裝光碟 iso 檔案,你可以先將 iso 檔案複製到手機上的外接記憶卡中方便此處挑選,如果你想從 Windows XP 安裝光碟直接轉成 iso 檔案來使用的話,記得製作 iso 檔案的時候,要包含進去開機資訊,否則轉出來的 iso 檔案,雖可以在 VBOX 這類的商用虛擬機中開機,但在此處是開不了的,如果你不知道你的 iso 檔案是否包含開機資訊,可以上網找一下例如 Winiso 這類的小軟體,他可以幫你檢查,或是替你將光碟片中的開機資訊,複製出來,存到你的 iso 檔案中。


「Graphics \ VGA Display螢幕顯示」選用標準「std」驅動程式即可。
「Audio \ Sound Card音效卡」可使用「sb16」驅動程式,但非常建議選用「None無音效卡」,這樣已經夠慢的虛擬機速度會快些,只是不會有聲音而已。
「NetWork\ Network網卡」可使用「User」選項,配合「Network Card網卡型號」欄位使用「ne2k_pci卡驅動程式」,但如果你用不到網路功能的話,非常建議「NetWork\ Network網卡」欄位直接選用「None無網卡」,這樣已經夠慢的虛擬機速度會快些,只是不會有網路而已。


「Boot Settings \ Boot from Device開機設備順序」可使用 Default,他會試著依序使用「Floppy磁碟」、「CD ROM光碟」與「Hard Disk硬碟」來開機,所以 Windows XP安裝完成後,可以將前面 CDROM 選項移除,或將此處改為「Hard Disk硬碟」來開機。
「User Interface \ User Interface 使用者界面」可選用「SDL」直接在手機螢幕顯示虛擬機桌面即可,此處另外一個選項「VNC」可讓使用者透過網路從遠端經由 VNC 客戶端軟體,連上來操控虛擬機的桌面,就是操控此處 Windows XP 的桌面。
「User Interface \ Orientation 螢幕方向」建議直接定死「Landscape橫式螢幕」,以免手機正拿橫拿的時候,畫面一直「橫式」與「直式」切來切去,而且當畫面直式顯示的時候,Windows XP 虛擬機的畫面依舊是橫式,就會變得畫面非常小,只佔據整個螢幕的上半部。
「User Interface \ Show Toolbar顯示鍵盤和滑鼠切換工具紐」請打勾,到時候會有一個鍵盤和一個滑鼠圖案在螢幕的最上方,點按鍵盤圖案,就可以啟用手機的螢幕鍵盤,並在虛擬機中打字,點按滑鼠圖案,就可以用螢幕當作「觸控板」,沒錯,是觸控板,也就是你在螢幕上滑動手指,虛擬機中滑鼠指標,會跟著手指的相對位置移動,而不是手指移動到那,滑鼠指標就出現到那,「滑鼠左鍵」就是手機上的「音量調大按鈕」,但是很不靈敏,「輕觸螢幕一下」,也是代表滑鼠左鍵,則非常靈敏好用,「滑鼠右鍵」就是手機上的「音量調小按鈕」,則非常靈敏。
「User Interface \ Fullscreen 全螢幕顯示」建議請打勾,看得會比較清楚,即使如此,其實螢幕還是太小,根本看不清楚,除非是外接大螢幕或投影機,才會比較清楚。


「Advance 進階選項」可以不管他們,基本上,因為 Windows XP 不支援 「HPET晶片中的硬體計時器」,所以筆者將「Disable HPET」打勾代表停用,至於「High Priority高優先等級執行序」,筆者將他打勾了,但速度還是很慢,由於當手機過忙時,這樣也許會影響到手機其他 App 的效能,所以讀者可自行斟酌是否勾選。


Windows XP 虛擬機...由安裝光碟開機!

所有的設定都設定好了,回到畫面最上方,按下啟動紐,替虛擬機開機。


他會出現例如 Boot from CDROM 等字樣,然後就進入 Windows XP 的安裝畫面。


安裝過程非常的久,請耐心等待,否則很容易就誤判為當機了,這中間會要求使用者輸入使用者名稱、所屬組織、電腦名稱與系統管理者的密碼等等,請直接按畫面上方的滑鼠與鍵盤紐,來輔助輸入相關設定,鍵盤輸入與滑鼠操作其實速度還蠻流暢的,筆者則是使用 OTG 線,外接了實體鍵盤和滑鼠來操作,如果你有藍芽滑鼠鍵盤,也可以拿來使用。


由於筆者第一次沒有預期他會安裝那麼久,所以中途手機沒電自動關機,重開機後Windows XP自動清除安裝失敗的暫存檔案,如下圖,自動重新再安裝一次,原來安裝過程超過 8 小時。


系統即將安裝完畢。


Windows XP 即將啟動。


輸入安裝過程中,使用者所設定的系統管理者密碼,以便登入 Windows XP 虛擬機。


登入Windows XP 虛擬機的畫面







備份 Limbo 虛擬機檔案

本機註冊與啟用Windows XP、和 XP 自動登入等等設定,都可以透過 google 在網路上應輕易的找到,此處就不贅述,下圖是由筆者的筆電透過 USB 線連結手機所抓的圖,而 Limbo 存放虛擬機檔案的位置,位於手機內部儲存空間的 limbo \ machines 路徑下,如果你要備份的話,記得先關閉虛擬機,再進行備份此處的檔案。




2016年11月14日 星期一

設定 IBM X3500 M3 Raid Server 並安裝 SUSE Linux 11


這是幾年前的筆記
偶爾翻到把它貼上網
有需要的人可以參考

=================
上週拿到幾台
IBM System X3500 M3
ServeRAID M1015

開始安裝 SUSE 11 中文繁體版
受了很多挫折
後來終於裝好了
其實很容易
不過第一次裝卻繞了很多遠路
分享一下
希望別人不要跟我一樣繞遠路
閱讀這篇手札,您需要具備基本的 RAID 和 BIOS 常識
=================

1.
第一步驟
把例如兩顆硬碟裝進伺服器的 RAID 插槽裡
新機器第一次開機時
可能會出現硬碟無法開機的錯誤畫面
經過漫長的等待
會出現一個畫面詢問是否要按下 Ctrl-H 進入 IBM 的 Web BIOS 設定 (這個就是設定 RAID 的地方)

按下 Ctrl-H 之後
會進入一個 Adapter Selectoin 視窗
依照畫面的指示
建立例如一顆虛擬硬碟(就是將 2 顆硬碟設定成 RAID 1 變成一顆虛擬硬碟)

操作畫面選項按鈕如下 (僅供參考,隨著版本更新畫面會變)
Configuration Wizard
Add Configuration \ NEXT
JBOD Drivers \ (配合 Ctrl 挑選兩顆硬碟) \ NEXT
Manual Configurations \ NEXT
Drivers \ (配合 Ctrl 鍵依序挑選 Slot 0, Slot 1] \ Add To Array (如此 Driver Groups 的 Driver Group0 就會有兩顆硬碟) \ Driver Groups 挑選 Driver Group0 \ Accept DG \ NEXT
Array with Free space 挑選 Driver Group0 \ Add to SPAN \ NEXT
RAID Level 確定是挑選 RAID 1 \ Select Size 輸入可以參考畫面上的 R1 綠色字建議值例如 278.464 GB \ Accept
(跳出一個警告視窗說 Write Through mode 降低使用 Cash 時斷電引發的資料損失,但是會降低執行速度,是否確認) 基本上挑選 YES 後畫面會到前一個畫面,Virtual Drive 的欄位 Driver Group0 就出現一個 VD 0 了, 接著按 NEXT 再回到更前一個畫面,再按 Accept,接著出現警告畫面是否儲存設定,基本上就按 YES, 然後又出現一個警告畫面說所有硬碟的資料會遺失.基本上也是按 YES
Virtual Drivers 挑選 VD 0 \ Set Boot Drive \ GO
大致完成了 RAID 的設定,畫面中其他功能可以自行研究看看

2.
下載 SUSE 11 x64 的版本 (我之前有試過下載 x86 版本,讀不到 IBM RAID )
(請務必檢查 MD5,因為如果使用 IE 或 google 瀏覽器下載好幾 g 的檔案,通常 MD5 檢查都錯誤且安裝都失敗,近年來 IE 或 google 都有針對這個問題改善了
不過最好還是用專業的下載軟體,如果用專業的下載軟體,則設定 NOVELL 會員帳號與密碼的網域應該為 cdn.novell.com, 不是你在 IE 上看到的網址)
安裝方法皆和一般安裝相同
如果是繁體中文安裝預設會出錯

舉我碰到的出錯實例來說
SUSE 會建議你例如將硬碟分割成為 3 個分區
1.使用 FAT 格式化的 \boot\efi (70.6 MB 開機用的受保護區塊)
2.使用 SWAP 格式化的 SWAP (網路上有人建議用實際記憶體的兩倍)
3.使用 EXT3 格式化的 \ (例如剩下的空間都給他)

基本上
分割  FAT 格式化的 \boot\efi 時
要使用[編輯]鈕改 codepage
從預設的 950 (就是 Microsoft 制定的 cp950 (ms950), 目前應該仍未納入標準) 成為例如空白選項 (也許其他選項也可以,沒試過)
這樣就行了
否則安裝中途會出現 \boot\efi 無法 mount 的狀況或是裝完之後無法開機

(附帶一提,安裝 SUSE 途中出錯時可以按下 ALT+CTRL+F2 , 由圖形介面安裝模式切換成命令列安裝模式,接著執行指令 dmesg | tail 就會看到安裝的錯誤訊息為「 cp950 無法辨識」, 接著可以按 ALT+F7 回畫面模式)

That's all

附上當時下載各個軟體的網址

SUSE Linux Enterprise Server 11 x64 Base/SP1 download
http://download.novell.com/protected/Summary.jsp?buildid=GafTuvg_ldk~

IBM ServeRAID Support CD v9.20 (signed) - IBM BladeCenter and System x
http://www-947.ibm.com/support/entry/portal/docdisplay?lndocid=MIGR-64669
檔案為 ibm_fw_sraidmr_1015-20.10.1-0045_linux_32-64.bin

http://delivery04.dhe.ibm.com/sar/CMA/XSA/02gd7/1/ibm_utl_sguide_8.42_anyos_x86-64.iso


參考資料:

SLES11 X86_64 unattended network install to ServeRAID M1015 fails - IBM System x
http://www-947.ibm.com/support/entry/portal/docdisplay?lndocid=MIGR-5084680

(Critical update) IBM ServeRAID M1015 SAS/SATA controller firmware v20.10.1-0045 for Linux - IBM System x
http://www-947.ibm.com/support/entry/portal/docdisplay?lndocid=MIGR-5082827

2016年5月25日 星期三

全世界最便宜的SSL憑證加簽


全世界最便宜的SSL憑證加簽

台灣SSL憑證加簽公司因為市場小,所以SSL憑證加簽,看服務類型一年動輒數萬到超過十萬都有,一般來說,憑證加簽有針對單一網域名稱加簽,價格較便宜,例如針對bookclub.fmj.com.tw這個網域名稱加簽,也可以針對一個網域的所有子網域名稱加簽,價格會較貴,例如針對*.fmj.com.tw加簽,則www.fmj.com.tw或bookclub.fmj.com.tw或fmj.com.tw下的任意子網域名稱都同時有效。
全世界最便宜的SSL憑證加簽,約落在一年美金3-5元之間,通常一次購買三年或以上,才享此優惠價格,前一陣子因為要做FB的APP,需要https連線,所以趕緊去申請了一個當時全球最便宜之一的SSL憑證加簽,所以這篇文章的範例,是JSP網站使用COMODO(柯摩多)憑證加簽作範例,一年只要4.99美元,一次買三年,三年合計不到台幣500元,安裝了之後,我其實蠻滿意的,唯二不太開心的是,原本預計一個小時內會買完並且安裝完,結果如預期般的遇到挫折,花了超過三小時才安裝完,而且才剛裝完,又看到另外一家一年只要3.xx美元...

1.申請前要先做的準備工作

由於COMODO線上申請憑證,他需要確認你擁有所申請網域名稱的控制權,以本文章範例來說,我要申請bookclub.fmj.com.tw的SSL憑證加簽,所以我要具有以下幾個控制權之一,且隨便一個即可,例如我要能收到 admin@bookclub.fmj.com.tw之EMail信箱的認證信函,至於信箱admin也可以挑選其他例如administrator、postmaster、hostmaster或webmaster之類的一堆象徵網域系統管理者名稱的信箱名字,或者呢,你可以將COMODO提供的認證檔案,放到bookclub.fmj.com.tw網站的根目錄下,選擇讓COMODO的測試程式透過http或https連上來認證,又或者呢,你要有添加bookclub.fmj.com.tw網域名稱下子網域名稱的權力,這樣COMODO會給你一個看似亂碼組成的子網域名稱,你必須將他加到你的DNS伺服器裡,變成例如34857394CBA.bookclub.fmj.com.tw,但由於DNS伺服器通常有更新時間的問題,所以認證時間可能要很久,最後,你要有接受購買訊息通知的EMail信箱,並請再準備一張線上付費的信用卡。

2.使用java發SHA2的憑證與加簽申請CSR檔

SHA1的憑證已經被大多數的瀏覽器認定為不安全,所以除非有特殊要求,建議你發SHA2的憑證,其keyalg和sigalg參數範例如下,validity是憑證有效天數,以下範例請自行修改你的網域名稱與路徑等參數,SHA2的keysize要用2048,alias是憑證的名字,keypass和storepass則是憑證和憑證儲存檔案的存取密碼,當然,發憑證的這台電腦,要安裝java才行,且為了發SHA2憑證,不要使用太老舊的java版本(筆者試過java7與8都可以),以下是單獨的一行指令,可以用Windows的命令提示字元來執行。


"C:/Program Files/Java/jre7/bin/keytool" -keysize 2048 -genkey -alias tomcat -keyalg RSA -sigalg SHA256withRSA -validity 36500 -dname "CN=bookclub.fmj.com.tw,OU=it,O=fmj,L=taipei,S=taiwan,C=tw" -keypass changeit -keystore "C:/fmj.keystore" -storepass changeit


附帶一提,此處憑證儲存檔案名稱取名為例如fmj.keystore,儘量不要用預設的憑證名稱.keystore,因為這種只有附檔名的檔案名稱在Mac或Linux上都是屬於隱藏檔,使用不太方便, 憑證發完了,接著發憑證加簽請求CSR檔案:
"C:/Program Files/Java/jre7/bin/keytool" -certreq -keyalg RSA -alias tomcat -file "C:/fmj.csr" -keystore "C:/fmj.keystore"
大功告成,fmj.csr這個文字檔,就是憑證加簽請求檔。

3.上Cheap SSL網購買COMODO憑證

連到以下CheapSSL網址https://cheapsslsecurity.com/,如下圖,如果要加簽含有多個子網域的網域名稱,才選Wildcard Certification,此處範例只要加簽一個不含子網域的網域名稱,例如bookclub.fmj.com.tw,就選Domain Validation,下圖有多家憑證加簽公司,選最便宜的COMODO,按下BUY NOW按鈕購買。
如下圖,挑選購買3年的憑證加簽,則3年後要重新續約,否則憑證加簽會失效,選了要加簽的憑證數量(此處為1個),按下Add TO CARD按鈕加入購物車。
如下圖,填入個人聯絡資料與信用卡付費資料,重點是電子郵件信箱要填正確,等等要去收CheapSSL寄給你的信,信中會提供下一步驟的指示,下圖確認要購買的項目與數量後,就可以按下PLACE ORDER按鈕購買憑證加簽,按下此按鈕後,又會跳出一個畫面(此圖省略)要求你再確認一次,就再按一次確認就可以了。
接著跳出畫面如下,僅是確認你所購買的項目無誤而已

4.由CheapSSL發的EMail連結到其網站貼入憑證加簽申請CSR檔案

做完步驟3,此時去收信應該CheapSSL的信已經立馬寄到了,筆者收到兩封EMail,一封是付費收據,一封有提供超聯結給你連入他的系統,以便貼入前面步驟2所產生的CSR憑證加簽申請檔案,連入他的系統畫面如下,可以看到你所購買的憑證項目。
接著,如下圖,畫面左方挑選ORDERS\incomplete Orders目錄,就會看到未完成的申請項目,也就是Status欄位顯示Pending的憑證項目,按下畫面中的GENERATE CERT NOW按鈕來進行接下來的憑證加簽檔案上傳步驟。
如下圖,欄位1選擇是要續約還是申請新的憑證加簽,此處選擇New代表申請新的憑證加簽,欄位2選擇你要如何證明你擁有所申請網域名稱的權限,筆者選擇最簡單的HTTP File-Based,就是等等讓他給我一個電子檔案下載,下載後我會將這個電子檔案放到我 bookclub.fmj.com.tw的根目錄下,讓他遠端連過來認證。
接著,將前面步驟2產生的CSR檔案用例如Windows的記事本打開,將其內所有的內容複製貼到下圖欄位3裡面,然後在欄位4選擇你所使用的網頁應用程式伺服器,雖然這個步驟可以省略,但此處筆者還是選擇了我所使用的網頁應用程式伺服器Tomcat,最後按下CONTINUE按鈕繼續往下。
接著畫面如下,利用按下Download Auth File按鈕來下載上圖欄位2認證網域名稱擁有權的電子檔案(檔名是一堆亂碼例如287342908374.txt),我將他放到我的網站bookclub.fmj.com.tw的根目錄下以便受檢,如果他檢查不到,你也可以自行檢查看看,例如我可以自行使用瀏覽器連上我的網址http://bookclub.fmj.com.tw/287342908374.txt,看看是否有成關連上這個電子檔,檢查速度會很快在背後偷偷自動完成,下圖欄位1與欄位2是由你所上傳的CSR檔案解析出來的資料,僅供你再次確認,欄位3可以填入這個憑證申請管理者與工程師的聯絡資料,除非有需要分人負責,否則都填入你的聯絡資料就可以了。
至於下圖欄位4只是確認你知道購買憑證加簽的權利義務以及你擁有15天的不滿意退費服務,應該預設都是勾選的,按下CONTINUE按鈕繼續往下。
接著畫面如下圖,會要求你替此次購物經驗評分,隨便評一下吧。
接著下圖畫面選擇Yes讓他能夠後續追蹤你的滿意程度(否則他會跳出更多煩人的視窗說服你選Yes),既然同意讓他後續追蹤,就填入你的名字與電子郵件信箱,按下SUBMIT FEEDBACK按鈕上傳你的意見回饋。
大功告成啦,接著如下圖,按下紅色打叉小按鈕繼續往下。
接著畫面如下,又是確認再確認就可以了,倒是如果你前面忘了下載那個確認你網域名稱擁有權的電子檔案,下面的畫面還可以再下載一次,如果前面你已經正確的做過了,此處就不用再做一次。
此時,你又要去收信了,這次不是CheapSSL寄信給你,是COMODO寄加簽完成的憑證給您啦。

5.由COMODO發的EMail附件下載加簽完畢的憑證

下圖是我的EMail信箱,前兩封是這次COMODO寄給我的,一個是跟我說我可以去下載COMODO的安全認證標章圖片放到我的網頁裡,我只能說,管他的,但是如果你有需要的話,是可以照著做,另一封就是寄來加簽完成的憑證以及COMODO的根憑證與中繼憑證,點進去下載他的附件吧。
至於下圖的第三和第四封EMail,是前面第四步驟CheapSSL發給我的EMail讓我連結到他網站貼入憑證加簽申請CSR檔案。
EMail點進去,畫面如下,申請不同的憑證類別,所需的中繼憑證數量可能不同,此範例的EMail附件是一個ZIP壓縮檔案,解壓後是一個根憑證,兩個中繼憑證,和一個這次所申請我網域名稱的加簽憑證,而根憑證和中繼憑證,基本上都可以在COMODO的公開網站裡下載得到。

6.安裝加簽憑證到你的伺服器上

往下做之前,請先將憑證儲存檔案如此處的fmj.keystore備份,做錯了就將錯誤的憑證儲存檔案刪除掉就好了,重新複製一個原始的憑證儲存檔案來重新做,前面說到EMail附件解壓縮後有以下四個憑證檔案:

Root CA Certificate - AddTrustExternalCARoot.crt
Intermediate CA Certificate - COMODORSAAddTrustCA.crt
Intermediate CA Certificate - COMODORSADomainValidationSecureServerCA.crt
Your PositiveSSL Certificate - bookclub_fmj_com_tw.crt

我們要將這四個憑證都匯入步驟二所發的憑證儲存檔案fmj.keystore中,因為這個步驟要使用java所提供的龜毛程式keytool,我們先講COMODO官方網站所提供錯誤且keytool不接受的方法,這也是為什麼我花了超3小時才搞定這個憑證的原因,官方說明如下:


Certificate Installation: Java Based Web Servers (Tomcat) using keytool
Installing SSL Certificate Chain (Root, Intermediate(s) and the End Entity)

所以我使用Windows的命令提示字元照著做(這是錯誤示範):
1. Import Root Certificate (這是錯誤示範)
"C:/Program Files/Java/jre7/bin/keytool" -import -trustcacerts -alias AddTrustExternalCARoot -file "C:/db/ca/fmj/AddTrustExternalCARoot.crt" -keystore "C:/db/ca/fmj/fmj.keystore"

2. Import Intermediate(s) (這是錯誤示範)
"C:/Program Files/Java/jre7/bin/keytool" -import -trustcacerts -alias intermediate_filename -file "C:/db/ca/fmj/COMODORSAAddTrustCA.crt" -file "C:/db/ca/fmj/COMODORSADomainValidationSecureServerCA.crt" -keystore "C:/db/ca/fmj/fmj.keystore"

3. Import Entity/Domain certificate (這是錯誤示範)
"C:/Program Files/Java/jre7/bin/keytool" -import -trustcacerts -alias tomcat -file "C:/db/ca/fmj/bookclub_fmj_com_tw.crt" -keystore "C:/db/ca/fmj/fmj.keystore"

結果就出現錯誤,訊息如下:
金鑰工具錯誤: java.lang.Exception: 無法從回覆中將鏈建立起來
我原本想,這個錯誤和上次申請的那家godaddy憑證加簽是一樣的,是因為他所提供的根憑證過期造成的,結果不是,因為我去COMODO重新下載最新的根憑證重新匯入,錯誤訊息還是一樣,最後讓我 google 到以下網站的方法就成功了:

https://community.igniterealtime.org/docs/DOC-2208

方法就是,將所有憑證依照根憑證、中繼憑證與加簽憑證的正確順序,貼到同一個文字檔案中(我自己取了一個檔名combine.crt),以 enter 分隔每個憑證的內容,再直接匯入這個新的文字檔到憑證儲存檔案fmj.keystore裡,成功了!沒有依序匯入憑證時造成憑證彼此無法互相關聯的錯誤訊息,指令範例如下:

"C:/Program Files/Java/jre7/bin/keytool" -import -keystore "C:/db/ca/fmj/fmj.keystore" -alias tomcat -file "C:/db/ca/fmj/combine.crt"

接下來,這個憑證儲存檔案fmj.keystore,就可以搬到你的網頁應用程式伺服器上,作為SSL通訊協定所使用的憑證啦,這個部分應該讀者已經很熟悉了,就不繼續寫下去了。


參考資料

https://community.igniterealtime.org/docs/DOC-2208


2015年9月6日 星期日

用 Java 在 JSP 伺服器端製作一張 QRCode 的 jsp 圖檔


製作 QRCode

這裡的範例,是為了用最簡單快速的方法,在 JSP 伺服器端製作一張 QRCode 的 jsp 圖檔,以便讓網頁呼叫。

1.下載 QRCode 元件

前往網站 http://www.swetake.com/qrcode/java/qr_java.html ,在這個只有一頁長度的日文網頁中,搜尋並下載 qrcode_java0.50beta10.tar.gz檔案,自行解壓縮後,到 qrcode_java0.50beta10\lib\ 路徑下,將 Qrcode.jar 檔案,複製到伺服器端安裝 java 的擴充元件路徑下(或是你自己設定用來放置元件的環境路徑下), 例如 C:\Program Files\Java\jdk1.7.0_45\jre\lib\ext , 然後重新啟動 Tomcat。

2.測試

呼叫以下測試網頁,例如 https://127.0.0.1/QRCodeTest.jsp 。
呼叫範例 QRCodeTest.jsp

<%@ page language="java" contentType="text/html;charset=UTF-8" %>
<%@ include file="/Module/Jf/genQRCode.jsp" %><%
//設定 QRCode 圖片要顯示的文字內容
String str = "http://www.fmj.com.tw"; //str = "不見得要放網址\r\n這是中文測試\r\n我今天很開心"; // \r\n 是 Windows 上的換行, Linux 或 Mac 上可用 \n
//用不會重複的 UUID 當作圖片檔名
String TempFileName = java.util.UUID.randomUUID().toString()+".jpg";
//指定圖片存檔路徑, request.getRealPath("") 是伺服器端網站架設的最上層路徑
String TempFilePath = request.getRealPath("")+"/";
//在 JSP 伺服器端產生一個長寬都是140的圖檔, genQRCode 函式內容在本文後面
String TempResult = genQRCode(140, 140, str, TempFilePath+TempFileName);
if (TempResult.equals("")) {//成功

out.print("<img src='/"+TempFileName+"'>");//將 QRCode 秀在網頁上
} else {//失敗
out.print(TempResult);//顯示錯誤訊息
}
%>


使用下載的元件,製作點陣圖,儲存成 jpg 格式圖片的函式 /Module/Jf/genQRCode.jsp
<%@ page language="java" contentType="text/html;charset=UTF-8" %><%!
/*
程式碼來源 : http://playjsp.blogspot.com ,作者:Steven,不限使用請註明出處。
參考資料 http://blog.yslifes.com/archives/684
genQRCode 傳入的參數:
Img_Width 圖片寬度
Img_Height 圖片高度
Text_In QRCode 圖片要顯示的文字內容
File_Path 圖片存檔路徑與檔名
*/
public String genQRCode(int Img_Width, int Img_Height, String Text_In , String File_Path){
try {
byte[] TempByte = Text_In.getBytes("Utf-8");//將要製圖的內容轉成 byte 矩陣
java.awt.image.BufferedImage bi = new java.awt.image.BufferedImage(Img_Width, Img_Height, java.awt.image.BufferedImage.TYPE_INT_RGB);// 設定圖檔寬度予高度
//所下載元件之參數設定
com.swetake.util.Qrcode TempQRCode = new com.swetake.util.Qrcode();
TempQRCode.setQrcodeErrorCorrect('M');//容錯率L M Q H
TempQRCode.setQrcodeEncodeMode('B');//字元模式,N A 或其它的A是英文,N是數字,其它是8 byte
TempQRCode.setQrcodeVersion(7);//可使用的字串長短跟所設定的QrcodeVersion有關,越大可設定的字越多, 0-40,0是自動
//建立一個 java 的 2D 畫布,並設定畫布的背景色、大小與前景色
java.awt.Graphics2D g2D = (java.awt.Graphics2D) bi.getGraphics();
g2D.setBackground(java.awt.Color.WHITE);//背景是白色
g2D.clearRect(0, 0, Img_Width, Img_Height);
g2D.setColor(java.awt.Color.BLACK);//用黑色來畫 QRCode
//在畫布中畫入 QRCode 圖
if (TempByte.length > 0 && TempByte.length < 120) {
boolean[][] TempBoolean = TempQRCode.calQrcode(TempByte);//QRCode 圖只有黑白兩色,呼叫元件建立 QRCode 圖點布林矩陣,false不塗色,True塗入黑色
for (int XINDEX = 0; XINDEX < TempBoolean.length; XINDEX++) {
for (int YINDEX = 0; YINDEX < TempBoolean.length; YINDEX++) {
if (TempBoolean[YINDEX][XINDEX]) {//元件有成功建構圖點者
g2D.fillRect(YINDEX * 3 + 2, XINDEX * 3 + 2, 3, 3);//就在畫布中塗一個方形點
} } } } else {
return "內容長度太長";
}
g2D.dispose();
// bi.flush();
//存檔
java.io.File TempFile = new java.io.File(File_Path);
javax.imageio.ImageIO.write(bi, "jpg", TempFile);
} catch (Exception e) {
return ""+e;//.printStackTrace();
}
return "";
}
%>

參考資料

http://blog.yslifes.com/archives/684


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伺服器的私鑰來解密,此處範例簡單省略此步驟。