本文分析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元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

