W eb应用的前景是在不断的演进中的,它已经从最开始作为共享文档和信息的方式演化为业务管理的平台,而在这种应用中,许可和 授权是一个关键的特征。Web的应用前景还在不断的演进中,而本文把关注放在面向群体的应用中,例如博可和维基。
在这种应用中,由于作者希望有交流和反馈,所以授权不是非常严格的。有时,由于害怕识别,会造成失去一些人可能做出的贡献。但是,缺少授权就会带来诸如垃圾邮件这类的问题。这里有几条在Web上抽取的信息:
很明显,垃圾邮件发送源必须被识别出来。大多数者类恶意的攻击发生数据匹配模式被识出来之后。一个可能的方法是人工而不是有计算机来停止这种攻击,很显然这是个挑战。通过图灵测试,按一个有名的计算机专家――阿兰。图灵命名的试验,可以确定机器能够实现类似人类操作的能力。者类测试中最有名的一个是CAPTCHA (an acronym for completely automated public Turing test to tell computers and humans apart)。这个测试常用来表明以个典型的情况:混乱或含义模糊的词,人很容易识别,但对于光学识别软件来讲却很困难。
图1是一个典型的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:
<web-app>
<!-- ... -->
<!--
Define the security constraint. This will limit the /admin/* portion of
the application to only be accessible to users within the "admin" role.
When an unauthenticated user attempts to access this section of the site,
they will be automatically presented with the login page.
-->
<security-constraint>
<!-- Define the context-relative URL(s) to be protected -->
<web-resource-collection>
<web-resource-name>Protected Area</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<!-- Define the roles that are allowed to access this URL with the given methods -->
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<!-- Transport guarantee could be used to guarantee an HTTPS protocol -->
<user-data-constraint>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!--
Define the method for authenticating a user when requesting a restricted
page. Methods include BASIC (the simple pop-up dialog), FORM and
CERTIFICATE.
-->
<login-config>
<!-- We will use form based authentication -->
<auth-method>FORM</auth-method>
<realm-name>Default Realm</realm-name>
<!-- where should the user be forwarded to enter his credentials -->
<form-login-config>
<form-login-page>/login/login.jsp</form-login-page>
<!--
On error the user will be shows this page It can also server side
forward back to the login page, which is popular behavior for most
sites.
-->
<form-error-page>/login/error.jsp</form-error-page>
</form-login-config>
</login-config>
<!--
Finally a list of all security roles in the application must be given.
-->
<security-role>
<description>Capable of administrating the site</description>
<role-name>admin</role-name>
</security-role>
</web-app>
这个简单的部署描述符包含以下几部分的安全配置:
·约束对有以/admin/*模式开头的URLs的存取(URLl模式)
·在/admin下的资源只能使用HTTP GET或POST来存取(HTTP方法)
·资源能在标准的HTTP连接方式下提供服务(传输保证)
·只有具有管理员身份的用户才能存取这些资源(身份命名)
·对远程用户使用基于规则的授权(授权方法)
·给用户显示一个登录的页面――/login/login.jsp ,以便用户输入信息来确认身份(形成登录页面)
·如果在授权过程中发生错误,给用户显示以个页面来提示出错――/login/error.jsp(形成出错页面)
扩展一个容器的安全设施 JAAS(Java Authentication and Authorization Services)实现了一个JAVA应用的可插入性授权模块(PAM)。它容许平行开发安全部分和应用部分。开发者可以从这些选项中选择并在应用中配置。由于容许与应用平行开发,所以JAAS有一些优点,还可以促进它在不同的应用中重用。
JAAS在应用的服务端也同样有价值。然而,JAAS在J2EE中并没有取得同样的成功。直到最近,才开放了一些可定制的API用于扩展安全设施。但情况在改变。应用的服务端现在提供了适配器,可以容许把JAAS整合近已有的安全设施。这种整合仍然是与具体的应用相关的,并且非常复杂。
Tomcat提供了一种使用JAAS的相当简单和直接整合的方法。用户登录模块是用配置文件来配置的(Tomcat realm configuration and the standard JAAS configuration)。当服务端需要调用登录模块时,它把所有的请求都路由到org.apache.catalina.realm.JAASRealm适配器。把JAAS整合进Tomact的细节可参考相应的资料Resources。
在这篇文章,我们实现了一个JAAS模块,并把它整合近Tomcat服务端,以提供J2EE的安全解决方案。
解决方法 在叙述实现方面之前,先描述一下解决方案的目标和所用的方法。
目标:
·为Web应用提供一种较弱的授权机制。这里,较弱的授权机制的含义是安全模块或应用不区分用户是否是在远程访问。
·不要求每个用户在系统中有惟一的一个标识(登录名)。这可以对远程用户做一定的隐藏。
·这种授权机制能区分计算机和用户,这样可以防止垃圾邮件的源自动登录并滥用资源。我们用CAPTCHAs来做测试。
·这个授权机制应该基于J2EE的安全模型。我们要避免与这个模型不一致的方法。
根据以上的目标,很显然,我们要保证每次会话中都是由实际的用户参与的。应用的服务端管理会话来保持用户的状态。当一个未授权的用户访问受保护的资源的时候,J2EE的安全模块要把用户转到登录页面。这个登录页面产生以个惟一的CAPTCHA并把它与用户的会话联系起来。这个登录页面就以一幅图像的形式显示这个CAPTCHA,并要求用户识别这幅图像。这个登录页面还有一个包含当前会话ID的隐藏起来的输入域。
用户把自己识别的结果添入输入域并提交。在得到反馈后,负责登录的模块取出对话的ID和用户的反馈。然后,把反馈与这个会话相关联的CAPTCHA相比较。如果匹配,那么这个对这个用户的鉴别就通过了,并把这个用户的身份定为anonymous。
在授权后,远端的用户就可以以anonymous用户的身份来存取所有受保护的资源。
图4说明了我们的方法
图4 我们的方法
实现 实现一个新授权机制的过程是相当明了的。整个过程可分为四个过程:
·保护Web资源
·为每个会话产生以个惟一的标识
·与已有的安全容器整合
·测试
下面详细叙述以下这些过程。
保护Web资源 Web资源保护用J2EE声明安全机制。下面的是web.xml文件的一个片断,显示了如何实现要求的配置。
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app>
<!-- constrain a section of the site -->
<security-constraint>
<display-name>Anonymous Security Constraint</display-name>
<web-resource-collection>
<web-resource-name>Protected Area</web-resource-name>
<url-pattern>/security/protected/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>anonymous</role-name>
</auth-constraint>
</security-constraint>
<!-- Default login configuration uses form-based authentication -->
<login-config>
<auth-method>FORM</auth-method>
<realm-name>Anonymous Form-Based Authentication Area</realm-name>
<form-login-config>
<form-login-page>/security/protected/login.jsp</form-login-page>
<form-error-page>/security/protected/error.jsp</form-error-page>
</form-login-config>
</login-config>
<!-- Security roles referenced by this web application -->
<security-role>
<role-name>anonymous</role-name>
</security-role>
<!-- The Usual Welcome File List -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
这个片断包含了这样一些内容:
·所有保存在/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="Your authentication token"/>
<%-- 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配置文件:
clogin {
jw.jaas.AnonymousLoginModule required debug=true;
};
为简便起见,编译脚本要自动产生所有的配置条目。
测试
为测试安全模块,我建了一个Web应用,叫Anonymous Bulletin Board(ABB),这是一个容许用户讨论日常问题的应用,它的主页要列出迄今所有发表的贴子。用户也可以在上面发贴子。容许发贴子的页是用生命安全的方式来保护的。只有通过授权的用户才能发贴子。
图10显示了这种应用的一种屏幕显示。
点击查看大图
图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的复杂性是他最感兴趣的。