如何自定義Tomcat Realm實現我們的用戶認證需求


導讀

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。

總結:說的簡單點就是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 .../>

元素可以被套嵌在下列任何一個Container元素里面。這個Realm元素所處的位置直接影響到這個Realm的作用范圍(比如,哪些web應用程序會共享相同的認證信息):

元素里邊 - 這個域(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是無效的,因為沒有配置完整,只是作為一個示例告訴你要在這里重新配置你自己的Realm,我們將這段Realm聲明注釋掉,然后聲明我們自己的Realm,怎么聲明?這要看你的需求了,Tomcat官方文檔中有每個標准Realm的詳細配置聲明,比如JNDIRealm你可以按下面格式聲明:
<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,進入登錄界面試試吧。

4. Realm的優點

* 安全:對於每個現有的Realm實現里,用戶的密碼(默認情況下)以明文形式被貯存。在許多環境中,這是不理想的,因為任何人看見了認證數據都可以收集足夠信息成功登錄,冒充其他用戶。為了避免這個問題,標准的實現支持digesting用戶密碼的概念。這被貯存的密碼是被加密后的(以一種不易被轉換回去的形式),但是Realm實現還是可以用它來認證。當一個標准的realm通過取得貯存的密碼並把它與用戶提供的密碼值作比較來認證時,你可通過在你的元素 上指定digest屬性選擇digested密碼。這個屬性的值必須是java.security.MessageDigest class (SHA, MD2, or MD5)支持的digest 算法之一。當你選擇這一選項,貯存在Realm里的密碼內容必須是這個密碼的明文形式,然后被指定的運算法則來加密。當這個Realm的authenticate()方法被調用,用戶指定的(明文)密碼被相同的運算法來加密,它的結果與Realm返回的值作比較。如果兩個值對等的話,就意味着原始密碼的明文版與用戶提供的一樣,所以這個用戶就被認證了。 * 調試方便:每個Realm排錯和異常信息將由與這個realm的容器(Context, Host,或 Engine)相關的日志配置記錄下來,方便我們調試。

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM