为了使您更好的理解本篇,请参阅我之前写的几篇文章:
在我心目中理想的登录模块应该是这样的,我可以通过配置文件将一些事交给应用服务器的JAAS去处理,但是有必要时我也能够通过重载某些方法拦截登录处理的信息来达到自己的某些目的,比如记录登录事件、读取登录用户的信息放到session中,甚至是再加上一个处理验证码。这些能够实现吗?在Tomcat中好象不行,但是在Jboss中呢?
由于一直没有时间去解决在JAAS中集成自己编写的验证处理模块的技术,我上一篇介绍的方法在我的项目中用了半年,等我把其他紧要的事情了结之后又回过头来琢磨登录模块的事了。
我反反复复看了login-config.xml文件,我对这一段产生了一些想法:
<login-module code = "org.jboss.security.auth.spi.DatabaseServerLoginModule"
flag = "required">
我能不能继承org.jboss.security.auth.spi.DatabaseServerLoginModule,然后把这个code的值替换为我自己编写的类呢?Jboss既然提供了这么灵活的配置,那么说理论上应该是可行的,于是我又拿过来Jboss的源代码,仔细地了解了登录方面的一些接口和类,尤其是这个DatabaseServerLoginModule类更是把每一行代码都反复的研究过,我试着编写了一个com.benjamin.commons.security.LoginModule类,并且重载了login函数和initialize函数,加入了一些测试代码,试验了几次测试结果之后决定从重载login函数着手。这个函数是这样定义的:
public boolean login() throws LoginException ;
在这个函数里可以通过this.getUsername()来取得登录的用户名,然后根据用户名从数据库读取用户信息也没问题,但是session呢?我怎么取得session对象并往里面加东西?这个类即没有继承servlet也没有任何属性或参数与request有关,看来这个session是没办法获取了?我很不甘心就这样放弃,在google上输入了各种关键字来查找有用的信息,国内介绍JAAS的资料本来就少之又少,更别说这牛角尖一样偏门的方面了,终于网络不负有心人,我在一个国外的论坛上找到了一点资料,原来在Jboss中可以通过PolicyContext这个类来获取应用服务器的安全性方面的上下文,其中就包括获取HttpServletRequest,具体方法是这样的:
HttpServletRequest request =
(HttpServletRequest)PolicyContext.getContext(WEB_REQUEST_KEY);
后来我更发现只要是在Jboss容器运行的任何类都可以通过这种方式获取request对象。
有了request获取session当然就不成问题了:
HttpSession session=request.getSession(true);
最后我的这个login函数变成了这样:
public boolean login() throws LoginException {
boolean loginAccepted =false;
HttpServletRequest request = null;
try {
// System.out.println("开始调用JAAS验证 ...");
request = (HttpServletRequest)PolicyContext.getContext(WEB_REQUEST_KEY);
HttpSession session=request.getSession(true);
// String loadPath = this.getClass().getProtectionDomain().getCodeSource().getLocation().toString();
// log.info("开始调用JAAS验证 ...");
loginAccepted = super.login();
log.info("已通过JAAS验证,开始获取登录用户信息");
} catch (LoginException e) {
request.setAttribute("ex", e);
LoginException ex=new LoginException("用户" + this.getUsername() +"登录失败:" + e.getMessage());
log.error(e);
throw ex;
} catch (PolicyContextException e) {
LoginException ex=new LoginException("获取request失败:" + e.getMessage());
log.error(ex);
request.setAttribute("ex", ex);
throw ex;
}
if(loginAccepted) {
try {
String userName=this.getUsername();
//根据登录用户名读取用户信息保存到session中
if (loginBo.login(userName,request)) {
log.info(userName + " 登录成功.");
}
else{
log.error(userName + "登录失败!");
throw new LoginException("登录失败,无法从数据库获取用户信息!");
}
}
catch(Exception e) {
LoginException ex=new LoginException("登录失败:" + e.getMessage());
log.error(ex);
request.setAttribute("ex", ex);
throw ex;
}
}
return loginAccepted;
}
然后我把login-config.xml的相关配置改成了这个样子:
<application-policy name = "jbosstest">
<authentication>
<!-- 重载org.jboss.security.auth.spi.DatabaseServerLoginModule的类 -->
<login-module code = "com.benjamin.commons.security.LoginModule"
flag = "required">
<module-option name = "dsJndiName">java:/testds</module-option>
<module-option name = "principalsQuery">SELECT PASSWORD FROM REL_PUB_OPERATOR WHERE LOGINID=?</module-option>
<module-option name = "rolesQuery">SELECT ROLENAME,'Roles' FROM VREL_ROLE WHERE LOGINID=?</module-option>
<module-option name="hashAlgorithm">MD5</module-option>
<module-option name="hashEncoding">base64</module-option> </login-module>
</authentication>
</application-policy>
这样我想实现的读取用户信息到session的功能就实现了,但是我想更灵活更通用一点,我在这个项目中用的loginBo的类和其中的login(userName,request)方法很可能在其他项目中就不一样了,我应该也通过配置来实现自定义自己的登录完毕后的处理类。我在前面几次测试的过程中发现<login-module … /> 的值是可以在loginModule类的initialize函数里通过options.get(String)方法获取得到,于是我改了改登录配置文件,加了一行:
<module-option name="loginBo">com.system.business.LoginBo</module-option>
变成这样:
<application-policy name = "jbosstest">
<authentication>
<!-- 重载org.jboss.security.auth.spi.DatabaseServerLoginModule的类 -->
<login-module code = "com.benjamin.commons.security.LoginModule"
flag = "required">
<module-option name = "dsJndiName">java:/testds</module-option>
<module-option name = "principalsQuery">SELECT PASSWORD FROM REL_PUB_OPERATOR WHERE LOGINID=?</module-option>
<module-option name = "rolesQuery">SELECT ROLENAME,'Roles' FROM VREL_ROLE WHERE LOGINID=?</module-option>
<module-option name="hashAlgorithm">MD5</module-option>
<module-option name="hashEncoding">base64</module-option>
<module-option name="loginBo">com.benjamin.system.business.LoginBo</module-option>
</login-module>
</authentication>
</application-policy>
然后又在我的LoginModule类里重载了initialize函数:
public void initialize(Subject subject,CallbackHandler callbackHandler,Map sharedState,Map options){
super.initialize(subject, callbackHandler, sharedState, options);
if(options.get("loginBo")!=null){
try {
loginBo=(ILoginBo)Class.forName(options.get("loginBo").toString()).newInstance();
} catch (InstantiationException e) {
log.error(e);
} catch (IllegalAccessException e) {
log.error(e);
} catch (ClassNotFoundException e) {
log.error("没有找到loginBo类:" + e);
}
}
}
这样我就可以很灵活地通过配置实现代码的移植重用,在其它项目里只要重新实现一个loginBo类并修改配置<module-option name="loginBo">为这个类就可以了。
完成之后重新发布到jboss上,运行Jboss,打开登录页面,登录成功,非常完美!
按理这样就算大功告成了,但是当我再次登录的时候发现登录后转向的页面出了一些错误,session里读出来的值都是null,再试了几次,换另外一个用户名登录又是好的,但退出后再次用这个用户名登录时就不行了。这个问题困扰了我好几天,我感觉应该是Jboss保存了登录用户的信息在缓冲区导致一些方面冲突。但是怎么解决呢?
最后还是在网上找到了解决办法,看来离开网络我是没办法再混下去了。
修改JBOSS安装目录下的server/default/deploy/security-service.xml文件,找到这样的一个节点
<mbean code="org.jboss.security.plugins.JaasSecurityManagerService"
name="jboss.security:service=JaasSecurityManager">
<attribute name="DefaultCacheTimeout">60</attribute>
<attribute name="DefaultCacheResolution">600</attribute>
</mbean>
将DefaultCacheTimeout和DefaultCacheResolution的值都改为0,即不将登录信息保存到缓冲区,其它地方不变,修改完后这两个节点应该是这样的:
<attribute name="DefaultCacheTimeout">0</attribute>
<attribute name="DefaultCacheResolution">0</attribute>
重启Jboss,登录,没问题,再次登录也没问题,经过多次测试后发现这一招果然管用,我用的Jboss版本是 4.0.3 SP1,后来我在布署其它版本的Jboss时发现没有security-service.xml文件,但是我在这个版本的Jboss的主目录下的server/default/conf/jboss-service.xml配置文件里也找到了同样的DefaultCacheTimeout和DefaultCacheResolution配置,照样修改为0,运行以后发现完全正常。
至此我想实现的功能基本上都可以实现了,我之所以加上一个“基本上”,是因为我前面还提到了一个加验证码的功能,我试过几次没有成功,因为我在登录模块里只能获取到页面提交的用户名和密码信息,我试过加上一个textbox来填写验证码,但是不知道在哪里能取得出来,前面所提的方法里获取的request里是取不到,不知道还有什么其它方式。希望有兴趣的朋友可以自己去试一下,试出来了别忘了告诉我。