在J2EE Web 应用中使用基于CAPTCHA 的授权模块
?
图1一个典型的CAPTCHA.
现在,大多数主要的服务提供商(Yahoo, Hotmail, Google)已经在他们的免费服务中使用CAPTCHA,用来作为区分垃圾邮件和虚假注册的手段。在本文中,我们要描述以下在我们的Web应用中加入基于CAPTCHA授权的方法。
首先,我们快速浏览以下J2EE中Web应用的安全模型。
J2EE安全模型
在Java开发中,安全性始终是一个最受关注的领域。毫无疑问,J2EE在构建安全的应用时,采用了同样的原理和健壮的框架。在J2EE中,安全性是一个很大的题目,在这里是不可能叙述细节的。在这方面有好多好的资源。我极力推荐团队和个人花些时间来熟悉这些概念。在这力,我只能极概括的叙述一些最关键的概念。
关键概念
在J2EE应用中,安全性必须采用声明或编程的话方法。就象名字中暗示的,当采用声明方法时,开发者在应用软件代码的外部定义用于应用的安全性约束。这些声明用部署描述符的形式来建造(web.xml, ejb-jar.xml),并由容器的运行环境来保证它的强制执行。声明的方式容许开发者:
·能够实现基于身份的对资源存取的约束(例如:/admin/* 只能容许有管理员身份的人来操作)
·能够实现对某些URL的存取只能用某种协议的约束(例如:“/customer/*”只能通过HTTPS来访问)
·能够实现基于身份的对某些服务存取的约束(例如:可以限定SiteShutdownServlet只能由具有“god”身份的人来操作)
·能够实现当用户要存取某些受限资源但用户还没有登录到系统的时候,自动重定向到登录页面的功能.而编程的方法能提供查询和调用安全设施的机制,而开发者必须实现这些机制。这个方法的特点是:
·检索出与当前用户相关联的部分:HttpServletRequest.getUserPrincipal or EJBContext.getCallerPrincipal
·查询用户是否具有某种特定的身份:HttpServletRequest.isUserInRole(String role) or EJBContext.isCallerInRole(String role)
这两种方法都有它的局限性,并且是能互相补充的。
Web应用的声明安全的方法
Web应用的声明安全的方法本质上是一种被动的方法。这意味者只有在刚开始访问受保护资源的用户,如果他们没有被受权,才会被重定向到登录页面。如果这个用户已经被授权并有适当的权限,他们就能访问这些资源。
这类方法中有一个最常用的方法是基于规则的受权。应用部署描述符web.xml分两个部分描述了在这个方法中需要的所有元素。
第一部分是适用于整个应用的。它要鉴别出:
·在登录中需要使用的方法。J2EE支持BASIC,DIGEST,FORM,或CERT等几种授权机制。???????
·用于基于规则受权的登录和错误的页面
·能在应用中使用的所有身份的超集
图2表明了第一部分的关键元素和它们之间的关系
图2 第一部分的关键元素和它们之间的关系
??
第二部分说明了资源方面的约束。部署描述符可以包含零个或多个类似于下面的声明:
·需要保护的站点。这可以在web-resource-collection内使用url-pattern来配置。
·能够存取资源的身份的集合(auth-constraint)。它通常是第一部分定义的身份集合的一个子集。
·与某个资源相关的传输的保证(user-data-constraint)。
图3表明了第二部分的关键元素和它们之间的关系
图3 第二部分的关键元素和它们之间的关系
现在,我们看一个简单的例子web.xml:
这个片断包含了这样一些内容:
·所有保存在/security/protected目录下的资源(JSP网页,SERVLETS)都要受容器的保护
·Anonymous是惟一的可访问这些资源的用户
·这个容器只接受HTTP 方式的GET 和POST请求
·受保护的资源只能通过常规的HTTP连接方式提供服务
·使用基于规则的授权;security/protected/login.jsp and /security/protected/error.jsp 分别作为登录和出错的页面
在写好安全部分后,就需要写登录和出错的页面。由于把每个会话与一个CAPTCHA或标识相关联,所有login.jsp 包含到TokenServlet的请求。
login.jsp的一部分:
.....
.....
<%-- Generates and associates a CAPTCHA --%>
<img src="/servlet/AuthToken" alt="在J2EE Web 使用中使用基于CAPTCHA 的授权模块"/>
<%-- The form login page --%>
<form method="POST" action='<%= response.encodeURL("j_security_check") %>' >
??<table border="0" cellspacing="5">
????<input type="hidden" name="j_username" value="<%= session.getId() %>">
????<tr>
??????<th align="right">Challenge:</th>
??????<td align="left"><input type="password" name="j_password"></td>
????</tr>
????<tr>
??????<td align="right"><input type="submit" value="Log In"></td>
??????<td align="left"><input type="reset"></td>
????</tr>
??</table>
</form>
.....
.....登录页面使用基于规则的安全方法,并把数据提交给j_security_check。j_username输入域被隐藏起来,默认值是用户会话的ID。另一个域,j_password是用户输入标识的地方。
出错页面是相当简单的。它只是提示灾害授权过程中发生乐和错误,还提供了以个到登录页面的连接。
产生新会话的惟一的标识
下面,需要提供与新会话相关联的标识符的方法。
为支持产生多个标识,我们采用抽象工厂模式。jw.token.factory.TokenFactory是抽象工厂模式,根据给定的参数,初始化一个具体的标识符产生工厂,并将其返回。如果没有要求一个特定的标识符产生工厂,就会返回一个默认的实现。Resources里有些分布式的可用的标识符产生工厂可以下载。第一个是我的业余练习jw.token.factory.SimpleTokenFactory。第二个是jw.token.Token,式由Jcaptcha项目提供的一个更成熟的实现。可以有getToken()来调用。标识符对象是一个用于CAPTCHAs的容器,可以通过在调用getTokenImage()时被当作一幅图像来渲染。
图5描述了标识符工厂对象模型
图5 标识符工厂和标识符类的对象模型
一旦远程客户被定向到登录页,它要调用jw.jaas.servlet.TokenGeneratorServlet来取回以个标识符。TokenServlet的行为对于所有的标识符产生请求类似于看门人,它在web.xml中被配置。它接受一个初始化参数――tokenFactory,这个参数指明了要使用的标识符产生工厂。Servlet把请求委托给这个工厂,得到一个标识符,稍后把标识符作为一幅图像传给远方的Web客户端。
图6说明了标识符产生的顺序
图6 标识符产生顺序
为存储与会话相关联的标识符,我们创建以个缓冲。jw.token.AuthenticationTokenCache,它作为一个标识符的仓库,还提供以个接口,以实现与某个会话相关的标识符的查询、删除,和新标识符的加入。缓冲是惟一的,并采用同步映射。
我们也需要周期的清理AuthenticationTokenCache。在web.xml中注册jw.token.TokenInvalidationListener作为一个会话的监听器。因此,不论何时,当一个会话被销毁时,sessionDestroyed()总会被调用,然后标识符就从缓冲中被清除。
图7显示了服务模块、缓冲和监听器对象间的关系
图7服务模块、缓冲和监听器对象间的关系
Web.xml显示了servlet的配置
<web-app>
??.....
??.....
??<listener>
???? <listener-class>jw.jaas.servlet.TokenInvalidationListener</listener-class>
??</listener>
??<!-- Standard Action Servlet Configuration (with debugging) -->
??<servlet>
????<servlet-name>tokengen</servlet-name>
????<servlet-class>jw.jaas.servlet.TokenGeneratorServlet</servlet-class>
????<load-on-startup>2</load-on-startup>
????<init-param>
??????<param-name>tokenFactory</param-name>
??????<param-value>jw.token.factory.SimpleTokenFactory</param-value>
????</init-param>
??</servlet>
??<servlet-mapping>
????<servlet-name>tokengen</servlet-name>
????<url-pattern>/servlet/AuthToken</url-pattern>
??</servlet-mapping>
??.....
??.....
</web-app>
在部署描述符中,路径/servlet/AuthToken被映射到TokenGeneratorServlet。Servlet被配置使用SimpleTokenFactory。
实现JAAS模块
在JAAS Authentication Guide是说明实现JAAS登录模块的很好的参考资料。登录模块包含涉及生命周期的方法,如initialize(),login(),logout(),abort()和commit(),它有登录上下文调用。大多数容器在它们的安全设施中提供插入JAAS登录模块的适配器。在Tomcat 中是JAASRealm。它截取登录信息并产生一个回调句柄――JAASCallbackHandler,有能力处理两种回调――NameCallback和PasswordCallback。这些回调分别包含在j_username 和 j_password输入域中的值。然后,回调句柄被传递给登录模块来执行授权操作。
图8显示了登录模块类的对象模型
图8登录模块类的对象模型
登录模块中的大多数的方法(commit(), abort(), initialize(), logout())是自解释的。让我们仔细看一下login()方法。
Login()方法从回调句柄中取得会话ID和用户的回答。接下来,它会查询AuthenticationTokenCache,以取得与这个会话相关联的标识符。用户的输入会与标识符对象的内部状态相比较。如果成功,用户的会话会与一个很重要的调用AnonymousPrincipal相关联。由于应用只对较弱的授权机制感兴趣,所以你可以把这看做类似于把每个在你网站上的用户命名为anonymous.
登录上下文负责处理这个过程,并确保用户已经登录。用户现在就已经登录到系统,并有anonymous的身份。
图9显示了login()的顺序图
点击查看大图?
?
?
图9 login()的顺序图
与容器的安全设施整合
现在考虑把各个部分与容器的安全设施相整合。这是一个与服务相关的步骤。Tomcat为每个Web 应用管理一个配置文件。这个文件命名为.xml,放在/conf/Catalina/localhost文件夹下。把这个文件替换为下面的内容:%lt;?xml version='1.0' encoding='utf-8'?>
%lt;Context debug="9" docBase="d:/work/captcha-login/web" path="/clogin">
??%lt;Realm className="org.apache.catalina.realm.JAASRealm" appName="clogin" debug="99" roleClassNames="jw.jaas.AnonymousPrincipal" userClassNames="jw.jaas.AnonymousPrincipal"/>
%lt;/Context>此外,我们要求JAAS配置直接与应用对应的登录模块。
JAAS配置文件:点击查看大图?
?
?
图10这种应用的一种屏幕显示
必要的准备
为了构造和测试这个应用,我们需要Web container version 5.0或更高的版本,还有Ant build environment version 1.5或更高的版本。这些可以从Apache下载和安装。一旦有了这些准备,我们就可以安下面的步骤来构造和部署应用了。
安装和测试
·下载并解压缩jw-0307-captcha.zip到一个目录(如:d:\captcha)。解压缩出来的文件会包括源代码、库文件、Web应用和编译脚本。
·转到文件被解压缩到的目录,编辑文件setAntEnv.cmd。象文件名暗示的那样,这个文件包含开发环境启动时所需要的变量。编辑变量ANT_HOME和JAVA_HOME,使其指向正确的位置。所有其它的部分都是从这两个变量导出的,因此不需要改动。
·还是在这个目录下,编辑文件ant.developer.properties。这个文件包含影响应用编译采用的方法的部分。文件中的大部分都有一个有意义的默认值,你只需要设置build.home和server.home。
·现在,打开命令行窗口,并转到这个目录(d:\captcha),并执行setAntEnv.cmd批处理脚本。
·输入ant compile jar deploy来编译并部署这个应用。Ant脚本首先创建需要的目录,编译并安全模块代码,并打包为JAR文件。最后部署它。
·通过输入ant start命令来启动应用的服务端。Tomcat应该在以个独立的窗口启动并运行。
·打开浏览器,让它指向http://localhost:8080/clogin (clogin是在ant.developer.properties 中配置的application.name属性)。
·在测试完毕后,你可以通过关闭Tomact窗口的方式来停止服务。
无论什么时候改变了ant.developer.properties中的属性,都要重新编译,以保证所有的属性的依赖都是正确的。例如,如果你使用了out-of-box配置,标示符服务器将使用简单的标示符工厂。你也可以把文件ant.developer.properties中的captcha.token.factory的值改为jw.token.factory.JCaptchaTokenFactory来做更复杂的测试。运行ant clean-all deploy来传播变化。启动服务端并测试应用。
调试
根据你的环境的不同,应用在启动的时候可能会失败。编译脚本已经部署了一个用于Tomcat配置的的名为log4j的文件。你可以打开/logs/jaasModule.log或/logs/tomcat.log,看一下追踪和调试级的信息。jaasModule.log包含了登录模块和Web应用的调试信息。tomcat.log捕捉了从容器和部署的代码产生的登录消息。打开这些文件来寻找线索。
结论
在这篇文章中,我们浏览了J2EE Web应用的声明安全模型,还有扩展它来使用JAAS登录模块的方法。我们扩展了Tomcat的J2EE安全设施以支持一种用户定义的授权机制。这个授权机制是作为一个JAAS登录模块实现的,并在较弱的用户授权中使用了CAPTCHSs。对于某种应用领域,它能确定远端用户是一个人。使用容器提供的安全机制,我们可以很容易的确保应用的安全而不用更改任何代码。
应该注意到,CAPTCHAa既可以产生对用户有用的结果,也可以产生无用的结果。当开发者在选择授权机制的时候应该注意到这个问题。
关于作者
Anand Raman作为技术方面的合作者为Sapient在德里以外的地方工作。在过去的五年中一直从事JAVA和J2EE相关的技术工作。追踪J2EE的复杂性是他最感兴趣的。