Javascript XMLHttpRequest与Spring Security配合实现具有鉴权认证功能的系统

使用Javascript开发基于Ajax架构的程序时如果不使用各种Ajax开发框架,就得自己处理HTTP通信问题,下面就鉴权认证功能的实现方法进行总结。



开发环境

客户端是使用XMLHttpRequest的Ajax程序,服务器是Tomcat, Spring Framework, Spring Security, Hibernate, MySQL, Lucene。

为了达到比较高的安全级别,鉴权使用HTTP Digest方式,而不使用HTTP Basic或者基于表单(Form based)的方式。

HTTP Digest和basic方式都是比较底层的,与基于表单的方式相比,在鉴权过程中XMLHttpRequest对象的responseText属性中没有鉴权表单页面内容,而在鉴权完成后在responseText中存储实际的目标页面内容或者是401失败内容。而在调用open()方法时可以直接指定用户名和口令。因此,十分方便。缺点是无法定制一个鉴权表单,因为用户看到的帐号和口令输入窗口实际上是浏览器产生的。



服务器配置

pring Security 2.0以后有了命名空间方式配置鉴权过滤器(filter),十分容易使用,但是要实现一些复杂的功能,还是要直接配置各种过滤器和处理器bean。例如,HTTP Digest鉴权方式就不能使用命名空间方式进行配置,必须要配置Digest过滤器bean。下面将混合使用命名空间配置方式和直接配置bean方式。

起初,采用了HTTP Basic鉴权方式,使用命名空间,配置如下:

<http auto-config="true">
<intercept-url pattern="/**" access="ROLE_TELLER" />
<http-basic />
</http>

用户帐号和口令存放在服务器MySQL数据库中,也是采用命名空间方法配置authentication provider:

<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>

然后要配置dataSource bean,不能使用命名空间方式,直接配置bean

<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<beans:property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<beans:property name="url" value="jdbc:mysql://localhost/"/>
<beans:property name="username" value="数据库登录帐号"/>
<beans:property name="password" value="数据库登录口令"/>
</beans:bean>

以上配置都存放在服务器应用的上下文配置文件中。这样服务器就配置好了。

数据库中存放用户帐号和口令的表结构在dbinit.txt中定义。



客户端程序片段

下述代码只能运行支持W3C标准的浏览器上,如果要运行在IE上需要将XMLHttpRequest对象用ActiveX对象替代。

if(typeof XMLHttpRequest == "undefined") {
alert("XMLHttpRequest unavailable"); return;
}
var oRequest = new XMLHttpRequest(); oRequest.open("get", "资源地址", false, "用户帐号", "用户口令");
oRequest.setRequestHeader("Content-type", "text/xml; charset=utf-8");
oRequest.send("");
alert(oRequest.responseText);
if(oRequest.status != 200) {
alert("Error: " + oRequest.status);
return;
}else {
alert(oRequest.getAllResponseHeaders());
var newWin = window.open("_blank", "testWin");
newWin.document.location = "资源地址";
}

上例,用户名和口令都带上了,如果正确,就不会出现要求输入帐号和口令的对话框了,否则,会弹出一个对话框,要求重新输入。



测试过程中出现的异常

因为Spring Framework和Spring Security的类库很大,在程序的运行环境中并不想把所有的库打包在一起,所以出现的异常全部是找不到某个类。使用的版本是:Spring Framework 2.5.1,Spring Security 2.0.3,跟官方推荐的配套不一致。不过在使用中没有发现问题。

首先遇到的异常是:

[ERROR] 17:50:55 ContextLoader - Context initialization failed
org.springframework.beans.factory.BeanDefinitionStoreException:
Unexpected exception parsing XML document from ServletContext resource [/WEB-INF/上下文配置文件.xml];
nested exception is org.springframework.beans.FatalBeanException: Invalid NamespaceHandler class
[org.springframework.security.config.SecurityNamespaceHandler] for namespace [2]:
problem with handler class file or dependent class; nested exception is java.lang.NoClassDefFoundError:
org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator
[ERROR] 17:59:56 ContextLoader - Context initialization failed
org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception
parsing XML document from ServletContext resource [/WEB-INF/上下文配置文件.xml];
nested exception is org.springframework.beans.FatalBeanException:
Invalid NamespaceHandler class [org.springframework.security.config.SecurityNamespaceHandler] for namespace [3]:
problem with handler class file or dependent class; nested exception is java.lang.NoClassDefFoundError:
org/springframework/security/config/InternalInterceptMethodsBeanDefinitionDecorator

根据[4],Spring Security 2.0.x依赖于于AOP Alliance API和Spring AOP,将Spring包中的lib/aopalliance/aopalliance.jar和dist/modules/spring- aop.jar拷贝到类加载路径下,问题解决。

后来将用户名和口令配置在文件中并使用MD5进行加密,又出现了:

ERROR] 19:04:48 [default] - Servlet.service() for servlet default threw exception
java.lang.ClassNotFoundException: org.apache.commons.codec.binary.Hex
应该是使用了MD5编码的口令造成的,因为需要apache的codec库进行编码,所以将SpringFramework包中的lib/jakarta-commons/commons-codec.jar拷贝到类加载目录