2001 年 3 月
編者的話:
雖然 MD5 簽名算法在 jdk 中早已實現(如 MessageDigest類),但作者從 MD5 的原理分析講述 MD5 具體算法的 Java實現并給出一個完整的示例程序,我想這對我們的讀者來說還是會有很多幫助的。
MD5簡介
MD5的全稱是Message-Digest Algorithm 5,在90年代初由MIT的計算機科學實驗室和RSA Data Security Inc發明,經MD2、MD3和MD4發展而來。
Message-Digest泛指字節串(Message)的Hash變換,就是把一個任意長度的字節串變換成一定長的大整數。請注意我使用了“字節串”而不是“字符串”這個詞,是因為這種變換只與字節的值有關,與字符集或編碼方式無關。
MD5將任意長度的“字節串”變換成一個128bit的大整數,并且它是一個不可逆的字符串變換算法,換句話說就是,即使你看到源程序和算法描述,也無法將一個MD5的值變換回原始的字符串,從數學原理上說,是因為原始的字符串有無窮多個,這有點象不存在反函數的數學函數。
MD5的典型應用是對一段Message(字節串)產生fingerprint(指紋),以防止被“篡改”。舉個例子,你將一段話寫在一個叫readme.txt文件中,并對這個readme.txt產生一個MD5的值并記錄在案,然后你可以傳播這個文件給別人,別人如果修改了文件中的任何內容,你對這個文件重新計算MD5時就會發現。如果再有一個第三方的認證機構,用MD5還可以防止文件作者的“抵賴”,這就是所謂的數字簽名應用。
MD5還廣泛用于加密和解密技術上,在很多操作系統中,用戶的密碼是以MD5值(或類似的其它算法)的方式保存的,用戶Login的時候,系統是把用戶輸入的密碼計算成MD5值,然后再去和系統中保存的MD5值進行比較,而系統并不“知道”用戶的密碼是什么。
一些黑客破獲這種密碼的方法是一種被稱為“跑字典”的方法。有兩種方法得到字典,一種是日常搜集的用做密碼的字符串表,另一種是用排列組合方法生成的,先用MD5程序計算出這些字典項的MD5值,然后再用目標的MD5值在這個字典中檢索。
即使假設密碼的最大長度為8,同時密碼只能是字母和數字,共26+26+10=62個字符,排列組合出的字典的項數則是P(62,1)+P(62,2)….+P(62,8),那也已經是一個很天文的數字了,存儲這個字典就需要TB級的磁盤組,而且這種方法還有一個前提,就是能獲得目標賬戶的密碼MD5值的情況下才可以。
在很多電子商務和社區應用中,管理用戶的Account是一種最常用的基本功能,盡管很多Application Server提供了這些基本組件,但很多應用開發者為了管理的更大的靈活性還是喜歡采用關系數據庫來管理用戶,懶惰的做法是用戶的密碼往往使用明文或簡單的變換后直接保存在數據庫中,因此這些用戶的密碼對軟件開發者或系統管理員來說可以說毫無保密可言,本文的目的是介紹MD5的Java Bean的實現,同時給出用MD5來處理用戶的Account密碼的例子,這種方法使得管理員和程序設計者都無法看到用戶的密碼,盡管他們可以初始化它們。但重要的一點是對于用戶密碼設置習慣的保護。
有興趣的讀者可以從這里取得MD5也就是RFC 1321的文本。http://www.ietf.org/rfc/rfc1321.txt
實現策略
MD5的算法在RFC1321中實際上已經提供了C的實現,我們其實馬上就能想到,至少有兩種用Java實現它的方法,第一種是,用Java語言重新寫整個算法,或者再說簡單點就是把C程序改寫成Java程序。第二種是,用JNI(Java Native Interface)來實現,核心算法仍然用這個C程序,用Java類給它包個殼。
但我個人認為,JNI應該是Java為了解決某類問題時的沒有辦法的辦法(比如與操作系統或I/O設備密切相關的應用),同時為了提供和其它語言的互操作性的一個手段。使用JNI帶來的最大問題是引入了平臺的依賴性,打破了SUN所鼓吹的“一次編寫到處運行”的Java好處。因此,我決定采取第一種方法,一來和大家一起嘗試一下“一次編寫到處運行”的好處,二來檢驗一下Java 2現在對于比較密集的計算的效率問題。
實現過程
限于這篇文章的篇幅,同時也為了更多的讀者能夠真正專注于問題本身,我不想就某一種Java集成開發環境來介紹這個Java Bean的制作過程,介紹一個方法時我發現步驟和命令很清晰,我相信有任何一種Java集成環境三天以上經驗的讀者都會知道如何把這些代碼在集成環境中編譯和運行。用集成環境講述問題往往需要配很多屏幕截圖,這也是我一直對集成環境很頭疼的原因。我使用了一個普通的文本編輯器,同時使用了Sun公司標準的JDK 1.3.0 for Windows NT。
其實把C轉換成Java對于一個有一定C語言基礎的程序員并不困難,這兩個語言的基本語法幾乎完全一致.我大概花了一個小時的時間完成了代碼的轉換工作,我主要作了下面幾件事:
- 把必須使用的一些#define的宏定義變成Class中的final static,這樣保證在一個進程空間中的多個Instance共享這些數據
- 刪去了一些無用的#if define,因為我只關心MD5,這個推薦的C實現同時實現了MD2 MD3和 MD4,而且有些#if define還和C不同編譯器有關
- 將一些計算宏轉換成final static 成員函數。
- 所有的變量命名與原來C實現中保持一致,在大小寫上作一些符合Java習慣的變化,計算過程中的C函數變成了private方法(成員函數)。
- 關鍵變量的位長調整
- 定義了類和方法
需要注意的是,很多早期的C編譯器的int類型是16 bit的,MD5使用了unsigned long int,并認為它是32bit的無符號整數。而在Java中int是32 bit的,long是64 bit的。在MD5的C實現中,使用了大量的位操作。這里需要指出的一點是,盡管Java提供了位操作,由于Java沒有unsigned類型,對于右移位操作多提供了一個無符號右移:>>>,等價于C中的 >> 對于unsigned 數的處理。
因為Java不提供無符號數的運算,兩個大int數相加就會溢出得到一個負數或異常,因此我將一些關鍵變量在Java中改成了long類型(64bit)。我個人認為這比自己去重新定義一組無符號數的類同時重載那些運算符要方便,同時效率高很多并且代碼也易讀,OO(Object Oriented)的濫用反而會導致效率低下。
限于篇幅,這里不再給出原始的C代碼,有興趣對照的讀者朋友可以去看RFC 1321。 MD5.java源代碼
測試
在RFC 1321中,給出了Test suite用來檢驗你的實現是否正確:
MD5 ("") = d41d8cd98f00b204e9800998ecf8427e
MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661
MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72
MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0
MD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b
……
|
這些輸出結果的含義是指:空字符串””的MD5值是d41d8cd98f00b204e9800998ecf8427e,字符串”a”的MD5值是
0cc175b9c0f1b6a831c399e269772661……
編譯并運行我們的程序:
javac –Cd . MD5.java
java beartool.MD5
|
為了將來不與別人的同名程序沖突,我在我的程序的第一行使用了package beartool;
因此編譯命令 javac ?Cd . MD5.java 命令在我們的工作目錄下自動建立了一個 beartool 目錄,目錄下放著編譯成功的 MD5.class
我們將得到和Test suite同樣的結果。當然還可以繼續測試你感興趣的其它MD5變換,例如:
將給出1234的MD5值。
可能是我的計算機知識是從Apple II和Z80單板機開始的,我對大寫十六進制代碼有偏好,如果您想使用小寫的Digest String只需要把byteHEX函數中的A、B、C、D、E、F改成a、b、 c、d、e、f就可以了。
MD5據稱是一種比較耗時的計算,我們的Java版MD5一閃就算出來了,沒遇到什么障礙,而且用肉眼感覺不出來Java版的MD5比C版的慢。
為了測試它的兼容性,我把這個MD5.class文件拷貝到我的另一臺Linux+IBM JDK 1.3的機器上,執行后得到同樣結果,確實是“一次編寫到處運行了”。
Java Bean簡述
現在,我們已經完成并簡單測試了這個Java Class,我們文章的標題是做一個Java Bean。
其實普通的Java Bean很簡單,并不是什么全新的或偉大的概念,就是一個Java的Class,盡管 Sun規定了一些需要實現的方法,但并不是強制的。而EJB(Enterprise Java Bean)無非規定了一些必須實現(非常類似于響應事件)的方法,這些方法是供EJB Container使用(調用)的。
在一個Java Application或Applet里使用這個bean非常簡單,最簡單的方法是你要使用這個類的源碼工作目錄下建一個beartool目錄,把這個class文件拷貝進去,然后在你的程序中import beartool.MD5就可以了。最后打包成.jar或.war是保持這個相對的目錄關系就行了。
Java還有一個小小的好處是你并不需要摘除我們的MD5類中那個main方法,它已經是一個可以工作的Java Bean了。Java有一個非常大的優點是她允許很方便地讓多種運行形式在同一組代碼中共存,比如,你可以寫一個類,它即是一個控制臺Application和GUI Application,同時又是一個Applet,同時還是一個Java Bean,這對于測試、維護和發布程序提供了極大的方便,這里的測試方法main還可以放到一個內部類中。
這里講述了把測試和示例代碼放在一個內部靜態類的好處,是一種不錯的工程化技巧和途徑。
把Java Bean裝到JSP里
正如我們在本文開頭講述的那樣,我們對這個MD5 Bean的應用是基于一個用戶管理,這里我們假設了一個虛擬社區的用戶login過程,用戶的信息保存在數據庫的個名為users的表中。這個表有兩個字段和我們的這個例子有關,userid :char(20)和pwdmd5 :char(32),userid是這個表的Primary Key,pwdmd5保存密碼的MD5串,MD5值是一個128bit的大整數,表示成16進制的ASCII需要32個字符。
這里給出兩個文件, login.html是用來接受用戶輸入的form, login.jsp 用來模擬使用MD5 Bean的login過程。
為了使我們的測試環境簡單起見,我們在JSP中使用了JDK內置的JDBC-ODBC Bridge Driver,community是ODBC的DSN的名字,如果你使用其它的JDBC Driver,替換掉login.jsp中的
Connection con= DriverManager.getConnection("jdbc:odbc:community", "", "");
|
即可。
login.jsp的工作原理很簡單,通過post接收用戶輸入的UserID和Password,然后將Password變換成MD5串,然后在users表中尋找UserID和pwdmd5,因為UserID是users表的Primary Key,如果變換后的pwdmd5與表中的記錄不符,那么SQL查詢會得到一個空的結果集。
這里需要簡單介紹的是,使用這個Bean只需要在你的JSP應用程序的WEB-INF/classes下建立一個beartool目錄,然后將MD5.class拷貝到那個目錄下就可以了。如果你使用一些集成開發環境,請參考它們的deploy工具的說明。在JSP使用一個java Bean關鍵的一句聲明是程序中的第2行:
<jsp:useBean id='oMD5' scope='request' class='beartool.MD5'/>
|
這是所有JSP規范要求JSP容器開發者必須提供的標準Tag。
id=實際上是指示JSP Container創建Bean的實例時用的實例變量名。在后面的<%和%>之間的Java程序中,你可以引用它。在程序中可以看到,通過 pwdmd5=oMD5.getMD5ofStr (password) 引用了我們的MD5 Java Bean提供的唯一一個公共方法: getMD5ofStr。
Java Application Server執行.JSP的過程是先把它預編譯成.java(那些Tag在預編譯時會成為java語句),然后再編譯成.class。這些都是系統自動完成和維護的,那個.class也稱為Servlet。當然,如果你愿意,你也可以幫助Java Application Server去干本該它干的事情,自己直接去寫Servlet,但用Servlet去輸出HTML那簡直是回到了用C寫CGI程序的惡夢時代。
如果你的輸出是一個復雜的表格,比較方便的方法我想還是用一個你所熟悉的HTML編輯器編寫一個“模板”,然后在把JSP代碼“嵌入”進去。盡管這種JSP代碼被有些專家指責為“空心粉”,它的確有個缺點是代碼比較難管理和重復使用,但是程序設計永遠需要的就是這樣的權衡。我個人認為,對于中、小型項目,比較理想的結構是把數據表示(或不嚴格地稱作WEB界面相關)的部分用JSP寫,和界面不相關的放在Bean里面,一般情況下是不需要直接寫Servlet的。
如果你覺得這種方法不是非常的OO(Object Oriented),你可以繼承(extends)它一把,再寫一個bean把用戶管理的功能包進去。
到底能不能兼容?
我測試了三種Java應用服務器環境,Resin 1.2.3、Sun J2EE 1.2、IBM WebSphere 3.5,所幸的是這個Java Bean都沒有任何問題,原因其實是因為它僅僅是個計算程序,不涉及操作系統,I/O設備。其實用其它語言也能簡單地實現它的兼容性的,Java的唯一優點是,你只需提供一個形態的運行碼就可以了。請注意“形態”二字,現在很多計算結構和操作系統除了語言本身之外都定義了大量的代碼形態,很簡單的一段C語言核心代碼,轉換成不同形態要考慮很多問題,使用很多工具,同時受很多限制,有時候學習一種新的“形態”所花費的精力可能比解決問題本身還多。比如光Windows就有EXE、Service、的普通DLL、COM DLL以前還有OCX等等等等,在Unix上雖說要簡單一些,但要也要提供一個.h定義一大堆宏,還要考慮不同平臺編譯器版本的位長度問題。我想這是Java對我來說的一個非常重要的魅力吧。
參考資料
IETF RFC 1321 http:// http://www.ietf.org/rfc/rfc1321.txt 這是關于MD5最原始和權威的文檔,同時包含了完整的C語言實現。這份文檔是1992年提交的,它的C程序還不是ANSI標準語法的,有些老Unix的親切感。在Windows下編譯這個C程序要小小改動一下。
J2EE教程 http://java.sun.com/j2ee/tutorial/doc/information/resources.html SUN的J2EE教程編得很不錯,言簡意賅,為全面了解Java Bean和JSP背后的體系結構提供了一個不錯的起點或索引。
Bruce Eckel《Think in Java 2 ndEdition》 這也是Developer Works推薦下載的一本不錯的Java教科書,我個人認為這本書是寫給至少有一種程序設計語言經驗的程序員的。
關于作者 作者本人于1991年畢業于武漢測繪科技大學計算機科學與工程系,畢業后被分配到中國計算機軟件與技術服務總公司,從事國產Unix系統開發和金融系統應用,三年后辭職創業,但至今未成大器。自中學時代開始接觸Apple II,經歷了中國軟件業到目前為止的完整發展歷程,在十多年的軟件工作生涯中,主要從事銀行、零售業系統的開發、體系架構設計和項目管理,曾經成功地主導開發和實施過多個大中型POS系統項目、銀行業務系統和企業MIS系統,現為自由職業者,主要為若干固定客戶從事軟件項目的技術咨詢工作,同時作為自由撰稿人為一些網站和電子媒體介紹一些新的企業計算技術。此外,自己建立了一個小型的個人新技術實驗室,有若干臺裝有不同平臺的電腦并且連成一個小型的Intranet。最大的愛好就是不斷地去弄明白層出不窮的軟件技術和概念。 |
posted on 2005-08-18 15:39
my java 閱讀(300)
評論(0) 編輯 收藏 所屬分類:
java 轉帖