本文分析tomcat容器的安全管理,servlet技术支持通过配置部署描述器(web.xml文件)来对受限内容进行访问控制;servlet容器是通过一个名为验证器的阀来支持安全限制的,当servlet容器启动时,验证器阀会被添加到Context容器的管道中。在调用Wrapper阀之前,会先调用验证器阀,对当前用户进行身份验证;验证器阀会调用Context容器的Realm对象的authenticate()方法,传入用户输入的用户名和密码来对用户进行身份验证。
Realm对象是用来对用户进行身份验证的组件,它会对用户输入的用户名和密码对进行有效性判断,通常与一个Context容器相关联;那么Realm对象是如何验证用户身份的呢?实际上,它保存了所有有效用户的用户名和密码对,或者它会访问存储这些数据的存储器。这些数据的具体存储依赖于Realm对象的具体实现,在tomcat中,有效用户信息默认存储在tomcat-user.xml文件中;当然也可以使用其他的针对其他资源验证Realm对象实现,比如关系数据库
在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实例相关联
在tomcat中,Realm接口的基本实现形式是org.apache.catalina.realm.RealmBase类,该类是一个抽象类,org.apache.catalina.realm包还提供了RealmBase类的一些继承类的实现,包括JDBCRealm、JNDIRealm、MemoryRealm和UserDatabaseRealm类等。 默认情况下,会使用MemoryRealm类的实例作为验证用的Realm对象。当第一次调用MemoryRealm实例时,它会读取tomcat-user.xml文档的内容。
在tomcat中,java.security.Principal接口的实例为org.apache.catalina.realm.GenericPrincipal类,GenericPrincipal实例必须始终与一个Realm对象相关联,正如其两个构造函数所示:
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实例必须有一个用户名和密码对。此外,该用户名和密码对所对应的角色列表是可选的;然后可以调用其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()方法由验证器调用,如果用户提供的用户名和密码是无效的便返回null,否则返回代表该用户的Principal对象
上面部分是描述Realm对象相关的,接下来描述与验证器相关的实现。验证器是org.apache.catalina.Authenticator接口的实例,Authenticator接口是一个标识接口,没有声明任何方法;在tomcat中提供了Authenticator接口的基本实现org.apache.catalina.authenticator.AuthenticatorBase类,同时AuthenticatorBase类还继承了org.apache.catalina.valves.ValveBase类,也就是说,AuthenticatorBase类也是一个阀;在tomat中,提供了很多AuthenticatorBase类的继承类,包括BasicAuthenticator类、FormAuthenticator类、DigestAuthenticator类和SSLAuthenticator类等;此外当tomcat用户没有指定验证方法名时,NonLoginAuthenticator类用来对来访者的身份进行验证。
AuthenticatorBase类的invoke()方法会调用authenticate()抽象方法,后者的实现依赖于子类,这里类似与templet方法模式
那么在我们的web应用程序中,具体采用那个验证器实现呢,这依赖于我们在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容器实例的监听器,在监听方法lifecycleEvent()中,调用authenticatorConfig()方法实例化BasicAuthenticator类,并将其作为阀添加到StandardContext实例的管道中
下面是SimpleContextConfig类的authenticatorConfig()方法实现
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系本人原创
转载请注明出处 博客园 刺猬的温驯
本人邮箱: chenying998179 # 163.com ( #改为@ )

