安全性
预览
web应用程序的一些Context是受限制的,这些Context仅仅允许那些受批准的、用户名密码正确的用户浏览。servlet技术支持通过在web.xml文件中配置这种方式针对这些Context的内容使用安全约束。在本章中,我们将看到一个web容器是怎么支持这种安全约束功能。
servlet容器通过一个叫做认证器(authenticator)的阀门支持安全约束功能。这个认证器阀门会在servlet容器启动时添加入Context的管道中。如果你忘记了管道是怎么工作的,可以重新温习一下第六章"Pipeline"
认证器阀门会在包装器阀门之前调用。认证器阀门用来认证用户的权限。如果用户输入了正确的用户名和密码,这个认证器阀门调用下一个阀门(表示通过认证)。如果认证失败,认证器将不调用下一个阀门而直接返回。那么认证失败的结果就是用户不会看到他请求的servlet。
认证器阀门通过调用context's realm的authenticate方法认证用户,在调用此方法时也将用户名密码传递给此方法。realm拥有可用用户集合的访问权限。
本章从servlet编程中与安全功能有关的类开始讲起(realms、principals、roles等等)。然后演示一个在servlet中使用基本认证器的应用程序。
提示:在这里我们假定你对servlet编程中的安全约束观念非常熟悉,包括principals、roles、realms、登陆配置等等。如果你对这些不理解的话,建议读读我写的另外一本书【Java for the Web with Senlets, JSP, and EJB】或者其他比较不错的servlet编程书籍。
Realm
Realm是用来认证用户的组件,它可以告诉你一对指定的用户名密码是否有效。一个Realm通常附属于一个Context,一个容器只能有一个Realm。如果想使一个Realm附属于一个容器只需将Realm对象传递给此容器的setRealm方法即可。
Realm是怎么知道认证用户权限呢?这是因为它包含了所有的有效用户的用户名和密码或者拥有访问保存有效用户仓库的权限。Realm所依赖的有效用户信息的存储方式是由实现类决定的。在Tomcat中,默认有效用户信息被存储在tomcat-user.xml文件中。然而,你可以使用其他的Realm实现类来使用其他存储介质进行权限认证,比如一个关系数据库。
在Catalina中,一个Realm通过org.apache.catalina.Realm接口定义。这个接口中最重要的方法是四个重载authenticate方法:
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);
同样的,getContainer和setContainer方法被用于将此Realm与一个容器关联起来。
此Realm接口的基础实现类是抽象类org.apache.catalina.realm.RealmBase。这个org.apache.catalina.realm包也提供了一些继承自RealmBase的实现类:JDBCRealm、JNDIRealm、MemoryRealm、UserDatebaseRealm,默认情况下使用的是MemoryRealm类。当MemoryRealm第一次启动时,它读取tomcat-users.xml文档。然而,在本章附带的应用中,我们也会构造一个简单的、将用户信息存储在Realm对象自身中的Realm实现类。
GenericPrincipal
委托人是由java.security.Principal接口定义的。此接口在Catalina中的实现类是org.apache.catalina.realm.GenericPrincipal类。一个GenericPrincipal必须始终与一个Realm相关联,就像如下的两个GenericPrincipal构造方法:
public class GenericPrincipal implements Principal { /** * 使用指定的用户名密码构造一个与指定Realm相关联的委托人 */ public GenericPrincipal(Realm realm, String name, String password) { this(realm, name, password, null); } /** * 使用指定的用户名密码和规则名字(字符串形式)集合构造一个与指定Realm相关联的委托人 */ 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); } } /** * 当前委托人拥有指定的规则吗? */ public boolean hasRole(String role) { if (role == null) return (false); return (Arrays.binarySearch(roles, role) >= 0); } } |
GenericPrincipal实例必须有一个用户名和密码。你也可选择性的将一个规则集合传递给它。这时,你可以通过调用它的hasRole方法检查当前委托人是否有指定的规则,这个hasRole方法需要的是一个规则的字符串形式。
Tomcat 5支持Servlet2.4,因此识别的是特殊字符*代表任何规则。
public boolean hasRole(String role) {
if ("*".equals(role))
return true;
if (role == null)
return (false);
return (Arrays.binarySearch(roles, role) >= 0);
}
LoginConfig
package org.apache.catalina.deploy; import org.apache.catalina.util.RequestUtil; /** * 此类表示web应用中的一个登陆配置元素,在开发部署中描述符为<login-config> */ public final class LoginConfig { /** * 使用默认参数构造一个新的LoginConfig */ public LoginConfig() { super(); // 父类直接就是Object类了,因此此方法是Object中的构造方法 } /** * 使用指定的参数构造一个新的LoginConfig对象 * @param authMethod 验证方法 * @param realmName 域名 * @param loginPage 登陆页面URI * @param errorPage 错误页面URI */ public LoginConfig(String authMethod, String realmName, String loginPage, String errorPage) { super(); setAuthMethod(authMethod); setRealmName(realmName); setLoginPage(loginPage); setErrorPage(errorPage); } /** * 当前应用登陆的验证方法。必须为BASIC、DIGEST、FORM、CLIENT-CERT中的一种 */ private String authMethod = null; public String getAuthMethod() { return (this.authMethod); } public void setAuthMethod(String authMethod) { this.authMethod = authMethod; } /** * 表单登陆错误页面,此错误页面是一个相对于上下文的URI */ private String errorPage = null; public String getErrorPage() { return (this.errorPage); } public void setErrorPage(String errorPage) { // if ((errorPage == null) || !errorPage.startsWith("/")) // throw new IllegalArgumentException("Error Page resource path must start with a '/'"); this.errorPage = RequestUtil.URLDecode(errorPage); } /** * 表单登陆的登陆页面,此页面是一个相对于上下文的URI */ private String loginPage = null; public String getLoginPage() { return (this.loginPage); } public void setLoginPage(String loginPage) { // if ((loginPage == null) || !loginPage.startsWith("/")) // throw new IllegalArgumentException ("Login Page resource path must start with a '/'"); this.loginPage = RequestUtil.URLDecode(loginPage); } /** * 验证用户时使用的域名 */ private String realmName = null; public String getRealmName() { return (this.realmName); } public void setRealmName(String realmName) { this.realmName = realmName; } public String toString() { StringBuffer sb = new StringBuffer("LoginConfig["); sb.append("authMethod="); sb.append(authMethod); if (realmName != null) { sb.append(", realmName="); sb.append(realmName); } if (loginPage != null) { sb.append(", loginPage="); sb.append(loginPage); } if (errorPage != null) { sb.append(", errorPage="); sb.append(errorPage); } sb.append("]"); return (sb.toString()); } } |
登陆配置包含一个域名,它由org.apache.catalina.deploy.LoginConfig最终类定义。LoginConfig实例封装了域名和使用的authentication方法。你可以通过调用getAuthName方法调用getRealmName和authentication方法获取域名。authentication方法名必须为下列几个之一:BASIC、DIGEST、FORM、CLIENT-CERT。如果使用的是基于表单(form-based)的验证,那么LoginConfig对象也应该包含字符串类型的登录页面URL和错误页面URL。
在程序部署之后,Tomcat在启动时会读取web.xml文件。如果web.xml文档中包含一个login-config元素,则创建一个LoginConfig对象并且设置其相应的属性。验证器阀门调用LoginConfig的getRealmName方法并且将域名发送至浏览器显示在登录对话框上。如果getRealmName方法返回null,则将服务器名和端口发送至浏览器。下图展示了一个基本的Windows XP/IE 6下验证器登录对话框。
Authenticator
org.apache.catalina.Authenticator定义了一个认证器。它没有定义任何方法,因此它可以当做一个“标记”,任何其他的组件可以直接使用instandof关键字测试此组件是否是一个认证器
Catalina提供了一个此接口的基本实现类:org.apache.catalina.authenticator.AuthenticatorBase。此类除了实现了Authenticator接口之外也继承了org.apache.catalina.valves.ValveBase类。这就意味着,AuthenticatorBase也是一个阀门。在org.apache.catalina.authenticator包中还可以发现其他的一些实现类,包括可以被用作基本认证器的BasicAuthenticator类、基于表单的认证器FormAuthenticator类、择要验证的DigestAuthenticator类、SSL验证的SSLAuthenticator类。除此之外,如果Tomcat使用者没有提供指定的验证方法值,NonLoginAuthenticator类将会被使用,此类表示了一个仅仅检查安全约束而不负责用户验证的验证器。
org.apache.catalina.authenticator包下的类图如下:
验证器的主要工作就是验证用户权限的。因此AuthenticatorBase类的invoke方法调用抽象的、依赖于子类的authenticate方法就不令人惊讶了。例如:在BasicAuthenticator类中的authenticate方法使用基本验证器验证用户
安装验证器阀门
login-config元素仅仅可以在部署描述文件中出现一次,login-config元素包含auth-method子元素,此子元素指定了验证方法。这就是说一个上下文仅仅可以有一个LoginConfig对象并且仅仅可以使用一个验证器实现类。
在一个上下文中,AuthenticatorBase将要用作验证器阀门的子类依赖于auth-method元素的值,下列给出了对于给定的验证器名称相匹配的验证器:
BASIC | BasicAuthenticator |
FORM | FormAuthenticator |
DIGEST | DigestAuthenticator |
CLIENT-CERT | SSLAuthenticator |
由于auth-method元素没有出现,那么LoginConfig对象的auth-method参数的值将被设置为NONE,然后NonLoginAuthenticator类将会被使用。
由于验证器类仅仅运行时才可以知道,因此此类被动态加载。StandardContext类使用org.apache.catalina.startup.ContextConfig类配置许多StandardContext参数。这些配置包括验证器实例化、将实例与上下文相关联。本章附带的应用使用了一个简单的ex10.pyrmont.core.SimpleContextConfig上下文配置。就像你晚会将要看到的,此类的实例为动态加载BasicAuthenticator类负责,初始化此类,然后将初始化后的实例作为一个阀门安装在StandardContext实例中。
提示:org.apache.catalina.startup.ContextConfig将会在第十五章讨论
应用
本章应用使用了几个与安全限制有关的Catalina中的类,也使用了与第九章应用十分相似的SimplePipeline、SimpleWrapper、SimpleWrapperValve类。除此之外,SimpleContextConfig类与第九章的SimpleContextConfig类很相似,但是此类添加了一个authenticatorConfig方法,此方法用来将一个BasicAuthenticator实例添加入StandardContext中。本章应用也使用了两个servlet:PrimitiveServlet、ModernServlet。以上这些类在本章两个应用中都用到。
第一个应用中使用了两个类:ex10.pyrmont.startup.Bootstrap1和ex10.pyrmont.realm.SimpleRealm。
第二个应用使用了ex10.pyrmont.startup.Bootstrap2、ex10.pyrmont.realm.SimpleUserDatabaseRealm类。这些类将在接下来的每一个小节中分别讨论
ex10.pyrmont.core.SimpleContextConfig类
此SimpleContextConfig类与第九章中非常相似。此类实例被org.apache.catalina.core.StandardContext用于设置其中的configured参数为true。然而,此类中添加了一个authenticatorConfig方法,此方法为私有方法,只能由lifeCycleEvent方法调用。此authenticatorConfig方法初始化一个BasicAuthenticator实例并且将此实例作为一个阀门添加入StandardContext的管道之中
提示:代码展示和解释此处省略
ex10.pyrmont.realm.SimpleRealm
此类演示了一个Realm的工作机制。此类在第一个应用中用到并且包含硬编码的用户名、密码键值对集合。
提示:此类代码比较简单、省略o(╯□╰)o
ex10.pyrmont.realm.SimpleUserDatabaseRealm
此类定义了一个更加复杂的Realm。它没有将用户集合存储类的内部,而是通过读取conf目录下的tomcat-users.xml并且将其中的用户内容加载入内存中,然后使用加载的用户进行验证管理。在本书随带的zip压缩文件中的conf目录中可以找到如下tomcat-users.xml文件:
<?xml version='1.0' encoding='utf-8'?> <tomcat-users> <role rolename="tomcat"/> <role rolename="role1"/> <role rolename="manager"/> <role rolename="admin"/> <user username="tomcat" password="tomcat" roles="tomcat"/> <user username="rolel" password="tomcat" roles="rolel"/> <user username="both" password="tomcat" roles="tomcat,rolel"/> <user username="admin" password="password" roles="admin,manager"/> </tomcat-users> |
源码略
此类中,在你实例化SimpleUserDatabaseRealm类之后,必须调用它的createDatebase方法将包含用户集合的xml文档路径传递给当前实例中。此方法使用此路径初始化一个org.apache.catalina.users.MemoryUserDatabase实例,此实例将被用于读取和解析XML文档。
ex10.pyrmont.startup.Bootstrap1类
此类用于启动第一个应用
ex10.pyrmont.startup.Bootstrap2类
此类用于启动第二个应用程序。除了它使用了一个SimpleUserDatabase实例作为与StandardContext相关联的Realm之外,此类与Bootstarp1非常相似,如果想访问PrimitiveServlet和ModernServlet,当前用户名密码必须是分别为admin和password
运行应用
总结
在Servlet编程中,安全性是一个非常重要的话题,servlet规范迎合安全性的需要而提供了一些安全性相关的对象,比如principal、role、security constraints、login configuration等等。在本章中我们学会了servlet容器如何处理这个话题