本文分析tomcat容器的安全管理,servlet技術支持通過配置部署描述器(web.xml文件)來對受限內(nèi)容進行訪問控制;servlet容器是通過一個名為驗證器的閥來支持安全限制的,當servlet容器啟動時,驗證器閥會被添加到Context容器的管道中。在調(diào)用Wrapper閥之前,會先調(diào)用驗證器閥,對當前用戶進行身份驗證;驗證器閥會調(diào)用Context容器的Realm對象的authenticate()方法,傳入用戶輸入的用戶名和密碼來對用戶進行身份驗證。
Realm對象是用來對用戶進行身份驗證的組件,它會對用戶輸入的用戶名和密碼對進行有效性判斷,通常與一個Context容器相關聯(lián);那么Realm對象是如何驗證用戶身份的呢?實際上,它保存了所有有效用戶的用戶名和密碼對,或者它會訪問存儲這些數(shù)據(jù)的存儲器。這些數(shù)據(jù)的具體存儲依賴于Realm對象的具體實現(xiàn),在tomcat中,有效用戶信息默認存儲在tomcat-user.xml文件中;當然也可以使用其他的針對其他資源驗證Realm對象實現(xiàn),比如關系數(shù)據(jù)庫
在tomcat中,Realm對象是org.apache.catalina.Realm接口的實例,與驗證相關的方法如下:
public Principal authenticate(String username, String credentials);
public Principal authenticate(String username, byte[] credentials);
public Principal authenticate(String username, String digest,
????????????????????????????????? String nonce, String nc, String cnonce,
????????????????????????????????? String qop, String realm,
????????????????????????????????? String md5a2);
public Principal authenticate(X509Certificate certs[]);
通常都會使用第一個重載方法。在Realm接口中,還有一個hasRole()方法,方法簽名如下:
public boolean hasRole(Principal principal, String role);
此外,Realm接口的getContainer()方法與settContainer()方法用來將Realm實例與一個Context實例相關聯(lián)
在tomcat中,Realm接口的基本實現(xiàn)形式是org.apache.catalina.realm.RealmBase類,該類是一個抽象類,org.apache.catalina.realm包還提供了RealmBase類的一些繼承類的實現(xiàn),包括JDBCRealm、JNDIRealm、MemoryRealm和UserDatabaseRealm類等。 默認情況下,會使用MemoryRealm類的實例作為驗證用的Realm對象。當?shù)谝淮握{(diào)用MemoryRealm實例時,它會讀取tomcat-user.xml文檔的內(nèi)容。
在tomcat中,java.security.Principal接口的實例為org.apache.catalina.realm.GenericPrincipal類,GenericPrincipal實例必須始終與一個Realm對象相關聯(lián),正如其兩個構(gòu)造函數(shù)所示:
public GenericPrincipal(Realm realm, String name, String password) {
??????? this(realm, name, password, null);
??? }
public GenericPrincipal(Realm realm, String name, String password,
??????????????????????????? List roles) {
??????? super();
??????? this.realm = realm;
??????? this.name = name;
??????? this.password = password;
??????? if (roles != null) {
??????????? this.roles = new String[roles.size()];
??????????? this.roles = (String[]) roles.toArray(this.roles);
??????????? if (this.roles.length > 0)
??????????????? Arrays.sort(this.roles);
??????? }
??? }
GenericPrincipal實例必須有一個用戶名和密碼對。此外,該用戶名和密碼對所對應的角色列表是可選的;然后可以調(diào)用其hasRole()方法,并傳入字符串形式的角色名來檢查該Principal對象是否擁有指定角色
public boolean hasRole(String role) {
??????? if (role == null)
??????????? return (false);
??????? return (Arrays.binarySearch(roles, role) >= 0);
??? }
下面我們來看一個簡單的Realm對象是怎樣工作的,里面采用了硬編碼的方式保存了兩個用戶名和密碼對
SimpleRealm類的源碼如下(略去了無關代碼)
public class SimpleRealm implements Realm { public SimpleRealm() { createUserDatabase(); } private Container container; private ArrayList users = new ArrayList(); public Container getContainer() { return container; } public void setContainer(Container container) { this .container = container; } /** * 驗證用戶名和密碼,返回Principal類型對象 */ public Principal authenticate(String username, String credentials) { System.out.println( "SimpleRealm.authenticate()" ); if (username== null || credentials== null ) return null ; User user = getUser(username, credentials); if (user== null ) return null ; return new GenericPrincipal( this , user.username, user.password, user.getRoles()); } /** * 判斷Principal類型對象是有擁有指定角色 */ public boolean hasRole(Principal principal, String role) { if ((principal == null ) || (role == null ) || !(principal instanceof GenericPrincipal)) return ( false ); GenericPrincipal gp = (GenericPrincipal) principal; if (!(gp.getRealm() == this )) return ( false ); boolean result = gp.hasRole(role); return result; } private User getUser(String username, String password) { Iterator iterator = users.iterator(); while (iterator.hasNext()) { User user = (User) iterator.next(); if (user.username.equals(username) && user.password.equals(password)) return user; } return null ; } private void createUserDatabase() { User user1 = new User("ken", "blackcomb" ); user1.addRole( "manager" ); user1.addRole( "programmer" ); User user2 = new User("cindy", "bamboo" ); user2.addRole( "programmer" ); users.add(user1); users.add(user2); } class User { public User(String username, String password) { this .username = username; this .password = password; } public String username; public ArrayList roles = new ArrayList(); public String password; public void addRole(String role) { roles.add(role); } public ArrayList getRoles() { return roles; } } }
其中的authenticate()方法由驗證器調(diào)用,如果用戶提供的用戶名和密碼是無效的便返回null,否則返回代表該用戶的Principal對象
上面部分是描述Realm對象相關的,接下來描述與驗證器相關的實現(xiàn)。驗證器是org.apache.catalina.Authenticator接口的實例,Authenticator接口是一個標識接口,沒有聲明任何方法;在tomcat中提供了Authenticator接口的基本實現(xiàn)org.apache.catalina.authenticator.AuthenticatorBase類,同時AuthenticatorBase類還繼承了org.apache.catalina.valves.ValveBase類,也就是說,AuthenticatorBase類也是一個閥;在tomat中,提供了很多AuthenticatorBase類的繼承類,包括BasicAuthenticator類、FormAuthenticator類、DigestAuthenticator類和SSLAuthenticator類等;此外當tomcat用戶沒有指定驗證方法名時,NonLoginAuthenticator類用來對來訪者的身份進行驗證。
AuthenticatorBase類的invoke()方法會調(diào)用authenticate()抽象方法,后者的實現(xiàn)依賴于子類,這里類似與templet方法模式
那么在我們的web應用程序中,具體采用那個驗證器實現(xiàn)呢,這依賴于我們在web.xml文件中的配置(配置示例如下)
< web-app > < security-constraint > < web-resource-collection > < web-resource-name > Member Area </ web-resource-name > < description > Only registered members can access this area. </ description > < url-pattern > /member/* </ url-pattern > < http-method > GET </ http-method > < http-method > POST </ http-method > </ web-resource-collection > < auth-constraint > < role-name > member </ role-name > </ auth-constraint > </ security-constraint > < login-config > < auth-method > BASIC </ auth-method > </ login-config > < security-role > < role-name > member </ role-name > </ security-role > </ web-app >
上面示例是采用BASIC驗證,也可以設置為FORM、DIGEST或CLIENT-CERT等,分別對應不同的驗證器類(BasicAuthenticator類、FormAuthenticator類、DigestAuthenticator類和SSLAuthenticator類),若沒有設置auth-method元素,則LoginConfig對象auth-method屬性的默認值為NONE,使用NonLoginAuthenticator進行安全驗證。(注:LoginConfig對象封裝了Realm對象名和要使用的身份驗證方法)
最后我們來描述一個Context容器實例是如何將驗證器閥的,這里介紹的是一個SimpleContextConfig類,它是作為Context容器實例的監(jiān)聽器,在監(jiān)聽方法lifecycleEvent()中,調(diào)用authenticatorConfig()方法實例化BasicAuthenticator類,并將其作為閥添加到StandardContext實例的管道中
下面是SimpleContextConfig類的authenticatorConfig()方法實現(xiàn)
private synchronized void authenticatorConfig() { // Does this Context require an Authenticator? SecurityConstraint constraints[] = context.findConstraints(); if ((constraints == null ) || (constraints.length == 0 )) return ; LoginConfig loginConfig = context.getLoginConfig(); if (loginConfig == null ) { loginConfig = new LoginConfig("NONE", null , null , null ); context.setLoginConfig(loginConfig); } // Has an authenticator been configured already? Pipeline pipeline = ((StandardContext) context).getPipeline(); if (pipeline != null ) { Valve basic = pipeline.getBasic(); if ((basic != null ) && (basic instanceof Authenticator)) return ; Valve valves[] = pipeline.getValves(); for ( int i = 0; i < valves.length; i++ ) { if (valves[i] instanceof Authenticator) return ; } } else { // no Pipeline, cannot install authenticator valve return ; } // Has a Realm been configured for us to authenticate against? if (context.getRealm() == null ) { return ; } // Identify the class name of the Valve we should configure String authenticatorName = "org.apache.catalina.authenticator.BasicAuthenticator" ; // Instantiate and install an Authenticator of the requested class Valve authenticator = null ; try { Class authenticatorClass = Class.forName(authenticatorName); authenticator = (Valve) authenticatorClass.newInstance(); ((StandardContext) context).addValve(authenticator); System.out.println( "Added authenticator valve to Context" ); } catch (Throwable t) { } }
---------------------------------------------------------------------------?
本系列How Tomcat Works系本人原創(chuàng)?
轉(zhuǎn)載請注明出處 博客園 刺猬的溫馴?
本人郵箱: ? chenying998179 # 163.com ( #改為@ )
更多文章、技術交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
