|
安全域是Tomcat服務(wù)器用來保護(hù)Web應(yīng)用的資源的一種機(jī)制。在安全域中可以配置安全驗證信息,即用戶信息(包括用戶名和口令)以及用戶和角色的映射關(guān)系。每個用戶可以擁有一個或多個角色,每個角色擁有不同的可以訪問的Web資源。一個用戶可以訪問其擁有的所有角色對應(yīng)的Web資源。
引入角色的原因:角色一方面代表一系列用戶,另外一方面可以代表一系列權(quán)限,因此可以說是用戶和權(quán)限的結(jié)合體。引入角色的概念主要是為了分離用戶和訪問權(quán)限的直接聯(lián)系。用戶與訪問權(quán)限的直接組合可能是短暫的,而角色則可以相對穩(wěn)定,這樣用戶的變化只要涉及到角色就可以,無需考慮權(quán)限。而權(quán)限的變化只涉及到角色,無需考慮用戶或用戶組。
安全域是Tomcat的內(nèi)置功能,在org.apache.catalina.Realm接口中聲明了把一組用戶名、口令及所關(guān)聯(lián)的角色集成到Tomcat中的方法。Tomcat 5提供了4個實現(xiàn)這一接口的類,它們分別代表了五種安全域類型:
|
安全域類型
|
類名
|
描述
|
|
內(nèi)存域
|
MemoryRealm
|
在初始化階段,從XML文件中讀取安全認(rèn)證信息,并把它們以一組對象的形式存放在內(nèi)存中
|
|
JDBC域
|
JDBCRealm
|
通過JDBC驅(qū)動程序訪問存放在數(shù)據(jù)庫中的安全驗證信息
|
|
數(shù)據(jù)源域
|
DataSourceRealm
|
通過JNDI數(shù)據(jù)源訪問存放在數(shù)據(jù)庫中的安全驗證信息
|
|
JNDI域
|
JNDIRealm
|
通過JNDI provider訪問存放在基于LDAP的目錄服務(wù)器中的安全驗證信息。
|
|
JAAS域
|
JAASRealm
|
通過JAAS的安全授權(quán)API,實現(xiàn)自己的安全認(rèn)證機(jī)制
|
要達(dá)到保護(hù)Web資源需要涉及到配置一些文件,但不管配置哪一種安全域,一般都包含以下步驟: (1)為Web資源設(shè)置安全約束,通過為Web資源設(shè)置安全約束,可以指定某種Web資源可以被哪些角色訪問。這個設(shè)置需要在Web應(yīng)用的web.xml文件中加入、和元素。
//聲明受保護(hù)的資源 The Entire Web Application
//標(biāo)識受保護(hù)的Web資源 *.jsp//指定受保護(hù)的URL路徑 *.do *.html *.htm
//可以訪問受保護(hù)資源的角色,可以包含多個角色 tomcat//指定可以訪問受保護(hù)資源的角色
//這個元素指定了當(dāng)Web客戶訪問受保護(hù)的Web資源時,系統(tǒng)彈出的登錄對話框的類型。 FORM//指定驗證方法。它有三個可選值:BASIC(基本驗證)、DIGEST(摘要驗證)、FORM(基于表單的驗證)。 Tomcat Supported Realm//設(shè)定安全域的名稱
//配置驗證網(wǎng)頁和出錯網(wǎng)頁
/login.jsp//設(shè)定驗證網(wǎng)頁
/login_fail.jsp//設(shè)定出錯網(wǎng)頁
//指明這個Web應(yīng)用引用的所有角色名字。 An example role defined in "conf/tomcat-users.xml" tomcat//設(shè)定tomcat角色
以上安全約束代碼指定了tomcat角色才能訪問admin應(yīng)用中的*.jsp、*.do、*.htm、*.html資源。
基本驗證
如果采用基本驗證,當(dāng)客戶訪問受保護(hù)的資源時,瀏覽器會先彈出一個對話框,要求用戶輸入用戶名和密碼,如果輸入正確,Web服務(wù)器就允許他訪問這些資源;否則,在連接3次嘗試失敗后,會顯示一個錯誤信息頁面。這個方法的缺點是把用戶名和密碼從客戶端傳送到Web服務(wù)器時,在網(wǎng)絡(luò)上傳送的數(shù)據(jù)采用Base64編碼(全是可讀文本),因此這種驗證方法不是非常安全。可以采用一些安全措施來克服這個弱點。例如在傳輸層上應(yīng)用SSL(安全套接層(Secure Sockets Layer)為驗證過程提供了數(shù)據(jù)加密,服務(wù)器端認(rèn)證,信息真實性等方面的安全保證。在此驗證方式中,客戶端必須提供一個公鑰證書,你可以把這個公鑰證書看作是你的數(shù)字護(hù)照。公鑰證書也稱數(shù)字證書,它是被稱作證書授權(quán)機(jī)構(gòu)(CA)——一個被信任的組織頒發(fā)的。這個數(shù)字證書必須符合X509公鑰體系結(jié)構(gòu)(PKI)的標(biāo)準(zhǔn)。如果你指定了這種驗證方式,Web服務(wù)器將使用客戶端提供的數(shù)字證書來驗證用戶的身份。)或者在網(wǎng)絡(luò)層上使用IPSEC或VPN技術(shù)。
摘要驗證 摘要驗證與基本驗證的不同在于:摘要驗證不會在網(wǎng)絡(luò)中直接傳輸用戶密碼,而是首先采用MD5(Message Digest Algorithm)對用戶密碼進(jìn)行加密,然后傳輸加密后的數(shù)據(jù),所有這種方法顯得更為安全。 基于表單的驗證 基于表單的驗證使系統(tǒng)開發(fā)者可以自定義用戶的登陸頁面和報錯頁面。用戶在表單中填寫用戶名和密碼,而后密碼以明文形式在網(wǎng)路中傳遞,如果在網(wǎng)路的某一節(jié)點將此驗證請求截獲,在經(jīng)過反編碼很容易就可以獲取用戶的密碼。因此在使用基本HTTP的驗證方式和基于表單的驗證方法時,一定確定這兩種方式的弱點對你的應(yīng)用是可接受的。但有個規(guī)定:用戶名對應(yīng)的文本框必須命名為:j_username,密碼對應(yīng)的文本框必須命名為:j_password,并且表單的aciton的值必須為:j_security_check。
下面分別介紹4種安全域:
1、內(nèi)存域(MemoryReal) 內(nèi)存域是由org.apache.catalina.realm.MemoryRealm類來實現(xiàn)的。MermoryRealm從一個XML文件中讀取用戶信息。默認(rèn)情況下,該XML文件為tomcat安裝目錄的conf/tomcat_users.xml。
在這個文件中定義了每個用戶擁有的角色。 要采用這個類型的安全域進(jìn)行web資源保護(hù)的設(shè)置步驟為: (1) 按上面的步驟在你的應(yīng)用程序的web.xml中配置安全約束,可以采用三種驗證(Basic、Digest、基于表單(Form))中的任一種。 (2) 在tomcat_user.xml文件中定義用戶、角色以及兩者的映射關(guān)系。 (3) 在server.xml中加入相應(yīng)的元素。
2、JDBC域 JDBCRealm通過JDBC驅(qū)動程序訪問存放在關(guān)系型數(shù)據(jù)庫中的安全認(rèn)證信息,JDBC域使得安全配置非常靈活。當(dāng)修改了數(shù)據(jù)庫中的安全認(rèn)證信息后,不必重啟tomcat服務(wù)器(兩者是相互獨立的)。 當(dāng)用戶第一次訪問受保護(hù)的資源時,Tomcat將調(diào)用Reaml的authenticate()方法,該方法從數(shù)據(jù)庫中讀取最新的安全認(rèn)證信息。該用戶通過認(rèn)證之后,在用戶訪問Web資源期間,用戶的各種驗證信息被保存在緩存中。 JDBC域設(shè)置步驟為: (1)、按上面的步驟在你的應(yīng)用程序的web.xml中配置安全約束,可以采用三種驗證(Basic、Digest、基于表單(Form))中的任一種。 (2)、創(chuàng)建數(shù)據(jù)庫和創(chuàng)建兩張表:users(定義用戶信息,包括用戶名和口令)和users_roles(定義用戶和角色的映射關(guān)系)。 (3)、將所用的數(shù)據(jù)庫驅(qū)動程序拷貝到Tomcat安裝目錄下/common/lib中。 (4)、配置元素,在server.xml中的中加入如下的元素:如果中已經(jīng)有元素了,應(yīng)該將其注釋掉。 driverName=”com.mysql.jdbc.Driver” debug=”99”//數(shù)據(jù)庫驅(qū)程,debug設(shè)定跟蹤級別 connectionURL=”jdbc:mysql://localhost/database” //數(shù)據(jù)庫的URL connectionName=”admin” connectionPassword=”8888” //數(shù)據(jù)庫連接的用戶名和口令 userTable=”users” userNameCol=”user_name” userCredCol=”user_pass”//指定用戶表 userRoleTable=”user_roles” roleNameCol=”role_name” />//指定用戶和角色映射關(guān)系表
3、DataSource域 DataSource域和JDBCRealm域的兩者的不同是訪問數(shù)據(jù)庫的方式不一樣:DataSource通過 JNDI來訪問數(shù)據(jù)庫。而JDBCRealm通過JDBC驅(qū)動程序來訪問數(shù)據(jù)庫。
區(qū)別:通過JDBC API連結(jié)數(shù)據(jù)庫是一種最原始、最直接的方法。而DataSource封裝了通過JDBC API來連結(jié)數(shù)據(jù)庫的細(xì)節(jié),它采用數(shù)據(jù)庫連結(jié)池機(jī)制,把可用的數(shù)據(jù)庫連結(jié)保存在緩存中,避免每次訪問數(shù)據(jù)庫都建立數(shù)據(jù)庫連結(jié),這樣可以提高訪問數(shù)據(jù)庫的效率。
適用場合: 在任何Java應(yīng)用中,都可以直接通過JDBC API連結(jié)數(shù)據(jù)庫,如果需要的話,可以手工編程實現(xiàn)數(shù)據(jù)庫連結(jié)池。當(dāng)Java應(yīng)用運行在JavaWeb容器或EJB容器中時,可以優(yōu)先考慮使用由容器提供的DataSource。以Tomcat容器為例,DataSource實例被作為JNDI資源發(fā)布到Tomcat容器中,Tomcat容器負(fù)責(zé)維護(hù)DataSource實例的生命周期,Java Web應(yīng)用通過JNDI來獲得DataSource實例的引用。
JNDI(The Java Naming and Directory Interface,Java命名和目錄接口)是一組在Java應(yīng)用中訪問命名和目錄服務(wù)的API。命名服務(wù)將名稱和對象聯(lián)系起來,使得我們可以用名稱訪問對象。目錄服務(wù)是一種命名服務(wù),在這種服務(wù)里,對象不但有名稱,還有屬性。
DataSource域配置步驟: (1)、按上面的步驟在你的應(yīng)用程序的web.xml中配置安全約束,可以采用三種驗證(Basic、Digest、基于表單(Form))中的任一種。 (2)、創(chuàng)建數(shù)據(jù)庫和創(chuàng)建兩張表:users(定義用戶信息,包括用戶名和口令)和users_roles(定義用戶和角色的映射關(guān)系)。 (3)、將所用的數(shù)據(jù)庫驅(qū)動程序拷貝到Tomcat安裝目錄下/common/lib中。 (4)、數(shù)據(jù)源的配置 數(shù)據(jù)源的配置涉及修改server.xml和web.xml文件。 A、 在server.xml中元素下加入和 元素。例如:
…… …… auth="Container" //指定管理Resource的Manager,又兩個可選值:Container和//Application。Container表示由容器來創(chuàng)建和管理//Resource,而Application表示由應(yīng)用程序//來創(chuàng)建和管理Resource。 type="javax.sql.DataSource"/> //指定Resource所屬的java類名 //指定配置TestDB數(shù)據(jù)源的參數(shù)
factory //指定生成DataSource的factory的類名 org.apache.commons.dbcp.BasicDataSourceFactory
maxActive 100
maxIdle 30
maxWait 10000
username test
password test
driverClassName org.gjt.mm.mysql.Driver
url jdbc:mysql://localhost:3306/test
注意:Tomcat的JNDI資源必須配置在元素下,服務(wù)器才能找到,否則會出現(xiàn)NameNotFoundException;低于Tomcat5.0.12的版本,即使正確配置了DataSourceRealm,也會出現(xiàn)找不到JNDI DataSource的異常,這個小貓的一個bug;在web.xml中是不需要配置元素的,因為Web應(yīng)用并不會訪問這個DataSource。(5)在server.xml添加和JDBC域幾乎相同的代碼: driverName="com.mysql.jdbc.Driver" debug="99" connectionURL="jdbc:mysql://localhost/tomcatusers" connectionName="admin" connectonPassword="" userTable="users" userNameCol="user_name" userCredCol="user_pass" userRoleTable="user_roles" roleNameCol="role_name"/>
javax.naming.Context提供了查找JNDI Resource的接口,可通過如下的代碼獲得數(shù)據(jù)源的引用和連接數(shù)據(jù)庫: Context ctx = new Context(); DatatSource ds = (DataSource)ctx.lookup(“java:comp/env/jdbc/TestDB”); Connection conn = ds.getConnection(); java:comp/env代表你的JVM的環(huán)境
4、JNDIRealm JNDI Directory Realm將Catalina連接到一個LDAP目錄,通過正確的JNDI驅(qū)動訪問。LDAP目錄存儲了用戶名,密碼以及他們相應(yīng)的角色。LDAP(輕量級目錄訪問協(xié)議)的英文全稱是Lightweight Directory Access Protocol,LDAP是用來訪問存儲在信息目錄(也就是LDAP目錄)中的信息的協(xié)議。更為確切和正式的說法應(yīng)該是 “通過使用LDAP,可以在信息目錄的正確位置讀取(或存儲)數(shù)據(jù)”。LDAP最大的優(yōu)勢是:可以在任何計算機(jī)平臺上,用很容易獲得的而且數(shù)目不斷增加的LDAP的客戶端程序訪問LDAP目錄。而且也很容易定制應(yīng)用程序為它加上LDAP的支持。
JNDI Realm支持許多使用LDAP進(jìn)行認(rèn)證的方法: a、realm可以使用模式來決定用戶目錄條目的唯一名字(distinguished name),或者搜索目錄來定位該條目; b、realm可以將用戶條目的唯一名字和用戶給出的密碼綁定到目錄上,對用戶進(jìn)行認(rèn)證;或者,從用戶條目中取出密碼,在本地進(jìn)行比較; c、在目錄中,角色可以以單獨的條目存在(比如,用戶所屬的組條目),或者,角色可以是用戶條目的一個屬性,或者兩種情況都是; 除了到目錄的連接,用戶從目錄中獲取信息的元素和屬性名稱以外,Directory Realm還支持很多其他的附加屬性: 屬性 描述 authentication 使用的認(rèn)證類型,字符串類型??梢允恰皀one”,“simple”,“strong”或者提供者定義的其他類型,如果沒有值,使用提供者提供的缺省值。
connectionName 創(chuàng)建目錄連接使用的目錄用戶名。如果沒有指定,使用匿名連接,這在大多數(shù)情況下就足夠了,除非你指定了userPassword屬性
connectionPassword 創(chuàng)建目錄連接使用的目錄密碼。如果沒有指定,使用匿名連接,這在大多數(shù)情況下就足夠了,除非你指定userPassword屬性。
connectionURL 創(chuàng)建目錄連接時,傳遞給JNDI驅(qū)動的連接URL。
contextFactory 用來取得JNDI InitialContext的工廠類的Java類名。缺省情況下,假定使用標(biāo)準(zhǔn)的JNDI LDAP提供者。
protocol 使用的安全協(xié)議。如果沒有指定,使用提供者提供的缺省值。
roleBase 用于角色查找的基準(zhǔn)目錄條目。如果沒有指定,使用目錄上下文的頂級元素。
roleName 在角色查找中,包含角色名的屬性的名稱。另外,在用戶條目中,你可以使用userRoleName來指定包含額外角色名的屬性名稱,。如果沒有指定,不搜索角色,角色存在于用戶條目中。
roleSearch 用來進(jìn)行角色查找的LDAP過濾器表達(dá)式。使用{0}來代替用戶的唯一名稱,{1}來代替用戶名。如果沒有指定,不對角色進(jìn)行搜索,角色從用戶條目中由userRoleName指定的屬性得到。
roleSubtree 在查找與用戶相關(guān)聯(lián)的角色時,如果想搜索由roleBase屬性指定的元素的整個子樹,設(shè)為true。缺省值為false,只對頂級元素進(jìn)行搜索。
userBase 在使用userSearch表達(dá)式搜索用戶的時候的基準(zhǔn)元素。如果使用userPattern表達(dá)式進(jìn)行搜索,不使用這個屬性。
userPassword 在用戶條目中包含用戶密碼的屬性名。如果指定了這個值,JNDIRealm使用connectionName和connectionPassword屬性指定的值綁定到目錄,取出對應(yīng)的屬性,與被認(rèn)證的用戶給出的值進(jìn)行比較。如果不指定這個值,JNDIRealm會嘗試使用用戶條目的唯一名稱和用戶給出的密碼綁定到目錄,如果綁定成功,說明認(rèn)證成功。
userPattern 用戶目錄條目的唯一名稱的模式,{0}代表實際的用戶名。在唯一名稱包含用戶名,其他的項都相同的時候,可以使用這個屬性,而不是使用userSearch,userSubtree和userBase.
userRoleName 用戶目錄條目中的一個屬性名,該屬性包含了零個或多個指定給用戶的角色名的值。另外,如果角色以單獨的條目存在,可以使用roleName屬性來指定屬性名,在搜索目錄的時候,可以得到這個屬性。 如果不指定userRoleName,用戶的所有角色通過角色搜索得到;
userSearch 搜索用戶目錄條目時,使用的LDAP過濾表達(dá)式,{0}代表實際的用戶名,可以使用userSearch,userBase和userSubtree屬性一起來代替userPattern搜索目錄。
userSubtree 如果希望搜索由userBase屬性指定的元素的整個子樹,設(shè)為true,缺省值為false,即只搜索頂級元素。如果使用userPattern表達(dá)式,不使用這個屬性。
5、JAASRealm Java Authentication Authorization Service(JAAS,Java驗證和授權(quán)API)提供了靈活和可伸縮的機(jī)制來保證客戶端或服務(wù)器端的Java程序。JAAS是一個比較新的的Java API。在J2SE 1.3中,它是一個擴(kuò)展包;在J2SE 1.4中變成了一個核心包。通過實現(xiàn)以下兩個接口:javax.security.auth.spi.LoginModule 和javax.security.Principal,你可以實現(xiàn)自己的安全機(jī)制。
配置的步驟: a、實現(xiàn)自己的LoginMode、User和Role的類。 b、盡管不是JAAS要求,但你應(yīng)該通過javax.security.Principal這個接口實現(xiàn)一個單獨的類來區(qū)別users和roles,這樣tomcat就能區(qū)分哪個Principals是users和roles返回。 C、設(shè)置login.config文件告訴tomcat去哪里找指定的JVM。 D、配置web.xml文件,和前面的配置一樣。 E、在server.xml文件中加入<Realm>元素。 --------my.MyLoginModule.java--------------- package my; import java.util.Map; import java.security.Principal; import javax.security.auth.login.LoginContext; import javax.security.auth.Subject; import javax.security.auth.callback.*; import javax.security.auth.login.*; import javax.security.auth.spi.LoginModule; import java.io.IOException; public class MyLoginModule implements LoginModule { protected CallbackHandler callbackHandler = null; protected boolean committed = false; protected boolean debug = false; protected Map options = null; protected Principal principal = null; protected Map sharedState = null; protected Subject subject = null; protected void log(String message) { System.out.print("MyLoginModule: "); System.out.println(message); } public boolean abort() throws LoginException { log("abort"); return (true); } public boolean commit() throws LoginException { log("commit phase"); // If authentication was not successful, just return false if (principal == null){ log("no principal commit fails"); return (false); } if (!subject.getPrincipals().contains(principal)) subject.getPrincipals().add(principal); // add role principals subject.getPrincipals().add(new MyRolePrincipal("admin")); committed = true; log("commit succesful"); return (true); } public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { // Save configuration values this.subject = subject; this.callbackHandler = callbackHandler; this.sharedState = sharedState; this.options = options; } public boolean login() throws LoginException { log("login phase"); // Set up our CallbackHandler requests if (callbackHandler == null) throw new LoginException("No CallbackHandler specified"); Callback callbacks[] = new Callback[2]; callbacks[0] = new NameCallback("Username: "); callbacks[1] = new PasswordCallback("Password: ", false); // Interact with the user to retrieve the username and password String username = null; String password = null; try { callbackHandler.handle(callbacks); username = ((NameCallback) callbacks[0]).getName(); password = new String(((PasswordCallback) callbacks[1]).getPassword()); } catch (IOException e) { throw new LoginException(e.toString()); } catch (UnsupportedCallbackException e) { throw new LoginException(e.toString()); } if (!authenticate(username,password)) return false; principal = new MyPrincipal(username); return true; } public boolean logout() throws LoginException { subject.getPrincipals().remove(principal); committed = false; principal = null; return (true); } boolean authenticate(String s,String p){ return (s.compareTo("jaas") == 0) && (p.compareTo("jaas") == 0); } static public void main(String args[]) throws Exception{ LoginContext ctx = new LoginContext("TomCatAdminApplication"); ctx.login(); } } ------------------------------------------------ ---------my/MyPrincipal.java------------------- package my; public class MyPrincipal implements java.security.Principal { String m_Name = new String(""); public MyPrincipal(String name) { m_Name = name; } public boolean equals(Object another) { try { MyPrincipal pm = (MyPrincipal)another; return pm.m_Name.equalsIgnoreCase(m_Name); } catch(Exception e){ return false; } } public String getName() { return m_Name; } public int hashCode() { return m_Name.hashCode(); } public String toString() { return m_Name; } } ------my/MyRolePrincipal.java------------- package my; public class MyRolePrincipal extends MyPrincipal { /** Creates a new instance of MyRolePrincipal */ public MyRolePrincipal(String s) { super(s); } } -------------------------------------- 在 server.xml中配置 <Context ..Tomcat Administration..> <Realm className="org.apache.catalina.realm.JAASRealm" debug="3" appName="TomCatAdminApplication" userClassNames="my.MyPrincipal" roleClassNames="my.MyRolePrincipal"> </Realm> </Context>
|