導讀
Tomcat對於J2EE或Java web開發者而言絕不陌生,但說到Realm,可能有些人不太清楚甚至沒有聽說過,那么到底什么是Realm?簡單一句話就是:Realm是Tomcat中為web應用程序提供訪問認證和角色管理的機制。配置了Realm,你就不需要在程序中寫web應用登陸驗證代碼,不需要費力的管理用戶角色,甚至不需要你自己寫登陸界面。因此,使用Realm可以減輕開發者不少編程和管理負擔。下面從幾個方面簡單介紹Tomcat Realm,為Realm學習者提供一個入門級教程。
目錄
正文
1. 什么是Realm?
Realm,中文可以翻譯為“域”,是一個存儲用戶名,密碼以及和用戶名相關聯的角色的”數據庫”,用戶名和密碼用來驗證用戶對一個或多個web應用程序的有效性。你可以將Realm看做Unix系統里的group組概念,因為訪問應用程序中特定資源的權限是被授予了擁有特殊角色的用戶,而不是相關的用戶名。通過用戶名相關聯,一個用戶可以有任意數量的角色。盡管Servlet規范描述了一個可以讓應用程序聲明它們安全性要求(在web.xml部署描述符里)的機制,但是並沒有的API來定義一個基於servlet容器和其相關用戶角色之間的接口。然而在許多情況下,最好能把一個servlet容器和那些已經存在的認證數據庫或機制“連接”起來。因此,Tomcat定義了一個Java接口(org.apache.catalina.Realm),它可以通過"插件"的形式來實現這種連接。
因此,可以通過現有數據庫里的用戶名、密碼以及角色來配置Tomcat,從而來支持容器管理的安全性(container managed security)。如果你使用一個網絡程序,而這個程序里包括了一個或多個
總結:說的簡單點就是Realm類似於Unix里面的group。在Unix中,一個group對應着系統的一組資源,某個group不能訪問不屬於它的資源。Tomcat用Realm來將不同的應用(類似系統資源)賦給不同的用戶(類似group),沒有權限的用戶則不能訪問相關的應用。
2. 如何配置使用Tomcat自帶的Realm?
Tomcat 7中提供了六種標准Realm,用來支持與各個認證信息來源的連接: * JDBCRealm - 通過JDBC驅動來訪問貯存在關系數據庫里的認證信息。 * DataSourceRealm - 通過一個叫做JNDI JDBC 的數據源(DataSource)來訪問貯存在關系數據庫里的認證信息。 * UserDatabaseRealm - 通過一個叫做UserDatabase JNDI 的數據源來訪問認證信息,該數據源通過XML文件(conf/tomcat-users.xml)來進行備份使用。 * JNDIRealm - 通過JNDI provider來訪問貯存在基於LDAP(輕量級目錄訪問協議)的目錄服務器里的認證信息。 * MemoryRealm - 訪問貯存在電腦內存里的認證信息,它是通過一個XML文件(conf/tomcat-users.xml)來進行初始化的。 * JAASRealm - 使用 Java Authentication & Authorization Service (JAAS)訪問認證信息。在使用標准Realm之前,弄懂怎樣配置一個Realm是很重要的。通常,你需要把一個XML元素加入到你的conf/server.xml配置文件中,它看起來像這樣:
<Realm className="... class name for this implementation"
... other attributes for this implementation .../>
在
元素里邊 - 這個域(Realm)將會被所有虛擬主機上的所有網絡程序共享,除非它被嵌套在下級 或 元素里的Realm元素覆蓋。
在元素里邊 - 這個域(Realm)將會被該虛擬主機上所有的網絡程序所共享,除非它被嵌套在下級 元素里的Realm元素覆蓋。
在元素里邊 - 這個域(Realm)只被該網絡程序使用。
如何使用各個標准Realm也很簡單,官方文檔也講的非常詳細,具體可以參考我下面給出的幾個參考資料。下面重點講如何配置使用我們自定義的Realm。
3. 如何配置使用我們自定義的Realm?
雖然Tomcat自帶的這六種Realm大部分情況下都能滿足我們的需求,但也有特殊需求Tomcat不能滿足的時候,比如我最近的一個需求就是:**我的用戶和密碼信息存儲在LDAP中,但用戶角色卻存儲在關系數據庫(PostgreSQL)中,那么如何認證呢?**我們知道Tomcat自帶的JNDIRealm可以實現LDAP認證,JDBCRealm可以實現關系數據庫認證,那么我們可不可以首先通過LDAP認證,認證通過后,到數據庫中讀取角色信息呢?答案是肯定的,就是自定義Realm實現我們的需求。我們所需要做的就是:
- 實現org.apache.catalina.Realm接口;
- 把編譯過的Realm放到 $CATALINA_HOME/lib里邊;
- 像上面配置標准realm一樣在server.xml文件中聲明你的realm;
- 在MBeans描述符里聲明你的realm。
下面我具體的以我自己的需求作為例子向大家演示如何自定義Realm並成功配置使用。
需求:自定義一個Realm,使得能夠像JNDIRealm一樣可以實現LDAP認證,又像JDBCRealm一樣可以從數據庫中讀取我們用戶的角色信息進行認證。
3.1 實現org.apache.catalina.Realm接口
從需求上看似乎我們可以將Tomcat自帶的JNDIRealm和JDBCRealm結合起來,各取所需,形成我們自己的Realm。是的,的確可以這樣,因此我們首先需要下載Tomcat的源碼,找到這兩個Realm的具體實現代碼,基本看懂后,提取出我們所需要的部分進行重構形成自己的Realm。比如我自定義的Realm中所需要的實例變量有以下這些: ```java // -----------------------------------------------Directory Server Instance Variables/**
* The type of authentication to use.
*/
protected String authentication = null;
/**
* The connection username for the directory server we will contact.
*/
protected String ldapConnectionName = null;
/**
* The connection password for the directory server we will contact.
*/
protected String ldapConnectionPassword = null;
/**
* The connection URL for the directory server we will contact.
*/
protected String ldapConnectionURL = null;
/**
* The directory context linking us to our directory server.
*/
protected DirContext context = null;
/**
* The JNDI context factory used to acquire our InitialContext. By
* default, assumes use of an LDAP server using the standard JNDI LDAP
* provider.
*/
protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
/**
* How aliases should be dereferenced during search operations.
*/
protected String derefAliases = null;
/**
* Constant that holds the name of the environment property for specifying
* the manner in which aliases should be dereferenced.
*/
public final static String DEREF_ALIASES = "java.naming.ldap.derefAliases";
/**
* The protocol that will be used in the communication with the
* directory server.
*/
protected String protocol = null;
/**
* Should we ignore PartialResultExceptions when iterating over NamingEnumerations?
* Microsoft Active Directory often returns referrals, which lead
* to PartialResultExceptions. Unfortunately there's no stable way to detect,
* if the Exceptions really come from an AD referral.
* Set to true to ignore PartialResultExceptions.
*/
protected boolean adCompat = false;
/**
* How should we handle referrals? Microsoft Active Directory often returns
* referrals. If you need to follow them set referrals to "follow".
* Caution: if your DNS is not part of AD, the LDAP client lib might try
* to resolve your domain name in DNS to find another LDAP server.
*/
protected String referrals = null;
/**
* The base element for user searches.
*/
protected String userBase = "";
/**
* The message format used to search for a user, with "{0}" marking
* the spot where the username goes.
*/
protected String userSearch = null;
/**
* The MessageFormat object associated with the current
* <code>userSearch</code>.
*/
protected MessageFormat userSearchFormat = null;
/**
* Should we search the entire subtree for matching users?
*/
protected boolean userSubtree = false;
/**
* The attribute name used to retrieve the user password.
*/
protected String userPassword = null;
/**
* A string of LDAP user patterns or paths, ":"-separated
* These will be used to form the distinguished name of a
* user, with "{0}" marking the spot where the specified username
* goes.
* This is similar to userPattern, but allows for multiple searches
* for a user.
*/
protected String[] userPatternArray = null;
/**
* The message format used to form the distinguished name of a
* user, with "{0}" marking the spot where the specified username
* goes.
*/
protected String ldapUserPattern = null;
/**
* An array of MessageFormat objects associated with the current
* <code>userPatternArray</code>.
*/
protected MessageFormat[] userPatternFormatArray = null;
/**
* An alternate URL, to which, we should connect if ldapConnectionURL fails.
*/
protected String ldapAlternateURL;
/**
* The number of connection attempts. If greater than zero we use the
* alternate url.
*/
protected int connectionAttempt = 0;
/**
* The timeout, in milliseconds, to use when trying to create a connection
* to the directory. The default is 5000 (5 seconds).
*/
protected String connectionTimeout = "5000";
// --------------------------------------------------JDBC Instance Variables
/**
* The connection username to use when trying to connect to the database.
*/
protected String jdbcConnectionName = null;
/**
* The connection password to use when trying to connect to the database.
*/
protected String jdbcConnectionPassword = null;
/**
* The connection URL to use when trying to connect to the database.
*/
protected String jdbcConnectionURL = null;
/**
* The connection to the database.
*/
protected Connection dbConnection = null;
/**
* Instance of the JDBC Driver class we use as a connection factory.
*/
protected Driver driver = null;
/**
* The JDBC driver name to use.
*/
protected String jdbcDriverName = null;
/**
* The PreparedStatement to use for identifying the roles for
* a specified user.
*/
protected PreparedStatement preparedRoles = null;
/**
* The string manager for this package.
*/
protected static final StringManager sm =
StringManager.getManager(Constants.Package);
/**
* The column in the user role table that names a role
*/
protected String roleNameCol = null;
/**
* The column in the user role table that holds the user's name
*/
protected String userNameCol = null;
/**
* The table that holds the relation between user's and roles
*/
protected String userRoleTable = null;
/**
* Descriptive information about this Realm implementation.
*/
protected static final String info = "XXXXXX";
/**
* Descriptive information about this Realm implementation.
*/
protected static final String name = "XXXRealm";
可以看出,將JNDIRealm中不需要的role信息去掉,加上JDBCRealm中獲取用戶role所需要的信息即可。
然后就是修改JNDIRealm中的認證方法authenticate()為我們自己認證所需要的,也就是將通過LDAP獲取role信息的部分改成使用JDBC連接數據庫查詢獲得。代碼不是很復雜但有兩千多行,這里就不貼出來了,有需要的可以在下面回復郵箱,我可以發送給你們。
<h3 id="3.2">3.2 將Realm編譯成.class文件</h3>
寫好自定義Realm過后,就需要編譯了,建議單獨建個包編譯出.class文件,注意只需要.class文件,而該class文件所依賴的Tomcat相關jar包不需要,為什么?因為 $CATALINA_HOME/lib里邊已經有了。
<h3 id="3.3">3.3 在MBeans描述符里聲明你的realm</h3>
什么是MBeans描述符?[這里](https://tomcat.apache.org/tomcat-7.0-doc/mbeans-descriptor-howto.html)有詳細的介紹,簡單說就是Tomcat使用JMX MBeans技術來實現Tomcat的遠程監控和管理,在每個package下面都必須有一個MBeans描述符配置文件,叫做:mbeans-descriptor.xml,如果你沒有給自定義的組件定義該配置文件,就會拋出"ManagedBean is not found"異常。
mbeans-descriptor.xml文件的格式如下:
```java
<mbean name="XXXRealm"
description="Custom XXXRealm..."
domain="Catalina"
group="Realm"
type="com.myfirm.mypackage.XXXRealm">
<attribute name="className"
description="Fully qualified class name of the managed object"
type="java.lang.String"
writeable="false"/>
<attribute name="debug"
description="The debugging detail level for this component"
type="int"/>
...
</mbean>
具體的可用參考Tomcat源碼中realm包下的mbeans文件。該配置文件十分重要,里面的attribute元素直接對應自定義Realm源碼中對應的實例變量字段,也就是我上面貼出來的代碼,不過並不是每個實例變量都要添加進來,添加的都是一些重要的需要我們自己在server.xml文件中指明的屬性(后面講),比如JDBC 驅動、數據庫用戶名、密碼、URL等等,這里的attribute名必須與代碼中的變量名完全一致,不能出錯,否則讀取不到相應的值。
下面貼出因上面我的需求所定義的mbeans-descriptor.xml文件:
<?xml version="1.0"?>
<mbeans-descriptors>
<mbean name="CoralXRRealm"
description="Implementation of Realm that works with a directory server accessed via the Java Naming and Directory Interface (JNDI) APIs and JDBC supported database"
domain="Catalina"
group="Realm"
type="org.opencoral.xreport.realm.CoralXRRealm">
<attribute name="className"
description="Fully qualified class name of the managed object"
type="java.lang.String"
writeable="false"/>
<attribute name="ldapConnectionName"
description="The connection username for the directory server we will contact"
type="java.lang.String"/>
<attribute name="ldapConnectionPassword"
description="The connection password for the directory server we will contact"
type="java.lang.String"/>
<attribute name="ldapConnectionURL"
description="The connection URL for the directory server we will contact"
type="java.lang.String"/>
<attribute name="contextFactory"
description="The JNDI context factory for this Realm"
type="java.lang.String"/>
<attribute name="digest"
description="Digest algorithm used in storing passwords in a non-plaintext format"
type="java.lang.String"/>
<attribute name="userBase"
description="The base element for user searches"
type="java.lang.String"/>
<attribute name="userPassword"
description="The attribute name used to retrieve the user password"
type="java.lang.String"/>
<attribute name="ldapUserPattern"
description="The message format used to select a user"
type="java.lang.String"/>
<attribute name="userSearch"
description="The message format used to search for a user"
type="java.lang.String"/>
<attribute name="userSubtree"
description="Should we search the entire subtree for matching users?"
type="boolean"/>
<attribute name="jdbcConnectionName"
description="The connection username to use when trying to connect to the database"
type="java.lang.String"/>
<attribute name="jdbcConnectionPassword"
description="The connection URL to use when trying to connect to the database"
type="java.lang.String"/>
<attribute name="jdbcConnectionURL"
description="The connection URL to use when trying to connect to the database"
type="java.lang.String"/>
<attribute name="jdbcDriverName"
description="The JDBC driver to use"
type="java.lang.String"/>
<attribute name="roleNameCol"
description="The column in the user role table that names a role"
type="java.lang.String"/>
<attribute name="userNameCol"
description="The column in the user role table that holds the user's username"
type="java.lang.String"/>
<attribute name="userRoleTable"
description="The table that holds the relation between user's and roles"
type="java.lang.String"/>
<operation name="start" description="Start" impact="ACTION" returnType="void" />
<operation name="stop" description="Stop" impact="ACTION" returnType="void" />
<operation name="init" description="Init" impact="ACTION" returnType="void" />
<operation name="destroy" description="Destroy" impact="ACTION" returnType="void" />
</mbean>
</mbeans-descriptors>
可以看到與我上面貼出的代碼重要實例變量一一對應。
3.4 將Realm編譯后的文件打成jar包
具體是:將Realm編譯后的.class文件和mbeans-descriptor.xml文件打成jar包放到 $CATALINA_HOME/lib里邊。注意:class文件還是在package里面,不能單獨拿出來打包,我們將mbeans-descriptor.xml文件放到.class文件同一目錄下,比如我自定義的Realm(比如就叫CustomRealm.java)所在包為:com.ustc.realm.CustomRealm.java,那么.class和mbeans配置文件目錄應該為:
|-- com
|-- ustc
|-- realm
|-- CustomRealm.class
|-- mbeans-descriptor.xml
然后命令行進入到com根目錄下,使用下面命令打包:
jar cvf customrealm.jar .
customrealm.jar是你自己取的jar名稱,第二個參數點.不能丟了,表示對當前的目錄進行打包。打包成功后,將customrealm.jar放到$CATALINA_HOME/lib里邊即可。
3.5 像配置標准realm一樣在server.xml文件中聲明你的realm
這個步驟非常關鍵,打開conf/server.xml文件,搜索Realm,你會看到Tomcat配置文件中自帶的Realm聲明: ```java<Realm className="org.apache.catalina.realm.JNDIRealm"
connectionURL="ldap://localhost:389"
userPattern="uid={0},ou=people,dc=mycompany,dc=com"
roleBase="ou=groups,dc=mycompany,dc=com"
roleName="cn"
roleSearch="(uniqueMember={0})"
/>
注:這是有關LDAP服務器的配置,關於LDAP如何使用可以查詢相關資料,不在本文討論范圍內。
而JDBCRealm你可以這樣聲明:
<Realm className="org.apache.catalina.realm.JDBCRealm"
driverName="org.gjt.mm.mysql.Driver"
connectionURL="jdbc:mysql://localhost/authority?user=dbuser&password=dbpass"
userTable="users" userNameCol="user_name" userCredCol="user_pass"
userRoleTable="user_roles" roleNameCol="role_name"/>
如果你是使用Tomcat自帶的標准Realm,那么只需要修改上面對應的屬性值即可。如果是自定義Realm呢?那么我們也需要自定義Realm的聲明,以我上面的需求為例,自定義的Realm聲明如下:
<Realm className="com.ustc.realm.CustomRealm"
ldapConnectionURL="ldap://server ip:389"
ldapUserPattern="uid={0},ou=people,dc=mycompany"
jdbcDriverName="org.postgresql.Driver"
jdbcConnectionURL="jdbc:postgresql://dbserver ip:port"
jdbcConnectionName="xxx"
jdbcConnectionPassword="xxx" digest="MD5"
userRoleTable="user_roles"
userNameCol="user_name"
roleNameCol="role_name" />
其實就是上面的兩個標准Realm聲明結合起來,各取所需,這里需要注意兩個問題:
- Realm聲明里面的字段名必須與Realm源碼及mbeans-descriptor.xml文件中的字段名對應,三者必須一致,否則就讀取不到我們在這里設置的具體值;
- Realm聲明里面不能加注釋語句,否則會報錯。
OK,到此為止,我們自定義Realm的編寫與配置就完成了。接下來就是測試了,重啟Tomcat,進入登錄界面試試吧。