程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> JAAS - 靈活的Java安全機制

JAAS - 靈活的Java安全機制

編輯:關於JAVA

摘要:

Java Authentication Authorization Service(JAAS,Java驗證和授權API )提供了靈活和可伸縮的機制來保證客戶端或服務器端的Java程序。Java早期的 安全框架強調的是通過驗證代碼的來源和作者,保護用戶避免受到下載下來的代 碼的攻擊。JAAS強調的是通過驗證誰在運行代碼以及他/她的權限來保護系統面 受用戶的攻擊。它讓你能夠將一些標准的安全機制,例如Solaris NIS(網絡信 息服務)、Windows NT、LDAP(輕量目錄存取協議),Kerberos等通過一種通用 的,可配置的方式集成到系統中。本文首先向你介紹JAAS驗證中的一些核心部分 ,然後通過例子向你展示如何開發登錄模塊。

你是否曾經需要為一個應用程序實現登錄模塊呢?如果你是一個比較有經驗 的程序員,相信你這樣的工作做過很多次,而且每次都不完全一樣。你有可能把 你的登錄模塊建立在Oracle數據庫的基礎上,也有可能使用的是NT的用戶驗證, 或者使用的是LDAP目錄。如果有一種方法可以在不改變應用程序級的代碼的基礎 上支持上面提到的所有這一些安全機制,對於程序員來說一定是一件幸運的事。

現在你可以使用JAAS實現上面的目標。JAAS是一個比較新的的Java API。在 J2SE 1.3中,它是一個擴展包;在J2SE 1.4中變成了一個核心包。在本文中,我 們將介紹JAAS的一些核心概念,然後通過例子說明如何將JAAS應用到實際的程序 中。本文的例子是根據我們一個基於Web的Java應用程序進行改編的,在這個例 子中,我們使用了關系數據庫保存用戶的登錄信息。由於使用了JAAS,我們實現 了一個健壯而靈活的登錄和身份驗證模塊。

Java驗證和授權:概論

在JAAS出現以前,Java的安全模型是為了滿足跨平台的網絡應用程序的需要 而設計的。在Java早期版本中,Java通常是作為遠程代碼被使用,例如Applet, 。因此最初的安全模型把注意力放在通過驗證代碼的來源來保護用戶上。早期的 Java安全機制中包含的概念,如SercurityManager,沙箱概念,代碼簽名,策略 文件,多是為了保護用戶。

JAAS的出現反映了Java的演變。傳統的服務器/客戶端程序需要實現登錄和 存取控制,JAAS通過對運行程序的用戶的進行驗證,從而達到保護系統的目的。 雖然JAAS同時具有驗證和授權的能力,在這篇文章中,我們主要介紹驗證功能。

通過在應用程序和底層的驗證和授權機制之間加入一個抽象層,JAAS可以簡 化涉及到Java Security包的程序開發。抽象層獨立於平台的特性使開發人員可 以使用各種不同的安全機制,而且不用修改應用程序級的代碼。和其他Java Security API相似,JAAS通過一個可擴展的框架:服務提供者接口(Service Provider Interface,SPI)來保證程序獨立於安全機制。服務提供者接口是由 一組抽象類和接口組成的。圖一中給出了JAAS程序的整體框架圖。應用程序級的 代碼主要處理LoginContext。在LoginContext下面是一組動態配置的 LoginModules。LoginModule使用正確的安全機制進行驗證。

圖一給出了JAAS的概覽。應用程序層的代碼只需要和LoginContext打交道。 在LoginContext之下是一組動態配置的LoginModule對象,這些對象使用相關的 安全基礎結構進行驗證操作。

圖一 JAAS概覽

JAAS提供了一些LoginModule的參考實現代碼,比如JndiLoginModule。開發 人員也可以自己實現LoginModule接口,就象在我們例子中的RdbmsLonginModule 。同時我們還會告訴你如何使用一個簡單的配置文件來安裝應用程序。

為了滿足可插接性,JAAS是可堆疊的。在單一登錄的情況下,一組安全模塊 可以堆疊在一起,然後被其他的安全機制按照堆疊的順序被調用。

JAAS的實現者根據現在一些流行的安全結構模式和框架將JASS模型化。例如 可堆疊的特性同Unix下的可堆疊驗證模塊(PAM,Pluggable Authentication Module)框架就非常相似。從事務的角度看,JAAS類似於雙步提交(Two-Phase Commit,2PC)協議的行為。JAAS中安全配置的概念(包括策略文件(Police File)和許可(Permission))來自於J2SE 1.2。JAAS還從其他成熟的安全框架 中借鑒了許多思想。

客戶端和服務器端的JAAS

開發人員可以將JAAS應用到客戶端和服務器端。在客戶端使用JAAS很簡單。 在服務器端使用JAAS時情況要復雜一些。目前在應用服務器市場中的JAAS產品還 不是很一致,使用JAAS的J2EE應用服務器有一些細微的差別。例如JBossSx使用 自己的結構,將JAAS集成到了一個更大的安全框架中;而雖然WebLogic 6.x也使 用了JAAS,安全框架卻完全不一樣。

現在你能夠理解為什麼我們需要從客戶端和服務器端的角度來看JAAS了。我 們將在後面列出兩種情況下的例子。為了使服務器端的例子程序更加簡單,我們 使用了Resin應用服務器。

核心JAAS類

在使用JAAS之前,你首先需要安裝JAAS。在J2SE 1.4中已經包括了JAAS,但 是在J2SE 1.3中沒有。如果你希望使用J2SE 1.3,你可以從SUN的官方站點上下 載JAAS。當正確安裝了JAAS後,你會在安裝目錄的lib目錄下找到jaas.jar。你 需要將該路徑加入Classpath中。(注:如果你安裝了應用服務器,其中就已經 包括了JAAS,請閱讀應用服務器的幫助文檔以獲得更詳細的信息)。在Java安全 屬性文件java.security中,你可以改變一些與JAAS相關的系統屬性。該文件保 存在<jre_home>/lib/security目錄中。

在應用程序中使用JAAS驗證通常會涉及到以下幾個步驟:

1. 創建一個LoginContext的實例。

2. 為了能夠獲得和處理驗證信息,將一個CallBackHandler對象作為參數傳 送給LoginContext。

3. 通過調用LoginContext的login()方法來進行驗證。

4. 通過使用login()方法返回的Subject對象實現一些特殊的功能(假設登 錄成功)。

下面是一個簡單的例子:

LoginContext lc = new LoginContext("MyExample");
try {
lc.login();
} catch (LoginException) {
// Authentication failed.
}
// Authentication successful, we can now continue.
// We can use the returned Subject if we like.
Subject sub = lc.getSubject();
Subject.doAs(sub, new MyPrivilegedAction());

在運行這段代碼時,後台進行了以下的工作。

1. 當初始化時,LoginContext對象首先在JAAS配置文件中找到MyExample項 ,然後更具該項的內容決定該加載哪個LoginModule對象(參見圖二)。

2. 在登錄時,LoginContext對象調用每個LoginModule對象的login()方法 。

3. 每個login()方法進行驗證操作或獲得一個CallbackHandle對象。

4. CallbackHandle對象通過使用一個或多個CallBack方法同用戶進行交互, 獲得用戶輸入。

5. 向一個新的Subject對象中填入驗證信息。

我們將對代碼作進一步的解釋。但是在這之前,讓我們先看代碼中涉及到的 核心JAAS類和接口。這些類可以被分為三種類型:

普通類型 Subject,Principal,憑證

驗證 LoginContext,LoginModule,CallBackHandler,Callback

授權 Policy,AuthPermission,PrivateCredentialPermission

上面列舉的類和接口大多數都在javax.security.auth包中。在J2SE 1.4中, 還有一些接口的實現類在com.sun.security.auth包中。

普通類型:Subject,Principal,憑證

Subject類代表了一個驗證實體,它可以是用戶、管理員、Web服務,設備或 者其他的過程。該類包含了三中類型的安全信息:

身份(Identities):由一個或多個Principal對象表示

公共憑證(Public credentials):例如名稱或公共秘鑰

私有憑證(Private credentials):例如口令或私有密鑰

Principal對象代表了Subject對象的身份。它們實現了 java.security.Principal和java.io.Serializable接口。在Subject類中,最重 要的方法是getName()。該方法返回一個身份名稱。在Subject對象中包含了多 個Principal對象,因此它可以擁有多個名稱。由於登錄名稱、身份證號和Email 地址都可以作為用戶的身份標識,可見擁有多個身份名稱的情況在實際應用中是 非常普遍的情況。

在上面提到的憑證並不是一個特定的類或借口,它可以是任何對象。憑證中 可以包含任何特定安全系統需要的驗證信息,例如標簽(ticket),密鑰或口令 。Subject對象中維護著一組特定的私有和公有的憑證,這些憑證可以通過 getPrivateCredentials()和getPublicCredentials()方法獲得。這些方法 通常在應用程序層中的安全子系統被調用。

驗證:LoginContext

在應用程序層中,你可以使用LoginContext對象來驗證Subject對象。 LoginContext對象同時體現了JAAS的動態可插入性(Dynamic Pluggability), 因為當你創建一個LoginContext的實例時,你需要指定一個配置。LoginContext 通常從一個文本文件中加載配置信息,這些配置信息告訴LoginContext對象在登 錄時使用哪一個LoginModule對象。

下面列出了在LoginContext中經常使用的三個方法:

login () 進行登錄操作。該方法激活了配置中制定的所有LoginModule對象 。如果成功,它將創建一個經過了驗證的Subject對象;否則拋出 LoginException異常。

getSubject () 返回經過驗證的Subject對象

logout () 注銷Subject對象,刪除與之相關的Principal對象和憑證

驗證:LoginModule

LoginModule是調用特定驗證機制的接口。J2EE 1.4中包含了下面幾種 LoginModule的實現類:

JndiLoginModule 用於驗證在JNDI中配置的目錄服務

Krb5LoginModule 使用Kerberos協議進行驗證

NTLoginModul 使用當前用戶在NT中的用戶信息進行驗證

UnixLoginModule 使用當前用戶在Unix中的用戶信息進行驗證

同上面這些模塊綁定在一起的還有對應的Principal接口的實現類,例如 NTDomainPrincipal和UnixPrincipal。這些類在com.sun.security.auth包中。

LoginModule接口中包含了五個方法:

initialize () 當創建一LoginModule實例時會被構造函數調用

login () 進行驗證

commit () 當LgoninContext對象接受所有LoginModule對象傳回的結果後將 調用該方法。該方法將Principal對象和憑證賦給Subject對象。

abort () 當任何一個LoginModule對象驗證失敗時都會調用該方法。此時沒 有任何Principal對象或憑證關聯到Subject對象上。

logout () 刪除與Subject對象關聯的Principal對象和憑證。

在應用程序的代碼中,程序員通常不會直接調用上面列出的方法,而是通過 LigonContext間接調用這些方法。

驗證:CallbackHandler和Callback

CallbackHandler和Callback對象可以使LoginModule對象從系統和用戶那裡 收集必要的驗證信息,同時獨立於實際的收集信息時發生的交互過程。

JAAS在javax.sevurity.auth.callback包中包含了七個Callback的實現類和 兩個CallbackHandler的實現類:ChoiceCallback、ConfirmationCallback、 LogcaleCallback、NameCallback、PasswordCallback、TextInputCallback、 TextOutputCallback、DialogCallbackHandler和TextCallBackHandler。 Callback接口只會在客戶端會被使用到。我將在後面介紹如何編寫你自己的 CallbackHandler類。

配置文件

上面我已經提到,JAAS的可擴展性來源於它能夠進行動態配置,而配置信息 通常是保存在文本。這些文本文件有很多個配置塊構成,我們通常把這些配置塊 稱作申請(Application)。每個申請對應了一個或多個特定的LoginModule對象 。

當你的代碼構造一個LoginContext對象時,你需要把配置文件中申請的名稱 傳遞給它。LoginContext將會根據申請中的信息決定激活哪些LoginModule對象 ,按照什麼順序激活以及使用什麼規則激活。

配置文件的結構如下所示:

Application {
ModuleClass Flag ModuleOptions;
ModuleClass Flag ModuleOptions;
...
};
Application {
ModuleClass Flag ModuleOptions;
...
};
...

下面是一個名稱為Sample的申請

Sample {
com.sun.security.auth.module.NTLoginModule Rquired debug=true;
}

上面這個簡單的申請指定了LoginContext對象應該使用NTLoginModule進行驗 證。類的名稱在ModuleClass中被指定。Flag控制當申請中包含了多個 LoginModule時進行登錄時的行為:Required、Sufficient、Requisite和 Optional。最常用的是Required,使用它意味著對應的LoginModule對象必須被 調用,並且必須需要通過所有的驗證。由於Flag本身的復雜性,本文在這裡不作 深究。

ModuleOption允許有多個參數。例如你可以設定調試參數為True (debug=true),這樣診斷輸出將被送到System.out中。

配置文件可以被任意命名,並且可以被放在任何位置。JAAS框架通過使用 java.securty.auth.long.config屬性來確定配置文件的位置。例如當你的應用 程序是JaasTest,配置文件是當前目錄下的jaas.config,你需要在命令行中輸 入:

java -Djava.security.auth.login.config=jass.config JavaTest

圖二描述了配置文件中各元素之間的關系

圖二 JAAS的配置文件

通過命令行方式進行登錄驗證

為了說明JAAS到底能干什麼,我在這裡編寫了兩個例子。一個是簡單的由命 令行輸入調用的程序,另一個是服務器端的JSP程序。這兩個程序都通過用戶名 /密碼的方式進行登錄,然後使用關系數據庫對其進行驗證。

為了通過數據庫進行驗證,我們需要:

1. 實現RdbmsLoginModul類,該類可以對輸入的信息進行驗證。

2. 編輯一個配置文件,告訴LoginContext如何使用RdbmsLoginModule。

3. 實現ConsoleCallbackHandler類,通過該類可以獲取用戶的輸入。

4. 編寫應用程序代碼。

在RdbmsLoginModul類中,我們必須實現LgoinModule接口中的五個方法。首 先是initialize()方法:

public void initialize(Subject subject, CallbackHandler
callbackHandler,
Map sharedState, Map options)
{
this.subject = subject;
this.callbackHandler = callbackHandler;
this.sharedState = sharedState;
this.options = options;
url = (String)options.get("url");
driverClass = (String)options.get("driver");
debug = "true".equalsIgnoreCase((String)options.get("debug"));
}

LoginContext在調用login()方法時會調用initialize()方法。 RdbmsLoginModule的第一個任務就是在類中保存輸入參數的引用。在驗證成功後 將向Subject對象中送入Principal對象和憑證。

CallbackHandler對象將會在login()方法中被使用到。sharedState可以使 數據在不同的LoginModule對象之間共享,但是在這個例子中我們不會使用它。 最後是名為options的Map對象。options向LgoinModule對象傳遞在配置文件 ModuleOption域中定義的參數的值。配置文件如下所示:

Example {
RdbmsLoginModule required
driver="org.gjt.mm.mysql.Driver"
url="jdbc:mysql://localhost/jaasdb?user=root"
debug="true";
};

在配置文件中,RdbmsLoginModule包含了五個參數,其中driver、url、user 和password是必需的,而debug是可選闡述。driver、url、user和password參數 告訴我們如何獲得JDBC連接。當然你還可以在ModuleOption域中加入數據庫中的 表或列的信息。使用這些參數的目的是為了能夠對數據庫進行操作。在 LoginModule類的initialize()方法中我們保存了每個參數的值。

我們前面提到一個LoginContext對應的配置文件告訴它應該使用文件中的哪 一個申請。這個信息是通過LgoinContext的構造函數傳遞的。下面是初始化客戶 端的代碼,在代碼中創建了一個LoginContex對象並調用了login()方法。

ConsoleCallbackHandler cbh = new ConsoleCallbackHandler();
LoginContext lc = new LoginContext("Example", cbh);
lc.login();

當LgoinContext.login()方法被調用時,它調用所有加載了的LoginModule 對象的login()方法。在我們的這個例子中是RdbmsLoginModule中的login() 方法。

RdbmsLoginModule中的login()方法進行了下面的操作:

1. 創建兩個Callback對象。這些對象從用戶輸入中獲取用戶名/密碼。程序 中使用了JAAS中的兩個Callback類:NameCallback和PasswordCallback(這兩個 類包含在javax.security.auth.callback包中)。

2. 通過將callbacks作為參數傳遞給CallbackHandler的handle()方法來激 活Callback。

3. 通過Callback對象獲得用戶名/密碼。

4. 在rdbmsValidate()方法中通過JDBC在數據庫中驗證獲取的用戶名/密 碼。

下面是RdbmsLoginModule中的login()方法的代碼

public boolean login() throws LoginException {
if (callbackHandler == null)
throw new LoginException("no handler");
NameCallback nameCb = new NameCallback("user: ");
PasswordCallback passCb = new PasswordCallback("password: ", true);
callbacks = new Callback[] { nameCb, passCb };
callbackHandler.handle(callbacks);
String username = nameCb.getName();
String password = new String(passCb.getPassword());
success = rdbmsValidate(username, password);
return(true);
}

在ConsoleCallbackHandler類的handle()方法中你可以看到Callback對象 是如何同用戶進行交互的:

public void handle(Callback[] callbacks)
throws java.io.IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
NameCallback nameCb = (NameCallback)callbacks[i];
System.out.print(nameCb.getPrompt());
String user=(new BufferedReader(new
InputStreamReader(System.in))).readLine();
nameCb.setName(user);
} else if (callbacks[i] instanceof PasswordCallback) {
PasswordCallback passCb = (PasswordCallback)callbacks[i];
System.out.print(passCb.getPrompt());
String pass=(new BufferedReader(new
InputStreamReader(System.in))).readLine();
passCb.setPassword(pass.toCharArray());
} else {
throw(new UnsupportedCallbackException(callbacks[i],
"Callback class not supported"));
}
}
}

使用JSP和關系數據庫進行登錄驗證

現在我們希望將通過命令行調用的程序一直到Web應用程序中。由於Web應用 程序與一般的應用程序的交互方式有區別不同,我們將不能使用JAAS提供的標准 Callback和CallbackHandler類。因為我們不能在Web程序中打開一個命令窗口讓 用戶輸入信息。也許你會想到我們也可以使用基於HTTP的驗證,這樣我們可以從 浏覽器彈出的用戶名/密碼窗口中獲得用戶輸入。但是這樣做也有一些問題,它 要求建立起雙向的HTTP連接(在login()方法很難實現雙向連接)。因此在我 們的例子中我們會使用利用表單進行登錄,從表單中獲取用戶輸入的信息,然後 通過RdbmsLoginModule類驗證它。

由於我們沒有在應用層同LoginModule直接打交道,而是通過LgoinContext來 調用其中的方法的,我們如何將獲得用戶名和密碼放入LoginModule對象中呢? 我們可以使用其它的方法來繞過這個問題,例如我們可以在創建LoginContext對 象前先初始化一個Subject對象,在Subject對象的憑證中保存用戶名和密碼。然 後我們可以將該Subject對象傳遞給LoginContext的構造函數。這種方法雖然從 技術上來說沒有什麼問題,但是它在應用程序層增加了很多與安全機制相關的代 碼。而且通常是在驗證後向Subject送入憑證,而不是之前。

前面我們提到可以實現一個CallbackHandler類,然後將它的實例傳遞給 LoginContext對象。在這裡我們可以采用類似的方法來處理用戶名和密碼。我們 實現了一個新的類PassiveCallbackHandler。下面是在JSP中使用該類的代碼:

String user = request.getParameter("user");
String pass = request.getParameter("pass");
PassiveCallbackHandler cbh = new PassiveCallbackHandler(user, pass);
LoginContext lc = new LoginContext("Example", cbh);

PassiveCallbackHandler中構造函數的參數包含了用戶名和密碼。因此它可 以在Callbick對象中設定正確的值。下面是PassiveCallbackHandler類的handle ()方法的代碼:

public void handle(Callback[] callbacks)
throws java.io.IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
NameCallback nameCb = (NameCallback)callbacks[i];
nameCb.setName(user);
} else if(callbacks[i] instanceof PasswordCallback) {
PasswordCallback passCb = (PasswordCallback)callbacks[i];
passCb.setPassword(pass.toCharArray());
} else {
throw(new UnsupportedCallbackException(callbacks[i],
"Callback class not supported"));
}
}
}
}

從上面的代碼中可以發現實際上我們只是從ConsoleCallbackHandler中去除 了那些提示用戶輸入的代碼。

在運行這個JSP例子的時候,我們需要設定系統屬性,這樣LgoinContext對象 才知道如何查找名稱為"Example"的配置。我們使用的是Resin服務器。在 resin.conf中,我們增加了一個<caucho.com>節點:

<system-property
java.security.auth.login.config="/resin/conf/jaas.config"/>

小結

JAAS通過提供動態的、可擴展的模型來進行用戶驗證和控制權限,從而使應 用程序有更加健壯的安全機制。同時它還能夠讓你能夠很輕松地創建自己的登錄 機制。JAAS可以同時在在客戶端和服務器端應用程序上工作。雖然在服務器端的 JAAS到目前還不是很穩定,但是隨著技術的發展,相信會很好地解決這個問題。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved