安全性是Java鼓吹得最多的特性之一,的確,Java的安全特性涵蓋了從應用級別到語言級別乃至JVM本身。以前大家都知道有個Sandbox,但僅有Sandbox尚不能滿足,或者說不能很方便地做到我們所需要的全部安全需求,譬如現在一個系統首先起碼需要一個登錄功能,更進一步的話,還需要對用戶訪問資源的行為進行約束,下面我想大致講一下Java是怎樣做這些事情的,基本上是一個總結或者說是“讀后感”的性質,同時給出一個簡單的實現例子,這個例子其實還是模仿人家的,呵呵……
談到訪問控制,或者說“授權”,這里有兩層含義,一是從資源的角度,這個socket端口是否被允許操作?這個文件是可讀的?可寫的?還是可執行的?還是以上都行?這就是我們在UNIX下用“ls ?l”命令列出當前目錄下文件時,那些“-rwx-”之類的含義;二是從訪問者的角度,我想通過80端口看Web上新浪歐洲杯的新聞,在這個系統中有沒有這個資格?我想播放D盤上一個名為“friends.rm”的視頻文件,我得到了訪問這個文件的權限了嗎?我有運行播放器的權限嗎?
Java在訪問控制策略上同時考慮了這兩方面內容,你說“不對呀,我用FileOutputStream寫文件,用Socket類連接遠程主機都用得好好的,沒什么限制呀”,這我們得先談談什么叫做“安全管理器”(SecurityManger)。安全管理器從JDK 1.0就開始有了,多古老啊!Java從設計的那一天開始就考慮了安全因素,安全管理器是Sandbox的最重要的一個部分,也是訪問控制的總協調者,我們能夠在通常情況下正常使用網絡和文件,那是因為當啟動application的時候(注意是application,不是applet!),如果你不加“-Djava.security.manager”選項,JVM是不會啟動Sandbox的,這時你可以“為所欲為”,而不會碰到SecurityException之類的異常;一旦加入了“-Djava.security.manager”選項,你就會發現有一連串的異常出現嘍!
Java內置了一個默認的安全策略,這種情況下安全管理器首先裝載的是這個默認的策略,不信啊,不信你檢查一下你的“%JAVA_HOME%\jre\lib\security\”目錄,是不是有個叫“java.policy”的文件?用notepad打開看看:
可以看到,JVM給沙箱內的application分配的權限僅限于中止線程,監聽1024以上的TCP端口,以及對一些系統屬性的讀取權限,像一般的socket操作和文件操作的權限都沒有。
了解了安全管理器的概念以后我們回到授權問題上來。對用戶來說,最擔心的莫過于機器中病毒,病毒本質上是一種惡意的程序,所以訪問控制首先是要對代碼的權限進行控制,上面我一直都在談Sandbox,也就是所謂的“沙箱”,熟悉Java安全性發展歷史的朋友大概對它不會陌生,初期的Java是采用這樣一種安全策略,即:本地代碼是可信的,而遠程代碼是不可信的,譬如applet是一種從網絡上下載到本地并在瀏覽器上運行的一段遠程代碼,因而是不可信的,所以早期的applet被完全置于Sandbox當中,得到的權限是非常有限的;在1.0以后,直至Java 2出現之前,安全策略作了一些靈活的改變,applet不再是完全被歧視的“二等公民”了,因為有了簽名applet,用戶可以選擇信任這種經過簽名的applet,從而applet也可以做一些以前被認為是“出格”的事情;到了Java 2,情況又變了,以前一向被信任的本地代碼似乎也變得不是那么可靠了,這還真說不準,難保誰不會在你出去跟女朋友逛街的時候,偷偷溜進來在你機器上拷個病毒什么的 ^_^ ,這樣本地代碼就落到了和遠程代碼相等同的地位了,這是比較符合現實世界場景的,在Java 2中的安全策略被稱之為“可配置的安全策略”,任何代碼,只要是通過安全管理器訪問,就必須為它預先設定好訪問權限,在這個之外的資源還是別的什么東東,對不起,
java.security.AccessControlException: access denied……
此路不通!
簡單總結一下Java安全模型的發展史,大概就是下面的幾幅圖了:



由于現在普遍是多用戶的系統,所以在實現代碼級訪問控制之外,我們還希望能夠對用戶的行為進行約束,因為對系統造成破壞的因素不僅僅是惡意代碼,人自身的有意或無意的不當操作也會危及系統,譬如向上面說的你不在的時候別人可以在你機器上拷病毒,如果系統能在你不在的時候也能拒絕這個家伙的登錄企圖,那樣麻煩豈不是少很多?于是在Java安全核心之外,提供了一個名為“Java認證與授權服務”(Java Authentication and Authorization Services,JAAS)東東,專門用來處理對用戶的認證和授權,這也就是所謂的“以用戶為中心的授權模型”,說白了就是在“以代碼為中心的授權模型”上再加一層,首先用戶要獲得訪問權限,然后用戶去操縱代碼,代碼來實行真正的訪問操作。下面我主要是講講JAAS是如何工作的。
JAAS的API基本上位于javax.security.auth包及其下屬子包中,很容易找到的。
javax.security.auth.Subject
Subject表征系統中一個認證的用戶,這個詞時而被譯為“主題”時而被以為“主體”(下面我要談到的Principal有時候也被譯為“主體”),不管它有幾個馬甲,反正你就可以看成是在Java中你這個人的影子,你對系統的訪問就體現為Subject.doAs()或Subject.doAsPrivileged()方法。
java.security.Principal
Principal代表用戶的一種身份對象,一個用戶的身份可能不只一個,他所在的組或所擔任的角色也是一種身份,“張翠山”可以說“鐵劃銀鉤”,可以說“張三豐的徒弟”,可以說“張無忌他老爹”,我說“武當七俠”甚至“武當派”,當然也沒錯,這是一個組,呵呵。通過一次登錄后,可能向Subject插入一個或多個Principal,這時候Subject才有實際意義,而不是一個空殼。
javax.security.auth.login.LoginContext
LoginContext旨在提供一個開放的登錄總接口,你只需要用從策略文件中取得的策略名,以及下面介紹的回調對象創建得到一個LoginContext,再調用一次login()方法即可完成登錄,登錄模塊在這里是透明的。
javax.security.auth.spi.LoginModule
登錄模塊實現了對用戶的認證邏輯,它的作用是在登錄配置文件中得到體現,在后面的例子里我們會看到怎么編寫一個登錄配置文件以及上面說過的策略文件。LoginModule接口包括五個主要的方法:
initialize方法,初始化模塊,保存當前Subject以及一些參數。
login方法,判斷一次登錄過程中是否認證通過。
commit方法,是否提交登錄結果。咦,login不就行了嗎?干嗎要來個提交呢?這是因為JAAS采用的是類似于數據庫事務處理的過程,將整體登錄分為兩階段,盡管你login成功,但系統仍有權力根據你這次login的“地位”來決定究竟要不要接納你的身份,只有通過commit,用戶的Principal才會被真正添加到Subject當中,哼哼,真陰險!這里所說的login的“地位”是指策略文件中登錄模塊的“控制標記”選項,有點類似于優先級的概念,因為登錄一個系統的過程可能會經過不止一個登錄模塊,譬如我們登錄系統輸入口令,但這個口令可能保存在一個數據庫或LDAP目錄中,訪問這個數據源也需要經過認證,這就不止一個登錄模塊了吧?所以我們需要分清哪些認證過程是重要的,哪些又是次要的,系統對用戶身份的接收與否是對這些策略綜合權衡的結果。
abort方法:哎呀,上面解釋得是不是太多了?我們再看看abort,還記得數據庫事務處理的回退過程(roll back)嗎?abort就有點像roll back,表示系統并不接受你的身份,以前做過的統統作廢,現場又恢復到和登錄前完全一樣。
logout方法:注銷過程,清除內部狀態,并刪除Subject中全部的Principal。
javax.security.auth.callback.CallbackHandler
回調對象是JAAS中用以將交互過程和認證邏輯分離的一種機制,這也是符合OO和松散耦合(loosely coupled是一個時髦詞匯 ^_^)精神的。JAAS已經實現了一些常用的回調對象,包括取得用戶名的NameCallback,取得口令的PasswordCallback,從終端獲得輸入文本的TextInputCallback,向終端發出文本消息的TextOutputCallback等等。我們所要做的僅僅是實現一個CallbackHandler接口,根據不同的交互信息類型,把從終端得到的信息填到相應的Callback中去就行了。后面的例子我是用了一個JoptionPane提示文本框來輸入用戶名和口令的。
java.security.PrivilegedAction
上面說了那么多登錄相關的接口,該說說授權了,如果我們只談寫源代碼,那么很簡單,只要實現一個PrivilegedAction接口,覆蓋一個run()方法,把你想要做的事情統統放到這個run中就可以了。但我說的只是寫源代碼部分,授權方面用得較多的還是在管理方面,譬如如何編寫一個策略文件,下面我們就來看看JAAS登錄和訪問控制的一個完整流程。
JAAS被稱為是“可插拔的認證框架”(Pluggable Authentication Module,PAMs),其實PAM也不是SUN的專利,Linux上就有這方面的實現,但PAM確實是較早用在了Solaris系統上。我們看看JAAS在認證和授權方面是怎么體現PAM思想的:
主要包括這么幾個部分:
用戶的Principal(MyPrincipal.class)
登錄模塊(MyLoginModule.class)
回調對象(MyCallbackHandler.class)
訪問代碼(MyAction.class)
系統入口(JAASTest.class)
資源(myfile.txt)
策略配置文件(login.conf)
登錄配置文件(jaas.policy)
啟動腳本(JAASTest.bat)
由于啟動java的選項太長,所以寫了一個shell,在控制臺下運行JAASTest.bat,選項“-Djava.security.manager”指定啟用安全管理器,執行的是JAASTest類的main線程,由于shell指定選項“-Djava.security.policy=jaas.policy”,該策略文件允許當前代碼創建LoginContext,并授權進行其它一些操作,它首先初始化一個LoginContext,選項“-Djava.security.auth.login.config=login.conf”指定了登錄配置文件,所以在當前目錄下找到文件login.conf,該文件中指定的登錄策略名稱為“JAASTest”,所以在LoginCotext中第一個參數也是“JAASTest”,同時使用我們自定義的回調對象MyCallbackHandler。創建LoginContext成功,可以進行登錄了,調用LoginContext的login方法,該方法找到login.conf中的登錄模塊MyLoginModule(當然可以有若干個登錄模塊,這里我只用了一個),執行該模塊的登錄過程,MyLoginModule首先初始化:
并使用LoginContext所賦予它的回調對象MyCallbackHandler,該回調過程彈出兩個圖形對話框,要求輸入用戶名和口令,


我們使用指定的用戶名“user”和口令“letmepass”,確定以后分別傳給當前的NameCallback和PasswordCallback,然后回到MyLoginModule的login過程,該過程從回調對象處得到NameCallback和PasswordCallback,進行認證(這里僅僅是簡單的用戶名和口令的對比),
并決定是否commit,由于在login.conf中定義該登錄模塊是required,所以是一個必須通過才能整體認證成功的模塊。
如果整體得到認證通過,那么Subject就可以授權允許MyAction中的代碼了,如語句Subject.doAs(…)所示,該代碼的動作是讀取當前目錄下的myfile.txt文件,并將其內容打印到控制臺,注意到在策略文件jaas.policy中賦予MyPrincipal身份對myfile.txt的讀取權限,所以我們成功看到控制臺下出現
這是我喜歡的一部經典影片“JFK”中檢察官Garrison激情的最后陳詞中的一段,呵呵!
以上過程我們可以用個圖表來表示:

以上流程中使用到的Java源代碼和配置文件如下:
Java Security Architecture
Java 授權內幕
Java安全性 第二部分 認證與授權
Java Security, 2nd Edition, by Scott Oaks
J2EE Security, by Pankaj Kumar
1.Java的訪問控制機制
談到訪問控制,或者說“授權”,這里有兩層含義,一是從資源的角度,這個socket端口是否被允許操作?這個文件是可讀的?可寫的?還是可執行的?還是以上都行?這就是我們在UNIX下用“ls ?l”命令列出當前目錄下文件時,那些“-rwx-”之類的含義;二是從訪問者的角度,我想通過80端口看Web上新浪歐洲杯的新聞,在這個系統中有沒有這個資格?我想播放D盤上一個名為“friends.rm”的視頻文件,我得到了訪問這個文件的權限了嗎?我有運行播放器的權限嗎?
Java在訪問控制策略上同時考慮了這兩方面內容,你說“不對呀,我用FileOutputStream寫文件,用Socket類連接遠程主機都用得好好的,沒什么限制呀”,這我們得先談談什么叫做“安全管理器”(SecurityManger)。安全管理器從JDK 1.0就開始有了,多古老啊!Java從設計的那一天開始就考慮了安全因素,安全管理器是Sandbox的最重要的一個部分,也是訪問控制的總協調者,我們能夠在通常情況下正常使用網絡和文件,那是因為當啟動application的時候(注意是application,不是applet!),如果你不加“-Djava.security.manager”選項,JVM是不會啟動Sandbox的,這時你可以“為所欲為”,而不會碰到SecurityException之類的異常;一旦加入了“-Djava.security.manager”選項,你就會發現有一連串的異常出現嘍!
- Exception in thread "main" java.security.AccessControlException: access denied (……)
- ……
Java內置了一個默認的安全策略,這種情況下安全管理器首先裝載的是這個默認的策略,不信啊,不信你檢查一下你的“%JAVA_HOME%\jre\lib\security\”目錄,是不是有個叫“java.policy”的文件?用notepad打開看看:
- // Standard extensions get all permissions by default
- grant codeBase "file:${java.home}/lib/ext/*" {
- permission java.security.AllPermission;
- };
- // default permissions granted to all domains
- grant {
- // Allows any thread to stop itself using the java.lang.Thread.stop()
- // method that takes no argument.
- // Note that this permission is granted by default only to remain
- // backwards compatible.
- // It is strongly recommended that you either remove this permission
- // from this policy file or further restrict it to code sources
- // that you specify, because Thread.stop() is potentially unsafe.
- // See "http://java.sun.com/notes" for more information.
- permission java.lang.RuntimePermission "stopThread";
- // allows anyone to listen on un-privileged ports
- permission java.net.SocketPermission "localhost:1024-", "listen";
- // "standard" properies that can be read by anyone
- permission java.util.PropertyPermission "java.version", "read";
- permission java.util.PropertyPermission "java.vendor", "read";
- permission java.util.PropertyPermission "java.vendor.url", "read";
- permission java.util.PropertyPermission "java.class.version", "read";
- permission java.util.PropertyPermission "os.name", "read";
- permission java.util.PropertyPermission "os.version", "read";
- permission java.util.PropertyPermission "os.arch", "read";
- permission java.util.PropertyPermission "file.separator", "read";
- permission java.util.PropertyPermission "path.separator", "read";
- permission java.util.PropertyPermission "line.separator", "read";
- permission java.util.PropertyPermission "java.specification.version", "read";
- permission java.util.PropertyPermission "java.specification.vendor", "read";
- permission java.util.PropertyPermission "java.specification.name", "read";
- permission java.util.PropertyPermission "java.vm.specification.version", "read";
- permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
- permission java.util.PropertyPermission "java.vm.specification.name", "read";
- permission java.util.PropertyPermission "java.vm.version", "read";
- permission java.util.PropertyPermission "java.vm.vendor", "read";
- permission java.util.PropertyPermission "java.vm.name", "read";
- };
可以看到,JVM給沙箱內的application分配的權限僅限于中止線程,監聽1024以上的TCP端口,以及對一些系統屬性的讀取權限,像一般的socket操作和文件操作的權限都沒有。
了解了安全管理器的概念以后我們回到授權問題上來。對用戶來說,最擔心的莫過于機器中病毒,病毒本質上是一種惡意的程序,所以訪問控制首先是要對代碼的權限進行控制,上面我一直都在談Sandbox,也就是所謂的“沙箱”,熟悉Java安全性發展歷史的朋友大概對它不會陌生,初期的Java是采用這樣一種安全策略,即:本地代碼是可信的,而遠程代碼是不可信的,譬如applet是一種從網絡上下載到本地并在瀏覽器上運行的一段遠程代碼,因而是不可信的,所以早期的applet被完全置于Sandbox當中,得到的權限是非常有限的;在1.0以后,直至Java 2出現之前,安全策略作了一些靈活的改變,applet不再是完全被歧視的“二等公民”了,因為有了簽名applet,用戶可以選擇信任這種經過簽名的applet,從而applet也可以做一些以前被認為是“出格”的事情;到了Java 2,情況又變了,以前一向被信任的本地代碼似乎也變得不是那么可靠了,這還真說不準,難保誰不會在你出去跟女朋友逛街的時候,偷偷溜進來在你機器上拷個病毒什么的 ^_^ ,這樣本地代碼就落到了和遠程代碼相等同的地位了,這是比較符合現實世界場景的,在Java 2中的安全策略被稱之為“可配置的安全策略”,任何代碼,只要是通過安全管理器訪問,就必須為它預先設定好訪問權限,在這個之外的資源還是別的什么東東,對不起,
java.security.AccessControlException: access denied……
此路不通!
簡單總結一下Java安全模型的發展史,大概就是下面的幾幅圖了:



由于現在普遍是多用戶的系統,所以在實現代碼級訪問控制之外,我們還希望能夠對用戶的行為進行約束,因為對系統造成破壞的因素不僅僅是惡意代碼,人自身的有意或無意的不當操作也會危及系統,譬如向上面說的你不在的時候別人可以在你機器上拷病毒,如果系統能在你不在的時候也能拒絕這個家伙的登錄企圖,那樣麻煩豈不是少很多?于是在Java安全核心之外,提供了一個名為“Java認證與授權服務”(Java Authentication and Authorization Services,JAAS)東東,專門用來處理對用戶的認證和授權,這也就是所謂的“以用戶為中心的授權模型”,說白了就是在“以代碼為中心的授權模型”上再加一層,首先用戶要獲得訪問權限,然后用戶去操縱代碼,代碼來實行真正的訪問操作。下面我主要是講講JAAS是如何工作的。
2.了解幾個主要的API
JAAS的API基本上位于javax.security.auth包及其下屬子包中,很容易找到的。
Subject表征系統中一個認證的用戶,這個詞時而被譯為“主題”時而被以為“主體”(下面我要談到的Principal有時候也被譯為“主體”),不管它有幾個馬甲,反正你就可以看成是在Java中你這個人的影子,你對系統的訪問就體現為Subject.doAs()或Subject.doAsPrivileged()方法。
Principal代表用戶的一種身份對象,一個用戶的身份可能不只一個,他所在的組或所擔任的角色也是一種身份,“張翠山”可以說“鐵劃銀鉤”,可以說“張三豐的徒弟”,可以說“張無忌他老爹”,我說“武當七俠”甚至“武當派”,當然也沒錯,這是一個組,呵呵。通過一次登錄后,可能向Subject插入一個或多個Principal,這時候Subject才有實際意義,而不是一個空殼。
LoginContext旨在提供一個開放的登錄總接口,你只需要用從策略文件中取得的策略名,以及下面介紹的回調對象創建得到一個LoginContext,再調用一次login()方法即可完成登錄,登錄模塊在這里是透明的。
登錄模塊實現了對用戶的認證邏輯,它的作用是在登錄配置文件中得到體現,在后面的例子里我們會看到怎么編寫一個登錄配置文件以及上面說過的策略文件。LoginModule接口包括五個主要的方法:
initialize方法,初始化模塊,保存當前Subject以及一些參數。
login方法,判斷一次登錄過程中是否認證通過。
commit方法,是否提交登錄結果。咦,login不就行了嗎?干嗎要來個提交呢?這是因為JAAS采用的是類似于數據庫事務處理的過程,將整體登錄分為兩階段,盡管你login成功,但系統仍有權力根據你這次login的“地位”來決定究竟要不要接納你的身份,只有通過commit,用戶的Principal才會被真正添加到Subject當中,哼哼,真陰險!這里所說的login的“地位”是指策略文件中登錄模塊的“控制標記”選項,有點類似于優先級的概念,因為登錄一個系統的過程可能會經過不止一個登錄模塊,譬如我們登錄系統輸入口令,但這個口令可能保存在一個數據庫或LDAP目錄中,訪問這個數據源也需要經過認證,這就不止一個登錄模塊了吧?所以我們需要分清哪些認證過程是重要的,哪些又是次要的,系統對用戶身份的接收與否是對這些策略綜合權衡的結果。
abort方法:哎呀,上面解釋得是不是太多了?我們再看看abort,還記得數據庫事務處理的回退過程(roll back)嗎?abort就有點像roll back,表示系統并不接受你的身份,以前做過的統統作廢,現場又恢復到和登錄前完全一樣。
logout方法:注銷過程,清除內部狀態,并刪除Subject中全部的Principal。
回調對象是JAAS中用以將交互過程和認證邏輯分離的一種機制,這也是符合OO和松散耦合(loosely coupled是一個時髦詞匯 ^_^)精神的。JAAS已經實現了一些常用的回調對象,包括取得用戶名的NameCallback,取得口令的PasswordCallback,從終端獲得輸入文本的TextInputCallback,向終端發出文本消息的TextOutputCallback等等。我們所要做的僅僅是實現一個CallbackHandler接口,根據不同的交互信息類型,把從終端得到的信息填到相應的Callback中去就行了。后面的例子我是用了一個JoptionPane提示文本框來輸入用戶名和口令的。
上面說了那么多登錄相關的接口,該說說授權了,如果我們只談寫源代碼,那么很簡單,只要實現一個PrivilegedAction接口,覆蓋一個run()方法,把你想要做的事情統統放到這個run中就可以了。但我說的只是寫源代碼部分,授權方面用得較多的還是在管理方面,譬如如何編寫一個策略文件,下面我們就來看看JAAS登錄和訪問控制的一個完整流程。
3.基本流程
JAAS被稱為是“可插拔的認證框架”(Pluggable Authentication Module,PAMs),其實PAM也不是SUN的專利,Linux上就有這方面的實現,但PAM確實是較早用在了Solaris系統上。我們看看JAAS在認證和授權方面是怎么體現PAM思想的:
主要包括這么幾個部分:
用戶的Principal(MyPrincipal.class)
登錄模塊(MyLoginModule.class)
回調對象(MyCallbackHandler.class)
訪問代碼(MyAction.class)
系統入口(JAASTest.class)
資源(myfile.txt)
策略配置文件(login.conf)
登錄配置文件(jaas.policy)
啟動腳本(JAASTest.bat)
由于啟動java的選項太長,所以寫了一個shell,在控制臺下運行JAASTest.bat,選項“-Djava.security.manager”指定啟用安全管理器,執行的是JAASTest類的main線程,由于shell指定選項“-Djava.security.policy=jaas.policy”,該策略文件允許當前代碼創建LoginContext,并授權進行其它一些操作,它首先初始化一個LoginContext,選項“-Djava.security.auth.login.config=login.conf”指定了登錄配置文件,所以在當前目錄下找到文件login.conf,該文件中指定的登錄策略名稱為“JAASTest”,所以在LoginCotext中第一個參數也是“JAASTest”,同時使用我們自定義的回調對象MyCallbackHandler。創建LoginContext成功,可以進行登錄了,調用LoginContext的login方法,該方法找到login.conf中的登錄模塊MyLoginModule(當然可以有若干個登錄模塊,這里我只用了一個),執行該模塊的登錄過程,MyLoginModule首先初始化:
- Login module initializing ...
并使用LoginContext所賦予它的回調對象MyCallbackHandler,該回調過程彈出兩個圖形對話框,要求輸入用戶名和口令,


我們使用指定的用戶名“user”和口令“letmepass”,確定以后分別傳給當前的NameCallback和PasswordCallback,然后回到MyLoginModule的login過程,該過程從回調對象處得到NameCallback和PasswordCallback,進行認證(這里僅僅是簡單的用戶名和口令的對比),
- MyLoginModule: Authentication pass!
并決定是否commit,由于在login.conf中定義該登錄模塊是required,所以是一個必須通過才能整體認證成功的模塊。
- MyLoginModule: Add a new principal to current subject.
如果整體得到認證通過,那么Subject就可以授權允許MyAction中的代碼了,如語句Subject.doAs(…)所示,該代碼的動作是讀取當前目錄下的myfile.txt文件,并將其內容打印到控制臺,注意到在策略文件jaas.policy中賦予MyPrincipal身份對myfile.txt的讀取權限,所以我們成功看到控制臺下出現
- Access successfully! Reading file:
- ==================================
- Why?
- Because they care!
- Because they want to know the truth!
- Because they want their country back!
- Because it still belongs to us as long as the people have the guts to fight for what they believe in!
- ==================================
這是我喜歡的一部經典影片“JFK”中檢察官Garrison激情的最后陳詞中的一段,呵呵!
以上過程我們可以用個圖表來表示:

4.簡單的例子
以上流程中使用到的Java源代碼和配置文件如下:
- // MyPrincipal.java
- package com.jungleford.auth;
- import java.security.Principal;
- public class MyPrincipal implements Principal
- { // 一個Principal的例子
- private String name; // Principal的名字
- public MyPrincipal(String name)
- {
- this.name = name;
- }
- public String getName()
- { //取得Principal的名字
- return this.name;
- }
- public boolean equals(Object principal)
- { // 判斷兩個Pincipal相同的依據
- if (principal instanceof MyPrincipal)
- return this.name.equals(((MyPrincipal)principal).getName());
- else
- return false;
- }
- public String toString()
- { // Principal的表示
- return "MyPrincipal: " + this.name;
- }
- public int hashCode()
- { // 確定本對象的散列值
- // 用于有基于散列容器的場合,判斷在散列容器中是否是同一個對象。
- // 如果對hashCode感興趣,請參見:
- // http://www-900.ibm.com/developerWorks/cn/java/j-jtp05273/
- return this.name.hashCode();
- }
- }
- // MyLoginModule.java
- package com.jungleford.auth;
- import java.util.*;
- import java.io.IOException;
- import java.security.Principal;
- import javax.security.auth.*;
- import javax.security.auth.callback.*;
- import javax.security.auth.login.*;
- import javax.security.auth.spi.*;
- public class MyLoginModule implements LoginModule
- { // 一個登錄模塊的例子
- private Subject subject; // 登錄主體的表征
- private CallbackHandler cbHandler; // 回調對象,提供終端下獲取用戶名、口令的界面
- private Map sharedState; // 用于緩存中間結果的共享區
- private Map options; // 用于保存某些登錄模塊所需要用到的一些配置選項
- private boolean succeeded = false; // 一次login成功的標志
- private boolean cmtSucceeded = false; // 整體登錄成功的提交標志
- private String username; // 取得用戶名
- private char[] password; // 取得口令
- private Principal principal; // 取得登錄后的身份標志
- public void initialize(Subject subject,
- CallbackHandler cbHandler,
- Map sharedState,
- Map options)
- { // 初始化過程
- System.out.println("Login module initializing ...");
- System.out.println();
- this.subject = subject;
- this.cbHandler = cbHandler;
- this.sharedState = sharedState;
- this.options = options;
- }
- public boolean login() throws LoginException
- { // 一次登錄過程
- if (cbHandler == null) // 尚未配置回調對象
- throw new LoginException("Error: No CallbackHandler available " +
- "to garner authentication information from the user");
- Callback[] cbs = new Callback[2]; // 僅使用用戶名回調和口令回調
- cbs[0] = new NameCallback("Login: ");
- cbs[1] = new PasswordCallback("Password: ", false);
- try
- {
- cbHandler.handle(cbs);
- username = ((NameCallback)cbs[0]).getName();
- char[] temp = ((PasswordCallback)cbs[1]).getPassword();
- if (temp == null)
- { // 口令為空
- temp = new char[0];
- }
- password = new char[temp.length];
- System.arraycopy(temp, 0, password, 0, temp.length);
- ((PasswordCallback)cbs[1]).clearPassword(); // 清除內存中的口令痕跡
- }
- catch (IOException ioe)
- {
- throw new LoginException(ioe.toString());
- }
- catch (UnsupportedCallbackException uce)
- {
- throw new LoginException("Error: " + uce.getCallback().toString() +
- " not available to garner authentication information " +
- "from the user");
- }
- boolean usrCorrect = false; // 用戶名正確否?
- boolean pwdCorrect = false; // 口令正確否?
- if (username.equals("user")) // 目前僅允許用戶名為user的登錄
- usrCorrect = true;
- if (usrCorrect &&
- password.length == 9 &&
- password[0] == 'l' &&
- password[1] == 'e' &&
- password[2] == 't' &&
- password[3] == 'm' &&
- password[4] == 'e' &&
- password[5] == 'p' &&
- password[6] == 'a' &&
- password[7] == 's' &&
- password[8] == 's') // user的口令指定為letmepass
- {
- System.out.println("MyLoginModule: Authentication pass!");
- System.out.println();
- pwdCorrect = true;
- succeeded = true;
- return true; // 一次登錄成功
- }
- else
- {
- System.out.println("MyLoginModule: Authentication failed!");
- System.out.println();
- succeeded = false;
- username = null;
- for (int i = 0; i < password.length; i++) // 清除內存中的口令痕跡
- password[i] = ' ';
- password = null;
- if (!usrCorrect)
- {
- throw new FailedLoginException("Username incorrect!");
- }
- else
- {
- throw new FailedLoginException("Password incorrect!");
- }
- }
- }
- public boolean commit() throws LoginException
- { // 根據登錄配置策略判斷是否整體登錄成功
- if (succeeded == false)
- {
- return false;
- }
- else
- {
- principal = new MyPrincipal(username);
- if (!subject.getPrincipals().contains(principal))
- subject.getPrincipals().add(principal); // 把新的身份添加到subject中
- System.out.println("MyLoginModule: Add a new principal to current subject.");
- System.out.println();
- username = null;
- for (int i = 0; i < password.length; i++) // 清除內存中的口令痕跡
- password[i] = ' ';
- password = null;
- cmtSucceeded = true;
- return true;
- }
- }
- public boolean abort() throws LoginException
- { // 放棄登錄,將狀態復位至登錄前
- if (succeeded == false)
- {
- return false;
- }
- else if (succeeded == true && cmtSucceeded == false)
- {
- succeeded = false;
- username = null;
- if (password != null)
- {
- for (int i = 0; i < password.length; i++) // 清除內存中的口令痕跡
- password[i] = ' ';
- password = null;
- }
- principal = null;
- }
- else
- {
- logout();
- }
- return true;
- }
- public boolean logout() throws LoginException
- { // 注銷,并將狀態復位至登錄前
- subject.getPrincipals().remove(principal);
- succeeded = false;
- succeeded = cmtSucceeded;
- username = null;
- if (password != null)
- {
- for (int i = 0; i < password.length; i++) // 清除內存中的口令痕跡
- password[i] = ' ';
- password = null;
- }
- principal = null;
- return true;
- }
- }
- // MyCallbackHandler.java
- package com.jungleford.auth;
- import java.io.IOException;
- import javax.security.auth.callback.*;
- import javax.swing.*;
- import java.awt.*;
- import java.awt.event.*;
- public class MyCallbackHandler implements CallbackHandler
- {
- public void handle(Callback[] cbs)
- throws IOException, UnsupportedCallbackException
- {
- String username =
- JOptionPane.showInputDialog(null,
- "<html>Available name: " +
- "<font color=\"blue\">user</font></html>",
- "Enter your name",
- JOptionPane.QUESTION_MESSAGE);
- String password =
- JOptionPane.showInputDialog(null,
- "<html>Available password: " +
- "<font color=\"blue\">letmepass</font></html>",
- "Enter your password",
- JOptionPane.QUESTION_MESSAGE);
- for (int i = 0; i < cbs.length; i++)
- {
- if (cbs[i] instanceof TextOutputCallback)
- {
- TextOutputCallback toc = (TextOutputCallback)cbs[i];
- switch (toc.getMessageType())
- {
- case TextOutputCallback.INFORMATION:
- System.out.println(toc.getMessage());
- break;
- case TextOutputCallback.ERROR:
- System.out.println("Error: " + toc.getMessage());
- break;
- case TextOutputCallback.WARNING:
- System.out.println("Warning: " + toc.getMessage());
- break;
- default:
- throw new IOException("Unsupported message type: " +
- toc.getMessageType());
- }
- }
- else if (cbs[i] instanceof NameCallback)
- {
- // prompt the user for a username
- NameCallback nc = (NameCallback)cbs[i];
- //System.err.print(nc.getPrompt());
- //System.err.flush();
- nc.setName(username);
- }
- else if (cbs[i] instanceof PasswordCallback)
- {
- // prompt the user for sensitive information
- PasswordCallback pc = (PasswordCallback)cbs[i];
- //System.err.print(pc.getPrompt());
- //System.err.flush();
- pc.setPassword(password.toCharArray());
- }
- else
- {
- throw new UnsupportedCallbackException(cbs[i], "Unrecognized Callback");
- }
- }
- }
- }
- //MyAction.java
- package com.jungleford.auth;
- import java.io.*;
- import java.security.*;
- public class MyAction implements PrivilegedAction
- { // 對資源的授權訪問動作
- public Object run()
- { // run方法是必須overriding的
- // 這里我們假設訪問動作是讀取當前目錄下myfile.txt文件的內容
- File file = new File("myfile.txt");
- String content = "";
- try
- {
- BufferedReader reader =
- new BufferedReader(
- new FileReader(file));
- String line = reader.readLine();
- while (line != null)
- {
- content += line + "\n";
- line = reader.readLine();
- }
- }
- catch (Exception e)
- {
- System.err.println("Error: Reading file failed!");
- System.err.println();
- e.printStackTrace();
- }
- return content;
- }
- }
- //JAASTest.java
- package com.jungleford.auth;
- import javax.security.auth.Subject;
- import javax.security.auth.login.LoginContext;
- public class JAASTest
- { // 測試我們JAAS登錄和授權的shell
- public static void main(String[] args)
- {
- LoginContext lc = null;
- try
- {
- // 創建context,使用自定義的回調對象,策略名為JAASTest
- // 簡單起見,僅使用一個MyLoginModule模塊
- lc = new LoginContext("JAASTest", new MyCallbackHandler());
- }
- catch (Exception e)
- {
- System.err.println("Error: Creating login context failed!");
- System.err.println();
- e.printStackTrace();
- System.exit(-1);
- }
- try
- { // 整體登錄
- lc.login();
- }
- catch (Exception e)
- {
- System.err.println("Error: Login failed!");
- System.err.println();
- e.printStackTrace();
- System.exit(-1);
- }
- // 獲得授權訪問
- Object object = Subject.doAs(lc.getSubject(), new MyAction());
- System.out.println("Access successfully! Reading file:");
- System.out.println("==================================");
- System.out.println(object);
- System.out.println("==================================");
- System.exit(0);
- }
- }
- //login.conf
- JAASTest
- {
- com.jungleford.auth.MyLoginModule required;
- };
- //jaas.policy
- grant
- {
- permission javax.security.auth.AuthPermission "createLoginContext";
- permission javax.security.auth.AuthPermission "doAs";
- permission javax.security.auth.AuthPermission "modifyPrincipals";
- permission javax.security.auth.AuthPermission "getSubject";
- };
- grant principal com.jungleford.auth.MyPrincipal "guest"
- {
- permission java.io.FilePermission "myfile.txt","read";
- };
- //JAASTest.bat
- java -Djava.security.manager
- -Djava.security.auth.login.config=login.conf
- -Djava.security.policy=jaas.policy
- com.jungleford.auth.JAASTest
- //myfile.txt
- Why?
- Because they care!
- Because they want to know the truth!
- Because they want their country back!
- Because it still belongs to us as long as the people have the guts to fight for what they believe in!