技术开发 频道

使用Acegi进行身份认证[之二]

退出系统的后置处理

SecurityContext保存在HttpSession中,当用户退出系统时必须清除之,否则SecurityContext将一直保存在HttpSession中,需要等到Session过期后才会被清除,这将造成额外的内存消耗。从另外一个方面说,在退出系统时常常需要执行一些相关的操作,如记录用户退出系统的日志、将登录信息保存到Cookie中等。

Acegi为完成以上一系列由退出系统引发的操作,专门提供了一个退出过滤器:org.acegisecurity.ui.logout.LogoutFilter,它允许我们通过配置完成相关的操作:

代码清单 11 applicationContext-acegi-plugin.xml

退出系统后置处理配置
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"> <property name="filterInvocationDefinitionSource"> <value>/**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,logoutFilter ① </value> </property> </bean> <bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">②退出系统过滤器 <constructor-arg> ②-1退出系统前需要执行的操作 <list> <bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" /> </list> </constructor-arg> <constructor-arg value="/index.jsp" />②-2退出系统后转向的URL ②-3用于响应退出系统请求的URL <property name="filterProcessesUrl" value="/j_acegi_logout"/> </bean> …

在①处,我们在过滤器链中添加一个logoutFilter,它负责处理用户退出系统的操作。退出系统过滤器需要以下三方面的信息:

1) 哪一个URL是退出系统的HTTP请求,这通过filterProcessesUrl属性指定,②-3所示。LogoutFilter默认的退出系统URL即是“/j_acegi_logout”,这里显式进行配置是为了说明可以根据需要进行调整;

2) 退出系统时需要执行哪些处理器,通过LogoutFilter的构造函数指定,如②-1所示。处理器必须实现org.acegisecurity.ui.logout.LogoutHandler接口,Acegi为该接口提供了两个实现类,分别是SecurityContextLogoutHandler和 TokenBasedRememberMeServices,前者将SecurityContext从HttpSession中删除,而后者将Anthentication中的用户名/密码保存到客户端的Cookie中,以便下次用户访问系统时直接通过Cookie中的用户名/密码进行自动登录。我们将在下一节学习到TokenBasedRememberMeServices的知识。

3) 退出系统后转向哪个URL,通过构造函数参数指定,如②-2所示。

配置好退出系统过滤器后,在需要在系统页面中提供一个退出系统的操作链接:

<A href="<c:url value="/j_acegi_logout"/>">退出系统</A>

注意粗体所示代码代表退出系统所对应的URL,它必须和LogoutFilter的filterProcessesUrl属性一致。这样,当用户点击页面中的“退出系统”链接后,LogoutFilter拦截这个URL请求,并调用SecurityContextLogoutHandler将SecurityContext从HttpSession中清除掉,最后转向/index.jsp页面。

实施Remember-Me认证
很多网站的用户登录页面都提供了一个类似于“两周内不用再登录”、“记住我的帐号”等功能,其原理是在用户登录成功后使用客户端浏览器的Cookie记录用户登录信息,当下次再访问相同站点时,直接从Cookie中取得用户登录信息并进行自动登录。这即是经典的Remember-Me的功能,该功能在一定程度上降低了用户频繁登录的麻烦。根据系统安全性需求的不同,Remember-Me可能在Cookie中保存用户名/密码或仅保存用户名,前者可以完成自动登录,而后者只是让用户避免输入用户名。

如果在Cookie中记录用户名/密码,虽然可以避免每次访问网站都进行登录的麻烦,但这把双刃剑的反面是黑客可以在一定条件下获取这个免检的通行证。为了在给用户带来便利的同时尽力降低潜在的风险,Cookie保存用户名/密码的方式变得非常关键,以下几点是必须考虑的问题:

1) Cookie是易受攻击的,多用户共享浏览器和跨站点脚本攻击都可能使Cookie失窃;

2)将用户名/密码保存在Cookie中,意味着用户可以在不显式进行登录的情况下,获取正常登录的一切权限;

3)一切可以从Cookie中反推出密码明文的存储方式都是不可接受的;

4)必须将客户端IP信息绑定在Cookie中,这样即使Cookie失窃,也不可能在其它机器使用。

如果说HttpSessionContextIntegrationFilter通过HttpSession使Authentication获得跨请求共享的能力,那么Remember-Me则通过Cookie使Authentication获得跨多个Session的能力。Remember-Me功能可以视为一套解决方案,以下是Remember-Me中最关键的三个问题:

1) 在用户登录时,获取用户名/密码的信息,并将其以一定方式保存到Cookie中;

2) 在Cookie有效时间内,当用户访问站点安全页面时,自动进行登录;

3) 必须提供一个功能,让用户可以手工清除Remember-Me Cookie。

org.acegisecurity.ui.rememberme.RememberMeServices是Remember-Me方案中最关键的一个接口,它定义了以下几个方法:

l void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication):登录成功后调用该方法,将用户名/密码保存到Cookie中;

l void loginFail(HttpServletRequest request, HttpServletResponse response):登录失败后调用该方法;

l Authentication autoLogin(HttpServletRequest request, HttpServletResponse response):从Cookie中自动获取用户名/密码进行自动登录。

loginSuccess()和loginFail()方法的调用已经编制到Acegi的AbstractProcessingFilter抽象过滤器中,这意味着任何注入了RememberMeServices实例的过滤器都会以适合的方式调用这两个方法。而autoLogin()方法则通过RememberMeProcessingFilter进行调用,当RememberMeProcessingFilter发现SecurityContextHolder中不存在有效的Authentication时,autoLogin()方法就会被执行。

Acegi为RememberMeServices接口提供了两个实现类,它们分别是:

l NullRememberMeServices:类似于适配器的实现类,它不做任何有意义的事情,这是AbstractProcessingFilter中默认的实现类;

l TokenBasedRememberMeServices:基于凭证(一般指用户名/密码)的Remember-Me实现类,它真实地实现了接口中的方法。

在登录时将用户名/密码记录到Cookie中
我们第一个要做的工作是通过调整AuthenticationProcessingFilter的配置,在处理用户登录页面提交的用户认证信息时,将用户名/密码通过Response记录到客户端的Cookie中:

代码清单 12 applicationContext-acegi-plugin.xml

记录Remember-Me的Cookie信息

<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> … ①注入一个RememberMeServices <property name="rememberMeServices" ref="rememberMeServices"/> </bean> <bean id="rememberMeServices" ②RememberMeServices配置 class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices"> <property name="tokenValiditySeconds" value="432000"/>-1Cookie有效时间,单位为秒 <property name="key" value="baobaotao"/>-2 Cookie中的键值 </bean>

通过以上的配置,我们在处理用户登录的同时将用户名/密码的信息记录到Cookie中。authenticationProcessingFilter在完成用户身份认证后,如果认证成功,调用rememberMeServices的loginSuccess()方法,该方法将用户名/密码按以下方式进行编码,编码后再写到客户端的Cookie中:

base64(username + ":" + expirationTime + ":" + md5Hex(username + ":" + expirationTime + ":"+password + ":" + key))

base64()表示进行BASE64编码操作,而md5Hex()表示进行MD5摘要并将结果值以HEX(十六进制)进行编码。注意计算式中粗体所示key操作项,Acegi通过key防止整个加密串被恶意篡改。因为key是在服务端中指定的值,黑客无法进行猜测,在服务端通过如②-2所示的key属性指定该值。

Cookie的有效时间通过tokenValiditySeconds指定,默认为两个星期。②-1处我们显式指定为5天(对应的秒数)。

我们知道authenticationProcessingFilter处理对应/j_acegi_security_check的请求,我们应该让用户决定是否使用Remember-Me的功能,这可以通过一个名为“_acegi_security_remember_me”的HTTP参数来决定,当登录请求表单包含该参数时(勾选上对应的复选框),服务端认为需要启用Remember-Me功能,否则不启用Remember-Me功能。所以,我们必须相应地调整登录页面表单,如代码清单 13所示:

代码清单 13 index.jsp:添加是否使用Remember-Me功能的控制参数

<%@ page contentType="text/html;charset=UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><form name="form1" method="post" action="<c:url value="/j_acegi_security_check"/>"> 用户名:<input type="text" name="j_username"/><br/> 密 码:<input type="password" name="j_password"/><br/> ①用户可以通过勾选或取消该复选框决定是否启用Remember-Me功能 <input type="checkbox" name="_acegi_security_remember_me">5天内不用再登录 <input type="submit" value="登录"/> </form>
0
相关文章