Java平臺是創建企業應用程序的普遍選擇。它所以受歡迎,主要原因之一是在創建Java語言時充分考慮了安全性,而且市場上也普遍認爲Java是一種"安全的"語言。Java平臺在兩個層次上提供安全性:語言層次安全性和企業層次安全性。
1.語言層次安全性
最初的Java(JDK1.2)平臺採用沙箱安全模型,基本安全模型由三部分來承擔,這三部分構成Java運行環境的三個安全組件,分別是:類加載器,文件校驗器,安全管理器
1.1類加載器
類加載器負責從特定位置加載類,類加載器是JVM的看門人,控制着哪些代碼被加載或被拒絕,類加載器首先進行安全性檢查,它往往從以下幾個方面去檢查,裝入的字節碼是否生成指針,裝入的字節碼是否違反訪問限制,裝入的字節碼是否把對象當作它們本身來訪問,它在確保執行代碼的安全性方面至關重要。
1.2類文件校驗器的校驗
類文件校驗器負責檢查那些無法執行的明顯有破壞性的操作,類文件校驗器執行的一些檢查通常有:變量要在使用之前進行初始化;方法調用和對象引用類型之間要匹配;沒有違反訪問私有數據和方法的規則;對本地變量的訪問都在運行時堆棧內;運行時堆棧是否溢出.如果以上檢查中任何一條沒有通過,就認爲該類遭到了破壞,不被加載。
1.3安全管理器
一旦某個類被類加載器加載到虛擬機中,並由類文件校驗器檢查過之後,JAVA的第三種安全機制安全管理器就會啓動,安全管理器是一個負責控制某個操作是否允許執行的類,安全管理器負責檢查包括以下幾個方面的操作:當前線程是否能創建一個新的類加載器;當前線程是否能終止JVM的運行;某個類是否能訪問另一個類的成員;當前線程是否能訪問本地文件;當前線程是否能打開到達外部記住的socket連接;某個類是否能啓動打印作業;某個類是否能訪問系統剪貼板;某個類是否能訪問AWT事件隊列;當前線程是否可被信任以打開一頂層窗口。
儘管Java安全的支柱類加載器、類文件校驗器、安全管理器每一個都有獨特的功能,但它們又相互依賴、相輔相承。共同保證了Java語言的安全性。
2.企業層次的安全特性
即是構建安全的J2EE應用。Java平臺在提供語言安全性的同時還提供其他API功能,爲企業應用程序提供一個總體的安全性解決方案。下面將介紹幾種方案。
2.1 Java加密擴展(JCE)
JCE是一組包,爲加密、密鑰生成、密鑰協商和消息身份驗證代碼(MAC)算法提供一種框架和實現。JCE支持多種類型的加密,包括對稱的、非對稱的、塊和流密碼。在JDK 1.4之前,JCE是一個可選的包,現在它已經成爲Java平臺的一個標準組成部分。
2.2 Java安全套接字擴展(JSSE)
JSSE是支持安全的Internet通信的一組包。它是實現了SSL和傳輸層安全(TLS)協議的JAVA技術。它包括用於數據加密、服務器身份驗證、消息完整性和可選的客戶端身份驗證的諸多功能。JSSE已被集成到JDK 1.4以上版本的平臺中。
2.3 Java身份驗證和授權規範(JAAS)
JAAS通過對運行程序的用戶的進行驗證,從而達到保護系統的目的。JAAS主要由兩個部件構成,認證和授權,JAAS通過一個配置文件來定義認證機制。認證模塊是基於可插的認證模塊而設計的,它可以運行在客戶端和服務器端,授權模塊的設計是一個變化的過程,爲了對資源的訪問請求進行授權,首先需要應用程序認證請求的資源,subject術語來表示請求的資源,用java.security.auth.Subject類來表示subject。subject一旦通過了認證,就會和身份和主體想關聯。在JAAS中將主體表示爲javax.security.Principal對象,一個subject可能包含多個主題,除了和主題相關聯外,subject還可能擁有與安全相關的屬性或證書,證書是用戶的數據,它包含這樣的認證信息即認證subject所擁有的其他服務的信息。基於J2EE的分佈式應用程序使用
JAAS一般有兩種情況:第一種情況,一個單獨的應用系統與一個遠程的EJB系統連接,用戶必須嚮應用系統提供證明身份的信息或應用系統向文件和其它的系統來檢索可證明身份的信息。這個單獨的應用系統將在調用EJB組件之前使用JAAS來驗證用戶,由應用服務器完成驗證的任務。只有當用戶通過JAAS的驗證之後,客戶端程序纔可被信任地調用EJB方法。第二種情況,基於Web瀏覽器的客戶端程序連接到Servlet/JSP層,客戶端用戶將向Servlet/JSP層提供證明身份的信息,而Servlet/JSP層可以採用JAAS驗證用戶。Web客戶端一般可以採
用基本驗證、基於表格的驗證、摘要驗證、證書驗證等方式來提供證明身份的信息。這種支持選擇不同認證方法的靈活性有助於支持在管理員層實施更爲複雜的安全策略,而不是在編程層上去實現。一旦客戶端通過應用服務器認證,安全上下文環境能被傳播到EJB層。在應用程序中使用JAAS驗證通常會涉及到以下幾個步驟:
1.創建一個LoginContext的實例。並傳遞LoginModule配置
文件程序段和CallbackHandler的名稱。
2.爲了能夠獲得和處理驗證信息,將一個CallbackHandler
對象作爲參數傳送給LoginContext。
3.通過調用LoginContext的login()方法來進行驗證。
4.通過使用login()方法返回的Subject對象實現一些特殊
的功能(假設登錄成功)。
舉個例子:
LoginModel是jaas的一個核心接口,她負責實施用戶認證。同時暴漏了initialize(),login(),commit(),abort(),logout()方法。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author worldheart * */ public class UsernamePasswordCallbackHandler implements CallbackHandler { protected static final Log log = LogFactory.getLog(UsernamePasswordCallbackHandler.class); public void handle(Callback callbacks[]) throws IOException, UnsupportedCallbackException { log.info("進入handle()........................"); for (Callback cb: callbacks) { if (cb instanceof NameCallback) { NameCallback nc = (NameCallback) cb; log.info(nc.getPrompt()); //採集用戶名 String username = (new BufferedReader(new InputStreamReader( System.in))).readLine(); nc.setName(username); } else if(cb instanceof PasswordCallback){ PasswordCallback pc = (PasswordCallback) cb; log.info(pc.getPrompt()); //採集用戶密碼 String password = (new BufferedReader(new InputStreamReader( System.in))).readLine(); pc.setPassword(password.toCharArray()); } } } } 一旦用戶收集到用戶賬號後NameCallback,PasswordCallback對象都會存儲他們,與此同時,上述login()方法會基於賬號信構建UsernamePasswordPrincipal對象,並保留在登錄模塊中,而且login()會返回true,當login方法順利完成用戶憑證信息的收集工作後,commit會被觸發,她將UsernamePasswordPrincipal對象擺到Subject對象中。 當login方法未能順利完成用戶憑證信息的收集工作後,abort會被觸發,將principal等信息破換掉。當登錄用戶完滿的完成自身的業務操作後便可以考慮退出當前的應用,調用logout方法。下面是Principal對象: package sample; import java.security.Principal; /** * * @author worldheart * */ //Acegi中的Authentication接口繼承了Principal接口 public class UsernamePasswordPrincipal implements Principal { private String username; private String password; //存儲用戶名、密碼,比如marissa/koala public UsernamePasswordPrincipal(String username, String password) { this.username = username; this.password = password; } public String getName() { return this.username; } public String toString() { return this.username + "->" + this.password; } } 爲了使用上述登錄模塊,需要準備一個jaas配置文件: Loginmodel.conf放在src下面 ScreenContent { sample.ScreenContentLoginModule required; }; 客戶應用: package sample; import java.io.File; import java.security.PrivilegedAction; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author worldheart * */ public class JaasSecurityClient { protected static final Log log = LogFactory .getLog(JaasSecurityClient.class); public static void main(String argv[]) throws LoginException, SecurityException { LoginContext ctx = null; ctx = new LoginContext("ScreenContent", new UsernamePasswordCallbackHandler()); //marissa用戶登錄到當前應用中 ctx.login(); log.info("當前用戶已經通過用戶認證"); Subject subject = ctx.getSubject(); log.info(subject); // log.info("啓用JAAS用戶授權能力"); // log.info("臨時目錄爲," + Subject.doAsPrivileged(subject, new PrivilegedAction() { // public Object run() { // log.info("當前用戶正在經過JAAS授權操作的考驗,並正調用目標業務操作"); // new File("D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf").exists(); // return System.getProperty("java.io.tmpdir"); // } // }, null)); // 退出當前已登錄marissa用戶 ctx.logout(); } } 在運行客戶應用之前還需要提供JVM參數,即引用到loginmoudel.conf配置文件: -Djava.security.auth.login.config=src/loginmoudel.conf 或者通過javahome/jre/lib/security目錄中的java.security配置文件指定上述loginmoudel.conf配置文件: #login.config.url.l=file:${user.home}/.java.login.config login.config.url.l=file:d:/eclipse/src/loginmoudel.conf SecurityContextLoginModule是Acegi內置的一個LoginModel實現,當開發Jaas應用時,用戶憑證信息的獲取可能來自Acegi,此時,我們便可以採用內置的SecurityContextLoginModel。要使用SecurityContextLoginModule,我們需要在Jaas配置文件中配置它: ACEGI { org.acegisecurity.providers.jaas.SecurityContextLoginModule required ignoreMissingAuthentication=true; }; 客戶端應用: package sample; import java.io.File; import java.security.PrivilegedAction; import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * * @author worldheart * */ public class AcegiSecurityClient { protected static final Log log = LogFactory .getLog(AcegiSecurityClient.class); public static void main(String argv[]) throws LoginException, SecurityException { LoginContext ctx = null; //在實際企業應用中,Authentication對象的構建形式多種多樣 SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken("marissa", "koala")); ctx = new LoginContext("ACEGI"); // marissa用戶登錄到當前應用中 ctx.login(); log.info("當前用戶已經通過用戶認證"); Subject subject = ctx.getSubject(); log.info(subject); // log.info("啓用JAAS用戶授權能力"); // log.info("臨時目錄爲," // + Subject.doAsPrivileged(subject, new PrivilegedAction() { // public Object run() { // log.info("當前用戶正在經過JAAS授權操作的考驗,並正調用目標業務操作"); // new File( // "D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf") // .exists(); // return System.getProperty("java.io.tmpdir"); // } // }, null)); // 退出當前已登錄marissa用戶 ctx.logout(); //清除已註冊的SecurityContext SecurityContextHolder.clearContext(); } } 注意到我們並未爲LoginContext提供CallbackHandler對象,由於Acegi負責提供兼容於Principal的Authentication對象,因此用戶憑證的收集也不用CallbackHandler操心了。 在運行客戶應用之前還需要提供JVM參數,即引用到loginmoudel.conf配置文件: -Djava.security.auth.login.config=src/loginmoudel.conf 或者通過javahome/jre/lib/security目錄中的java.security配置文件指定上述loginmoudel.conf配置文件: #login.config.url.l=file:${user.home}/.java.login.config login.config.url.l=file:d:/eclipse/src/loginmoudel.conf 啓用Java安全管理器:大部分java開發者都知道,藉助如下JVM參數能夠啓用java安全管理器,-Djava.security.manager。既然如此,我們通過如下JVM參數運行JaasSecurityClient客戶端和AcegiSecurityClient客戶端: -Djava.security.manager -Djava.security.auth.login.config=src/loginmodule.conf 但是這樣會出錯:java.security.auth.login.config.AccessControlException:access denied 出錯原因:默認時,直接藉助“-Djava.security.manager”啓動java安全管理器,JVM會採用javahome/jre/lib/security中的java.policy策略文件,而這一策略文件並未對上述涉及到的各種權限(比如:createLoginContext.ScreenContent,讀取acegi.security.strategyJava屬性)進行授權因此拋出了異常。 爲此我們可以提供新的授權信息jaassecuritypolicy.txt策略文件。由於我們需要同LoginContext進行各類操作因此需要提供相關AuthPermission權限給Acegi SecurityClient,同時我們使用了Commons-Logging,Log4j管理日誌,因此還必須將相應的操作權限給這一客戶,在操作日誌的過程中,客戶應用需要操控的:d:/ddlog.log日誌文件因此需要將讀寫權限授給這一客戶應用。 grant codebase "file:./-"{ permission java.io.FilePermission "D:/contactsforchapter8.log", "read, write"; permission javax.security.auth.AuthPermission "createLoginContext"; permission javax.security.auth.AuthPermission "modifyPrincipals"; }; grant codeBase "file:D:/eclipse/workspace/contactsforchapter8/context/WEB-INF/lib/log4j-1.2.14.jar" { permission java.security.AllPermission; }; grant codeBase "file:D:/eclipse/workspace/contactsforchapter8/context/WEB-INF/lib/commons-logging-1.0.4.jar" { permission java.security.AllPermission; }; 實際上java的策略文件編寫可以通過policytool工具。 運行JaasSecurityClient客戶端應用: -Djava.security.manager -Djava.security.policy=src/jaassecuriypolicy.txt -Djava.security.auth.login.config=src/loginmodule.conf 類似的運行AcegiSecurityClient的策略文件: grant codebase "file:./-"{ permission java.util.PropertyPermission "acegi.security.strategy", "read"; permission java.io.FilePermission "D:/contactsforchapter8.log", "read, write"; permission javax.security.auth.AuthPermission "createLoginContext"; permission javax.security.auth.AuthPermission "modifyPrincipals"; }; grant codeBase "file:D:/eclipse/workspace/contactsforchapter8/context/WEB-INF/lib/log4j-1.2.14.jar" { permission java.security.AllPermission; }; grant codeBase "file:D:/eclipse/workspace/contactsforchapter8/context/WEB-INF/lib/commons-logging-1.0.4.jar" { permission java.security.AllPermission; }; 運行AcegiSecurityClient客戶端應用: -Djava.security.manager -Djava.security.policy=src/acegisecuriypolicy.txt -Djava.security.auth.login.config=src/loginmodule.conf 啓用Jaas的用戶授權功能:jaas的授權能力依賴java策略文件,下面提供了另一個版本的jaasSecurityClient客戶應用,新增了兩行java代碼: LoginContext ctx = null; ctx = new LoginContext("ScreenContent", new UsernamePasswordCallbackHandler()); //marissa用戶登錄到當前應用中 ctx.login(); log.info("當前用戶已經通過用戶認證"); Subject subject = ctx.getSubject(); log.info(subject); new File("D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf").exists(); System.getProperty("java.io.tmpdir"); // 退出當前已登錄marissa用戶 ctx.logout(); 此時開發者必須往jaassecuritypolicy.txt策略文件中添加如下權限到其中: permission java.io.FilePermission "D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf", "read"; permission java.util.PropertyPermission "java.io.tmpdir", "READ"; 如果客戶要求只具有marissa用戶纔有權利運行上述兩行代碼,那麼應該這樣: LoginContext ctx = null; ctx = new LoginContext("ScreenContent", new UsernamePasswordCallbackHandler()); //marissa用戶登錄到當前應用中 ctx.login(); log.info("當前用戶已經通過用戶認證"); Subject subject = ctx.getSubject(); log.info(subject); // log.info("啓用JAAS用戶授權能力"); // log.info("臨時目錄爲," + Subject.doAsPrivileged(subject, new PrivilegedAction() { // public Object run() { // log.info("當前用戶正在經過JAAS授權操作的考驗,並正調用目標業務操作"); // new File("D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf").exists(); // return System.getProperty("java.io.tmpdir"); // } // }, null)); // 退出當前已登錄marissa用戶 ctx.logout(); 那麼jaassecuritypolicy.txt策略文件應該添加如下內容: grant codebase "file:./-", Principal sample.UsernamePasswordPrincipal "marissa" { permission java.io.FilePermission "D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf", "read"; permission java.util.PropertyPermission "java.io.tmpdir", "READ"; }; 啓動jaassecurityclient客戶端: -Djava.security.manager -Djava.security.policy=src/jaassecuriypolicy.txt -Djava.security.auth.login.config=src/loginmodule.conf 那麼對於acegisecurityclient客戶應用,acegisecuritypolicy.txt應該增加: grant codebase "file:./-", Principal org.acegisecurity.providers.UsernamePasswordAuthenticationToken "marissa" { permission java.io.FilePermission "D:/eclipse/workspace/contactsforchapter8/src/loginmodule.conf", "read"; permission java.util.PropertyPermission "java.io.tmpdir", "read"; }; 啓動: -Djava.security.manager -Djava.security.policy=src/acegisecuriypolicy.txt -Djava.security.auth.login.config=src/loginmodule.conf 直擊JaasAuthenticationProvider 配置: <bean id="jaasAuthenticationProvider" class="org.acegisecurity.providers.jaas.JaasAuthenticationProvider"> <property name="authorityGranters" <bean class="sample.TestAuthorityGranter"/> </property> <property name="callbackHandlers" <list> <bean class="org.acegisecurity.providers.jaas.JaasNameCallbackHandler"/> <bean class="org.acegisecurity.providers.jaas.JaasPasswordCallbackHandler"/> </property> <property name="loginConfig" value="classpath:acegi.conf"/> <property name="liginContextName" value="ACEGI"/> </bean> 另外需要將JaasAuthenticationProvider添加到認證管理器: acegi.conf的內容: ACEGI { sample.TestLoginModule required; }; 註釋:authorityGranters屬性能夠爲已經認證用戶提供角色映射信息,由於這裏的Jaas僅負責用戶認證,而授權仍然被acegi接管。TestAuthorityGranter實現類: package sample; import java.security.Principal; import java.util.HashSet; import java.util.Set; import org.acegisecurity.providers.jaas.AuthorityGranter; /** * * @author worldheart * */ public class TestAuthorityGranter implements AuthorityGranter { public Set grant(Principal principal) { Set<String> rtnSet = new HashSet<String>(); if (principal.getName().equals("TEST_PRINCIPAL")) { rtnSet.add("ROLE_USER"); rtnSet.add("ROLE_ADMIN"); } return rtnSet; } } 下面是TestLoginModel類: package sample; import java.security.Principal; import java.util.Map; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; /** * * @author worldheart * */ public class TestLoginModule implements LoginModule { private String user; private String password; private Subject subject; public boolean abort() throws LoginException { return true; } public boolean commit() throws LoginException { return true; } public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { this.subject = subject; try { NameCallback nameCallback = new NameCallback("prompt"); PasswordCallback passwordCallback = new PasswordCallback("prompt", false); callbackHandler.handle(new Callback[] {nameCallback, passwordCallback }); user = nameCallback.getName(); password = new String(passwordCallback.getPassword()); } catch (Exception e) { throw new RuntimeException(e); } } public boolean login() throws LoginException { if (!user.equals("marissa")) { throw new LoginException("用戶名不對"); } if (!password.equals("koala")) { throw new LoginException("密碼不對"); } subject.getPrincipals().add(new Principal() { public String getName() { return "TEST_PRINCIPAL"; } }); subject.getPrincipals().add(new Principal() { public String getName() { return "NULL_PRINCIPAL"; } }); return true; } public boolean logout() throws LoginException { return true; } } |