acegi流程分析之一《Acegi 中的HttpSessionEvent 监听机制 窥视Acegi的 工作流程》

先推荐一下SpringSide,写得有点乱,欢迎指正错误。msn:qian_bj@hotmail.com

 


 

Acegi 中的HttpSessionEvent 监听机制 窥视Acegi 工作流程。

 

Acegi结合了Spring 提供了不错的HttpSessionEvent监听机制

使用AcegiHttpSessionEvent监听机制,我首先要在web.xml中增加一个Listener

org.acegisecurity.ui.session.HttpSessionEventPublisher

有人要问这不是一个Listener么,类名应该是一个*Listener什么的啊。的确它实现了javax.servlet.ServletContextListener接口和javax.servlet.http.HttpSessionListener接口。前者是在web应用创建时,可以可以通过ServletContext来获取一个SpringApplicationContext。后者是在HttpSession创建和超时时发布利用SpringApplicationContext. publishEvent()方法发布事件的,注意并不时发布的HttpSesionEvent,而是有HttpSession作为构造参数,创建了HttpSessionCreatedEventHttpSessionDestroyedEvent,这两个Event都是SpringApplicationEvent的子类,这样Spring才会处理这些Event。事件发布后,Spring会在上下文查找实现了ApplicationListener接口的接口子类来处理这两个Event

 

到这,你心里或许有些疑惑,HttpSessionEvent发布了,那个ApplicationListener来处理这个HttpSessionEvent呢?注意,重要人物出场了。

org.acegisecurity.concurrent. SessionRegistryImpl出场了。看了它的源代码,你会发现它还实现另外一个接口org.acegisecurity.concurrent. SessionRegistry

这个定义如下一些方法

public interface SessionRegistry {

    public Object[] getAllPrincipals();

    public SessionInformation[] getAllSessions(Object principal, boolean includeExpiredSessions);

    public SessionInformation getSessionInformation(String sessionId);

    public void refreshLastRequest(String sessionId);

    public void registerNewSession(String sessionId, Object principal)

        throws SessionAlreadyUsedException;

    public void removeSessionInformation(String sessionId);

}

这些方法的功能通过看方法名就能知道了。

看一下SessionRegistryImpl. onApplicationEvent()方法,看它做了些什么

    public void onApplicationEvent(ApplicationEvent event) {

        if (event instanceof HttpSessionDestroyedEvent) {

            String sessionId = ((HttpSession) event.getSource()).getId();

            removeSessionInformation(sessionId);

        }

    }

它怎么只处理HttpSessionDestoryedEvent,这就说SessionRegistryImpl只处理HttpSession的销毁,也就是这又是为什么呢。如果你仔细看了SessionRegistry的接口定义,或许能明白一些,SessionRegistryImpl需要存储客户端用户的SessionInformationSessionInformation里面又有什么,SesssionInformation有四个字段

    private Date lastRequest;

    private Object principal;

    private String sessionId;

    private boolean expired = false;

除了principal,其他三个字段应该很容易理解,那这个Principal是什么东西呢,Principal可以理解为当前客户端用户的身份信息,这个身份信息可以包含用户名,用户密码,用户在系统里面拥有的权限。Acegi提供了一个UserDetail来表示用户的身份。

public interface UserDetails extends Serializable {

    public GrantedAuthority[] getAuthorities();

    public String getPassword();

    public String getUsername();

    public boolean isAccountNonExpired();

    public boolean isAccountNonLocked();

    public boolean isCredentialsNonExpired();

    public boolean isEnabled();

}

顺便提一下SessionRegistryImpl处理HttpSessionDestoryedEvent是,只是把这个HttpSesion的有关信息也就是SessionInformation从存储中移出。

 

 

通过上面的分析,你觉得每当新的HttpSession创建时,SessionRegistryImpl也处理这个Event还有意义吗?事实上SessionRegistryImpl没有处理HttpSessionCreatedEventAcegi也没有提供其他的Spring ApplicationListener来处理这个HttpSessionCreatedEvent

注意,我们的程序一般并不用直接与SessionRegistryImpl打交道,你只需在Spring的配置文件定义一个Bean就行了,如

<bean id="sessionRegistry" class="org.acegisecurity.concurrent.SessionRegistryImpl"/>

只是如此而已。

 

那么当HttpSession创建时,Acegi并不做处理,还会有其他的咚咚来处理么;SessionRegistryImpl还需要存储用户的Principal,那么什么咚咚与SessionRegistryImpl打交道呢?

有,但不过不是ApplicationListener了,别忘了,SpringSide还定义了一系列的FilterAcegi来做过滤(详情请见org.springside.bookstore.plugins.security. applicationContext-security-acegi.xml),那什么Filter与实现了SessionRegistry接口的SessionRegistryImpl打交道呢?下面我来介绍一下ConcurrentSessionFilter,这个Filter是与SessionRegistryImpl打交道的。我们先看一下该FilterdoFilter()的方法(截取了部分代码)

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

        throws IOException, ServletException {

------------------

 

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        HttpServletResponse httpResponse = (HttpServletResponse) response;

 

        HttpSession session = httpRequest.getSession(false);

 

        if (session != null) {

            SessionInformation info = sessionRegistry.getSessionInformation(session.getId());

 

            if (info != null) {

                if (info.isExpired()) {

                    // Expired - abort processing

                    session.invalidate();

 

                    String targetUrl = httpRequest.getContextPath() + expiredUrl;

//Session已经过期,转向一个targetUrl,比如登陆页面

                    httpResponse.sendRedirect(httpResponse.encodeRedirectURL(targetUrl));

 

                    return;

                } else {

                    // Non-expired - update last request date/time

                    info.refreshLastRequest();

                }

            }

        }

 

        chain.doFilter(request, response);

    }

各位看官可能要问为什么要调用SessionInformation.isExpired()来检测通过HttpSessionid来获取SessionInformation是否过期呢,过期(超时)SessionInformation 不是由SessionRegistryImpl清除掉了吗?

 

如果你能跟我一样发现这个问题,说明看得比较仔细和投入 :)

下面来介绍一下ConcurrentSessionController接口,因为SpringSide里面用了这个实现,所以一定要介绍下:)。

ConcurrentSessionController接口有两个方法:public void checkAuthenticationAllowed(Authentication request)public void registerSuccessfulAuthentication(Authentication authentication)。前一个方法检查当前用户请求的认证(这里是姑且把Authentication翻译成认证)是否是系统允许的,如果没有经过允许则抛出AuthenticationException异常给Acegi处理;后一个方法把认证赋予当前用户。

 

Acegi提供ConcurrentSessionController接口的一个增强实现ConcurrentSessionControllerImpl,提供更多的控制策略,比如配置是否允许用户重复登陆,最多允许多少个HttpSession

下面看下ConcurrentSessionController的接口实现ConcurrentSessionControllerImpl,不过是我们只看其中一部分,Aecgi是怎么让SessionInformation过期的

 

//允许HttpSession超过设定值  

 protected void allowableSessionsExceeded(String sessionId, SessionInformation[] sessions, int allowableSessions,

        SessionRegistry registry) {

        if (exceptionIfMaximumExceeded || (sessions == null)) {

            throw new ConcurrentLoginException(messages.getMessage("ConcurrentSessionControllerImpl.exceededAllowed",

                    new Object[] {new Integer(allowableSessions)}, "Maximum sessions of {0} for this principal exceeded"));

        }

//检查SessionRegistryImpl中最后更新的SessionInformation

        SessionInformation leastRecentlyUsed = null;

 

        for (int i = 0; i < sessions.length; i++) {

            if ((leastRecentlyUsed == null) || sessions[i].getLastRequest().before(leastRecentlyUsed.getLastRequest())) {

                leastRecentlyUsed = sessions[i];

            }

        }

//找到一个倒霉蛋,对不起,我要把你的SessionInformation设置为过期,这样ConcurrentSessionFilter在处理到你的SessionInformation时会让你重新登陆系统

        leastRecentlyUsed.expireNow();

    }

 

 

分析到这里,不禁感叹一句,Acegi提供了多么贴心的功能,感激得快流泪了。

 

写了这么多,你也看了这么多,快要骂我了,写了这么多,就说了Acegi怎么处理HttpSession超时,怎么处理每个HttpSession中的用户信息,怎么管理Session用户,以及那个倒霉蛋,还有那个倒霉蛋转到登陆页面再次输入用户名和密码的。

那用户访问受限资源时,Acegi又是怎么做的呢。好,各位稍休息一会,我也抽根烟调整下思路。

 

 

 

好,第二部分开始了。Acegi怎么保护受限资源的。这里稍微点一下,资源可以分为多种,比如某个类的方法啊,URL啊,只要写相应的拦截器去处理就OK了。拦截器检查到该用户没有权限访问资源,很简单,抛出异常 (至于针对方法和URL的拦截器是怎么工作的,这部分我们先不讲)

好戏正式开始---Acegi怎么处理这个异常的。你可以已经想到了,使用Filter。对,是FilterAcegi定义个Filter,用来处理这些异常,这个Filterorg.acegisecurity.ui.ExceptionTranslationFilterExceptionTranslationFilter

 

        try {

            chain.doFilter(request, response);

            if (logger.isDebugEnabled()) {

                logger.debug("Chain processed normally");

            }

        } catch (AuthenticationException ex) {

            handleException(request, response, chain, ex);

        } catch (AccessDeniedException ex) {

            handleException(request, response, chain, ex);

        } catch (ServletException ex) {

            if (ex.getRootCause() instanceof AuthenticationException

                || ex.getRootCause() instanceof AccessDeniedException) {

                handleException(request, response, chain, (AcegiSecurityException) ex.getRootCause());

            } else {

                throw ex;

            }

        } catch (IOException ex) {

            throw ex;

        }

ExceptionTranslationFilter主要处理2个异常,AuthenticationExceptionAccessDeniedException,这2个异常都是AcegiSecurityException的子类。

拦截到异常后,需要把用户的浏览器转到一个登陆页面,让用户提供用户名和密码来验证用户的身份。这样异常就有一个去向问题,提供什么样的用户认证入口,是转到本地登陆页面,还是远程的单点登陆服务器呢,还是其他的什么地方。因此ExceptionTranslationFilter必须定义一个合适AuthenticationEntryPoint来提供用户的认证入口。在SpringSide中,定义了这样一个入口

    <!-- AccessDeniedException且存在anonymous user时触发 -->

    <bean id="authenticationProcessingFilterEntryPoint"

          class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">

        <property name="loginFormUrl">

            <value>/security/login.jsp</value>

        </property>

        <property name="forceHttps" value="false"/>

</bean>

    <!-- ===================== HTTP REQUEST SECURITY ==================== -->

    <!-- 异常转换过滤器,主要是处理AccessDeniedExceptionAuthenticationException,将给每个异常找到合适的"去向"  -->

    <bean id="exceptionTranslationFilter"

          class="org.acegisecurity.ui.ExceptionTranslationFilter">

        <property name="authenticationEntryPoint"

                  ref="authenticationProcessingFilterEntryPoint"/>

    </bean>

 

这部分应该很简单的

我们继续下一部分,Acegi是怎么保护受限资源,先将怎么保护URL的吧。估计你又想到了,Filter,对,还是FilterAcegi定义了一个org.acegisecurity.intercept.web.FilterSecurityInterceptor来处理用户是否有权限访问某个URL,先看一下SpringSide是怎么定义这个Filter

    <!--

         filterInvocationInterceptor在执行转向url前检查objectDefinitionSource中设定的用户权限信息

         过程:

         首先,objectDefinitionSource中定义了访问URL需要的属性信息(这里的属性信息仅仅是标志,告诉accessDecisionManager要用哪些voter来投票)

         然后,authenticationManager掉用自己的provider来对用户的认证信息进行校验。

         最后,有投票者根据用户持有认证和访问url需要的属性,调用自己的voter来投票,决定是否允许访问。

     -->

    <bean id="filterInvocationInterceptor"

          class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">

        <property name="authenticationManager"

                  ref="authenticationManager"/>

        <property name="accessDecisionManager"

                  ref="httpRequestAccessDecisionManager"/>

        <property name="objectDefinitionSource"

                  ref="filterDefinitionSource"/>

    </bean>

我再罗嗦一下它的详细流程。

1,  filterInvocationInterceptor先检查用户请求的这个Request是否已经过滤过了,如果是的话,并且在filterInvocationIntercepto已经配置系统只希望每个请求Filter只做一次处理,Filter不做任何安全处理,把控制权交由其他Filter否则进行下一步

2,  使用Request.setAttribute()方法,设置此request已经经过处理了,步骤1中就是检查该值。进行下一步

3,  这一步开始进入beforeInvocation()方法,该执行完成后返回InterceptorStatusToken 。进入该方法后首先objectDefinitionSource检查该Request请求的URL是不是受保护的URL(filterDefinitionSource是什么东西,先不讲),如果检查到该URL是受保护的,并且filterInvocationInterceptor不允许匿名用户访问该资源(rejectPublicInvocations属性),抛出IllegalArgumentException异常,因为你的配置不正确,如果该URL是受保护的,进行下一步,否则跳到

4,  调用SecurityContextHolder.getContext().getAuthentication()来检查当前用户是否已经登陆(为什么用这个方法可以判断,用户登录部分讲)。如果用户没有登录,抛出AuthenticationCredentialsNotFoundException,这个异常是AuthenticationException的子类,所以抛出异常后filterInvocationInterceptor结束工作,把异常交给ExceptionTranslationFilter处理,ExceptionTranslationFilter怎么工作的,上面已经讲了,否则进行下一步,

5,  该步骤通常不是必须的,使用SecurityContextHolder.getContext().getAuthentication().isAuthenticated()检查该Authentication是否是已经通过认证的 Filter alwaysReauthenticate属性,其中任何一个值为true,都将是用authenticationManager. authenticate()方法重新验证该Authentication,如果该Authentication没有通过验证,同样抛出AuthenticationExceptionfilterInvocationInterceptor结束工作,异常交由ExceptionTranslationFilter处理,否则进行下一步(注:通常如果认为每个Request都不是安全的,才会进行这一步)

6,  使用accessDecisionManager. decide()方法检查该用户是否有访问该URL的权限,如果没有抛出AccessDeniedExceptionFilter结束工作,异常交由ExceptionTranslationFilter处理,否则filterInvocationInterceptor对用户是否有权限访问该URL的认证工作结束。进入附加功能第7

7,  附加功能。使用RunAsManager执行runAs功能,如果配置了RunAsManager的话。头有点晕,还没有搞明白,暂且把RunAsManager的说明放一段下面。

* This is provided so that systems with two layers of objects can be

 * established. One layer is public facing and has normal secure methods with

 * the granted authorities expected to be held by external callers. The other

 * layer is private, and is only expected to be called by objects within the

 * public facing layer. The objects in this private layer still need security

 * (otherwise they would be public methods) and they also need security in

 * such a manner that prevents them being called directly by external callers.

 * The objects in the private layer would be configured to require granted

 * authorities never granted to external callers. The

 * <code>RunAsManager</code> interface provides a mechanism to elevate

 * security in this manner.

这一步执行成功后返回InterceptorStatusToken

8,  filterInvocationInterceptor调用chain.doFilter()把控制权交给其他Filter,其他Filter的工作结束后把控制权交回filterInvocationInterceptorfilterInvocationInterceptor执行afterInvocation()方法,在afterInvocation里,首先filterInvocationInterceptor根据返回的InterceptorStatusToken结果检查是否将用户的Authentication返回到原始值,因为RunAsManager可能修改了用户的Authentication,接下来如果配置AfterInvocationManager的话,使用AfterInvocationManager做最后的检查,因为其他的Filter里面可能修改了用户的Authentication

9,  至此,filterInvocationInterceptor执行完毕

filterInvocationInterceptor的部分代码

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

        throws IOException, ServletException {

        FilterInvocation fi = new FilterInvocation(request, response, chain);

        invoke(fi);

    }

  public void invoke(FilterInvocation fi) throws IOException, ServletException {

        if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)

            && observeOncePerRequest) {

            // filter already applied to this request and user wants us to observce

            // once-per-request handling, so don't re-do security checking

            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());

        } else {

            // first time this request being called, so perform security checking

            if (fi.getRequest() != null) {

                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);

            }

 

            InterceptorStatusToken token = super.beforeInvocation(fi);

 

            try {

                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());

            } finally {

                super.afterInvocation(token, null);

            }

        }

    }

 

呵呵,不知道大家有没有看晕了,反正我已经晕了,看了下表,已经是凌晨4点了

今天就讲到这了,上面只是讲了org.acegisecurity.intercept.web.FilterSecurityInterceptor的流程,FilterSecurityInterceptor 中的objectDefinitionSource怎么检查该Request请求的URL是不是受保护的URLauthenticationManager. authenticate()方法怎么检查用户的Authentication的,accessDecisionManager. decide()怎么检查用户是否有访问该URL的权限的,这三个问题留作下回分析。

还有用户登录处理上,还需要再写一些

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值