Acegi的研究

来源:互联网 发布:基于单片机led旋转显示 编辑:程序博客网 时间:2024/05/21 09:04

 原文网址:http://rmn190.iteye.com/category/29638

Acegi(一):酝酿将近一年后的提高

过年后回到公司,工作上的事还没有正式开始, 于是就又开始学习Acegi这个框架了.

说起这个框架,真是有太多话想说:这个框架是除SSH外我自己独立学习分析的第一个框架, 从这方面第一篇博客 到现在快一年了, 这么长时间里断断续续在用, 也偶尔心血来潮地以debug方式"研究"一番, 但收获很小。

大概是酝酿够了吧,今天有了个突破性地进展.  这些天再看那些类/接口也亲切多了,它们之间怎么配合也清晰了起来. 鉴于对这个框架知识上的积累和分析研究过程中的一些经验收获,我想着写一系列的博客来加深理解、写出自己的理解与大家交流以得到大家的指正,若是能给别人带来些帮助我也就甚感欣慰了。

这一系列博客大致从两个方面来入手:
    1, Acegi自身知识休系,在实际项目应用中的一些扩展,与其它一些技术的配合。 
    2,在研究这个框架过程中的一些坎坷和收获,以希望能对自己对其它框架的研究有所帮助。

Acegi(二): 迷茫后,写还是不写?

 昨天写了篇Acegi方面的开头,由于需要些东西,上网搜时, 却"情理之中而又意料之外"地发现了05年有人写的Acegi方面的东西。别人写的很详细,从接触Acegi开始困扰很久的时间人家很通畅地表达了出来. 我一下有些蒙了: 别人这么早就写了,写的还不错,而我现在才开始写也不赶趟了,而且还有被人家取笑的可能. 这怎么办?写还是不写. 怎么写?

     犹豫中翻看别人写的东西,发现他们大多是专注这个Acegi怎么用,以及怎么扩展, 而对其框架内部设计及实现细节很少专注.可这个关注能否成为自己的买点呢? 虽说这是自己研究开源框架的一个方向.

     若不写呢,自己研究这个框架的投入就没有实现最大化. 可能跟一些人不同, 我很是想踏踏实实地研究这个框架的, 研究过程中肯定是会有不少收获的, 这些收获若没能及时记下来,即使当时的收获是多么的让自己感激涕零,时间一长那种兴奋劲也会被风干的. 不为有什么卖点,就算是为了满足下自己否则会空虚麻木的大脑也把把跟Acegi随行的所见所得所感记下来. 可以有一个大胆一点的预料, 随着这些触动心思技术刺激的增多,自己对技术的嗅觉也应该会有大幅度的提升.

     写到这里,渐渐发觉,这篇博客应该算是所写博客里条理与文采有明显提高的一个,一年多的时间里写的博客也不少了, 但每每回头看时,其条理之混乱,语言之干瘪连自己都看不过去了。 这样看来, 随着对Acegi学习 体会的加深,自己的文字表达也应该会有一个不小的提高,我有理由相信那些情真意切的思想在Acegi优雅设计的感召下定会有条不紊地呈现出来。

希望大家放手拍砖!

Acegi(三): Acegi? Who are you?

Acegi是个什么?
        是一个基于Spring的开源框架,用来做安全控制. 基于Spring? 能不能在不用Spring的情况下配置Acegi呢?Acegi官网上有一个链接:Use  Withou Spring ,看来是可以的,自己还没有动手做过. 实现安全控制? 原理是什么? Servlet的Filter和AOP机制: 利用Filter, Acegi实现了基于Web实用的URI保护; 通过AOP,Acegi实现了对象访问方法的保护;利用ACL,Acegi实现了对prototype类型的Object进行过滤和保护。

Acegi的核心概念是什么?类似实现又哪些?
        Acegi本质上是一种security solution的实现.
        而一般而言,security solution都有哪些基本概念呢? 基本概念有两个:认证(Authentication )与授权(Authorization). 随便说一下, 我一直认为这两个词翻译的很不直观, 若光看这两个字,觉得很是摸不着头脑. 我们来看下Acegi官方文档的解释。 authentication is the process of establishing a principal is who they claim to be. A "principal" generally means a user, device or some other system which can perform an action in your application. 这里边有个核心词principal,通俗的理解是要在咱们所写系统中操作的东东,这个东东可以是人(当然这是最见的),可以是某一个别的什么设备(机器人啥的,呵呵),还可以是别的系统。有了这个,authentication的理解就顺理成章了: authentication 就是看看要操作咱们系统的东东是不是真的,这在实际应用中大多是通过密码验证来实现的,也就是对暗号,如暗号对上了,你就是真的了。 而Authorization呢? "Authorization" refers to the process of deciding whether a principal is allowed to perform an action in your application. 上面我们说到一个principal要操作咱们的系统,假若这个principal是真的, 那接下来很自然的就是看Ta有没有权限来做Ta想要的操作了. 这也正是Authorization要干的事儿.
      有了这两个根本的概念,接下的所有事理解起来就容易了. 这个接下来的事就是Acegi怎么设计实现. 要是自己来设计实现那还真是个问题, 不过看别人的还不至于那么难.
     
      对这两个最核心的概念说的有点多了, 下面看下Acegi的类似实现,也就是Acegi的兄弟们,以及跟他们相比,Acegi自身又有什么优势.
    
      要说Acegi的兄弟, 人们最容易想起来的还是JAAS.大家知道, 这是Java EE里标准实现, 那是由于JAAS的什么缺点致使小弟Acegi把风头给抢了呢? JAAS配置繁琐、提供的安全访问机制粒度不够和JAAS在WAR和EAR层面上没有移植性这些缺点是江湖流传很广的说法,当然本人也没机会做这方面的详细比较,姑且就人云亦云地接受这种说法吧。

      另外一个兄弟我也是这些天刚认识, 一个叫Crowd的框架, 由于一点也没有接触过,也就不刚妄下判断。看一些文章知道这个Crowd可以跟Acegi结合起来用,想必是它弥补了些Acegi的缺点吧。

核心部件都有哪些?
    按理解的深度,Acegi有两个层次上的核心部件。第一层的核心部件是以配置的角度来看的, 也就是说我们要想配置一个Acegi保护的系统都要涉及到哪些. 第二层是实现的核心, 也就是说它们整个Acegi就玩不转了. 这个第二层跟配置不一样, 第一层所说的配置仅仅是应用的一方面,Acegi可以有多种实用并加自己的扩展, 而所有所有的这一切都是围绕第二层的核心部件组建的.
    第一层的部件放到下一篇结合实例来说明,这里重点说下实现层面上的核心部件,它们依次是:SecurityContextHolder ,SecurityContext ,HttpSessionContextIntegrationFilter(这个虽放到这里,它在这里也可以说是代表了它们一个群体) ,Authentication ,GrantedAuthority ,UserDetails ,UserDetailsService . 一共七个, 我们在这里称之为"七剑". 写到这里,脑子里隐隐约约感觉到老子的道家思想了. 你看, Acegi的核心思想是"保护", 围绕这个核心思想分出两个概念Authentication 和Authorization, 具体到怎么来实现这个两个概念就有了我们所说的"七剑", 再往下就是针对不同场合应用的支撑,而再往下就是现在千千万万项目中用到的Acegi配置及扩展.这一个演变系列是不是有点像"道生一,一生二,二生三,三生万物"呢?
    对这"七剑"的理解很重要, 只有理解了它们,才能达到万变不离其宗地灵活配置.

    这"七剑"在这也先不展开讨论, 等在体会了实例及分析了"第一层核心部件"后再回过头看吧, 那样理解上更自然些.

    絮絮叨叨写了不少, 回头总结下,也为下篇写下开头: 这篇以静态的角度了Acegi的核心概念及其核心部件, 下一篇中将动手配置一个小例子,来个真切感受下Web实用中围绕"七剑"都有哪些部件可用。

 

Acegi(四):Acegi初体验及初解剖

上篇博客中,我们以静态地角度对Acegi的核心概念及其实现上的核心部件进行了谈讨,本这篇中,我们将结合一个Web程序来体会并介绍下在Web项目中配置Acegi里都有哪些关键点。

    我们知道Acegi可以多种实用场景,但现在用的最多的还是在Web项目中,这里不再介绍配置的具体步骤,直接将一个配置好的Web项目传了上来(只做了那些必要的配置),见附件,不出意外的话,这个例子可以在Eclipse里直接运行,Eclipse版本为3.4.0。


    下面的介绍都是以这个Web例子为介绍的, 大家最好先把例子下载下来体会下, 先大致地看下web.xml和application_acegi_context.xml这两个文件的内容, 以对Acegi的配置有个直观的印象.

   
咱们从web.xml文件开始.

    我们看到这个文件特简单, 一共就context-param,filter,filter-mapping,listener,welcome-file-list五个配置元素. 先从最熟悉的配置元素逐个排除.第一个welcome- file-list不必多说, 排除掉. 再看listener,由于Acegi是建立在Spring框架上的, 这里通过listener来初始化Spring的Context是情理之中的, 也就不必再多考虑. 有了listener的理解基础,我们再看context-param也就明白了,这个元素把Acegi的配置文件applicationContext-acegi-security.xml提供给Spring.下面进入Acegi在Web应用中的关键部件Filter, 我们知道Acegi在Web应用方面的一个基本原理就是Servlet的Filter,这里配置中Filter实现类是FilterToBeanProxy, 它有一个配置参数targetClass,其值为FilterChainProxy, 这个Filter的配置很简单, 大多数 情况下我们可以直接从现有的例子中拷来就行, 很少需要做什么配置上的改动.不过,若想把Acegi工作原理及其工作细节搞明白的话,这个FilterToBeanProxy到FilterChainProxy的转换是不能躲过的关键点,再进一步想, 看人家是怎么设计实现的对自己的"钱途"也是大有帮助的吧?从我自身的体会上来看,FilterToBeanProxy和FilterChainProxy两个类在设计上很有"嚼头",我想着在另外的博客中单独来看它们的实现与给自己的启发, 这里我们先有这样的一个概念:FilterToBeanProxy在doFilter时会从Spring的Context里get出FilterChainProxy的实例,FilterChainProxy自身也是一个Filter的实现类,它在applicationContext-acegi-security.xml文件中初始化, 这样,FilterToBeanProxy就把经过它的所有请求转给了FilterChainProxy来处理, 这样就上了Acegi的道儿,也就是拦截下来的请求在真正做事前需得到Acegi的许可.

    下面我们来看
applicationContext-acegi-security.xml文件,上面的分析我们得出, FilterToBeanProxy把拦截下的请求交给了FilterChainProxy来处理, 这个处理就是Acegi的核心概念Authentication和Authorization的实现.
    在这个文件中我们首先看到如下的配置信息:
    
<bean id="filterChainProxy"
        class="org.acegisecurity.util.FilterChainProxy">
        <property name="filterInvocationDefinitionSource">
            <value>
                CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
                PATTERN_TYPE_APACHE_ANT
                /login.jsp=#NONE#
                /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
            </value>
        </property>
    </bean>   
    在上面介绍
web.xml文件内容时,我们提到FilterToBeanProxy从Spring的Contextget出FilterToBeanProxy的实例,上面的配置信息让Spring来初始化FilterChainProxy类,随便说下, 这里FilterChainProxy实现的id没有用到,再进一步想,Spring可以不用id再用类名就可以得到其实例.回到正题,FilterChainProxy类有一个名为filterInvocationDefinitionSource的属性, 通过这个属性, Acegi把拦截下来的请求再一次转移.
    我们先看
filterInvocationDefinitionSource属性里又都有些什么?从上面我们看到, value里是一些字符串描述的信息,这里有一个与当前Acegi不相干的思考:setFilterInvocationDefinitionSource方法的参数类型是FilterInvocationDefinitionSource,Spring是怎么把一个String类型的信息自动转成所需要的FilterInvocationDefinitionSource类型的呢?这个问题在这里先不思考.回到正题, 属性filterInvocationDefinitionSource里配置的关键信息是"/**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor", 从下面的配置可以看出,httpSessionContextIntegrationFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor 几个Bean都是Filter接口的实现类,这几个Filter的实现类再调用上一篇博客里提到的"七剑" 实现Authentication和Authoriaztion的"理想".

    那"七剑"是怎么与"
httpSessionContextIntegrationFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor"们配置的的呢?且看下回.

Acegi源码研究(五):七剑下天山

在Acegi初体验及初解剖(http://rmn190.iteye.com/blog/332711)里, 通过对web.xml和applicationContext-acegi-security.xml的跟踪,我们得出被Acegi拦截下的请求最终交到了filterInvocationDefinitionSource配置下的几个Filter的实现类来处理. 它们是怎么处理这个请求的呢? 在Acegi(三): Acegi? Who are you? ,我们听说江湖中有"七剑", 但这么久了"七剑"怎么还 没露面呢? 这篇博客中我们将看到下天山的"七剑".

    首先我看下都有哪些Bean参于了Acegi的保卫工作, 如下图所示:

    

上面是静态的定义, 再看下图的动态调用图:

 

 

这里结合着上图,我们跟着浏览器发出的请求走一遍.
     Step1: 对上就上图中数字1,这里浏览器发出一个请求.
     Step2: Web服务器,我们这里的Tomcat接受到Step1发来的HTTP请求, 把里面的信息抽取出来,组装成一个Request 对象, 同时Tomcat也生成了一个Response对象,这样接下的Request穿过的Filter都有机会来修改这个Response对象了, 也正是Acegi抓住了这个机会利用Filter来改变Response里的值达到保护我们系统的作用.这里Request到达图中的"Filter chain proxy", 我们还记得web.xml中配置的"FilterToBeanProxy"及基配置参数FilterChainProxy(targetClass的值), 再看Bean图中的filterChainProxy,这里我们动态地感觉到这个filterChainProxy的存在了.通过配置中的"/**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor"一句我们告诉filterChainProxy 当Request来时,都有哪些Filter处理这个Request.
     Step3/Step4: filterChainProxy调用filterChain中的第一个Filter,也就是httpSessionContextIntegrationFilter . 这个httpSessionContextIntegrationFilterHttpSessionContextIntegrationFilter类的实例, 这个类正是"七剑"之一. 从它的名字上,我们也隐隐约约地感觉到了点什么:把Context integrate 到HttpSession中? 对的, 在HttpSessionContextIntegrationFilter的doFilter方法里Acegi从HttpSession中获得SecurityContext 对象(或没有的话新建个), 随后再把这个SecurityContext存放到SecurityContextHolder中, 很自然嘛, SecurityContextHolder 就是用来放SecurityContext的. 做了这些处理后,httpSessionContextIntegrationFilter通过chain.doFilter方法将执行权交给下一filter.

    我们例子中下一个filter就是
authenticationProcessingFilter,它是AuthenticationProcessingFilter的实例,看它名字里有Authentication跟"七剑"中的Authentication有关系? 不错,authenticationProcessingFilter类中有一个requiresAuthentication方法, 顾名思义,它是看当前的Request是否是用来做验证的,也就是说是否是登录的. 这就用到了Bean配置authenticationProcessingFilter里属性, 看Acegi的源码可以得到验证. 接下来, 在方法attemptAuthentication中Acegi从Request中取出username和password组建一个Authentication 对象, 再由配置中的authenticationManager来加以验证,若"暗号"没对上的话, 就有一个AuthenticationException异常抛出,authenticationProcessingFilter catch住这个AuthenticationException,再通过sendRedirect方法在浏览器中指向authenticationFailureUrl的值" /login.jsp?login_error=1 ".若"暗号"对上了,将通过方法 successfulAuthentication重定向到 defaultTargetUrl指定的页面, 同时Acegi把对上"暗号"的Authentication对象通过方法SecurityContextHolder.getContext().setAuthentication存放到SecurityContext中,以备后用.

     Step5/Step6: Acegi将Request(当然还有Response)交给下一个Fillter, 我们例子中是
filterInvocationInterceptor(实际上交给了exceptionTranslationFilter,这里我们为了讨论的方便,假定交给了filterInvocationInterceptor),这个filter再从SecurityContextHolder中取出SecurityContext,再取出里面的已经对上"暗号"的Authentication对象(此对象在本例中实际上是封装了users.properties里这样的"james=tom@1231,ROLE_TECHNICIAN"键值对), 再把这个 Authentication对象交给当前bean中配置的accessDecisionManager属性以决定Authentication是否能访问它想访问的资源.经accessDecisionManager"研究"后发现可以访问,此时Acegi即将Request/Response交给我们系统里配置的相应方法(如Struts里配置的某一method).若"研究"时发现没权限,则以异常的方式交给上面提到的exceptionTranslationFilter,这个filter再重定向到loginFormUrl指定的URL上.

     到这步骤已走完了, 那怎么没看到"七剑"中的另三剑呢? 它们是
GrantedAuthority ,UserDetails ,UserDetailsService . 实际上它们在filterInvocationInterceptor中用到了, 不过当时为了讨论的方便,并没有写出来. 是这样的, 在决定一个Authentication是否可以访问它想要的资源时,accessDecisionManager会从Authentication里 调用getAuthorities得到它的GrantedAuthority [] , 再将它们与通过UserDetailsService 得到的UserDetails对象里方法返回的GrantedAuthority[]一一比较, 若在比较过程中发现有个对应上了, 就表示有权访问,否则即无权. 这里所论述的过程正是上面accessDecisionManager所做的"研究".

       好了,至此, 我们通过跟踪请求的来龙去脉把
applicationContext-acegi-security.xml配置中的Filter走了一遍,更重要的是走filter时,我们与Acegi江湖里的鼎鼎大名的"七剑"有了短暂但美好的接触.

Acegi源码研究(六): Acegi编码/设计碎得

前些天通过一个Acegi的Web实例,我们感受了下受保护的好处,也通过一步步的跟踪,感觉到Acegi里"七剑"的存在.本来是想着继续再往下做扩展的,后来一想还是回过头来整理下研究Acegi过程中的碎得吧,毕竟这样的碎得写起轻松些,我也稍稍放松下.   

 

      信马由缰地溜达了下,又想起了当时一开始看Acegi源码时的如下问题:

 

      1, FilterToBeanProxy --> FilterChainProxy, 一个VirtualFilterChain来模仿Web 容器中的FilterChain, 内容类   (自己写代码时也有意识地用protected, private了).
              方法的实现与调用 --> 代码块的重用.
              FilterChainProxy类里filterInvocationDefinitionSource属性从String到相应类的自动转换.

2, Filter调用与方法栈的吻合,画一个序列图来表示.

3, SecurityContextHolder的多种实现, 为什么要有多种实现,每一种实现又有什么特点?
    HttpSession中放没放SecurityContext? 在SecurityContextHolder里也有放. 这种有问题了: 如何让它们俩同步? 删了一个后又怎样?

4, 另处一些常用的配置:
     logoutFilter, securityContextHolderAwareRequestFilter,anonymousProcessingFilter

    出于什么考虑提出这样的概念?

5, 这一段的作用:
    
 if (request.getAttribute(FILTER_APPLIED) != null) {
            // ensure that filter is only applied once per request
            chain.doFilter(request, response);

            return;
     }

    要防止在
filterInvocationDefinitionSource中配置了多个httpSessionContextIntegrationFilter,httpSessionContextIntegrationFilter总是在第一个的. 多配置了也没用, 若不加有什么害处不?
6, 为什么如下这种方式来取HttpSession?
    
     try {
            httpSession = request.getSession(forceEagerSessionCreation);
        }
        catch (IllegalStateException ignored) {
        }

         若getSesssion时传一个False会是什么样的?一个HttpSession一般什么时机创建? 现在越来越不考虑这些问题了.
7, 为什么要把response再包装下?
OnRedirectUpdateSessionResponseWrapper
8, 为什么每次都要有 SecurityContextHolder.clearContext();这不挺浪费的吗? 防止Authentication里的用户名/密码/权限在用户操作时有改? 作为一个通用的安全框架应该支持这个功能的, 把权限控制存放到数据库中太常见了.
9, 如下代码段的的注意点:
         
if (cloneFromHttpSession) {
            Assert.isInstanceOf(Cloneable.class, contextFromSessionObject,
                    "Context must implement Clonable and provide a Object.clone() method");
            try {
                Method m = contextFromSessionObject.getClass().getMethod("clone", new Class[]{});
                if (!m.isAccessible()) {
                    m.setAccessible(true);
                }
                contextFromSessionObject = m.invoke(contextFromSessionObject, new Object[]{});
            }
            catch (Exception ex) {
                ReflectionUtils.handleReflectionException(ex);
            }
        }

         9.1 为什么要
cloneFromHttpSession? 联想到Hibernate的PersistentContext, 它会自动来做dirty check的, 若不clone一个,直接把一个原来的return给client的话,会影响dirty check的速度.
         9.2 自己写例子试试clone接口的实现与深度拷贝.
10, 从方法
readSecurityContextFromSession看框架级代码的书写:
    10.1 考虑到多种可能层层设防. 从而确保return一个
non-null,且为实现SecurityContext接口的HttpSession里以"ACEGI_SECURITY_CONTEXT_KEY"存放的对象, 不要被"狸猫换太子"了.
    10.2, log的书写.

 

 

Acegi(七): LogoutFilter配置及几个问题

这些天有点懒散, 想的也得振作一下吧. 从最好容易的部分下手, 这样想到Acegi.前段时间的博客里写了acegi的最简单配置, 以及围绕这些简单配置的一点点源码层面的钻研. 而实际项目中的配置光这些是不够的, 比如说用户信息及权限还有系统的访问权限都是要放到数据库里的, 这样在做用户登录与权限认证时就要hit数据库了, 这方面的配置又怎样呢?

    不过这篇博客中打算写与数据库扩展相关的话题, 而是先从较为简单的几个Filter开刀.

    先看LogoutFilter.

    配置很简单.
    1, 先配置一个id为
logoutFilter的Bean, 如下所示:
         
<bean id="logoutFilter"        class="org.acegisecurity.ui.logout.LogoutFilter">
        <constructor-arg value="/login.jsp" />  
        <constructor-arg>
            <list>
                <bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" />
            </list>
        </constructor-arg>
    </bean>

    2, 把配置好的
logoutFilter加到 filterInvocationDefinitionSource中去, 即改为"/**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor".
    3, 在JSP中适当的位置加上
<a href="/myOwnAcegi/j_acegi_logout">退出</a>

    几点说明, 这个的配置很简单的也多少要说有:
    1,
<constructor-arg value="/login.jsp" />的作用, 用来告诉Acegi当系统退出后跳转到的链接.
    2,
LogoutFilter构造方法中的第二个参数的作用, 告诉Acegi系统退出后, 都有哪些跟退出相关的action要做.
    3,
j_acegi_logout的链接, 这个没什么, 跟登录时的" j_acegi_security_check"类似, 都是Acegi自带的, 大多情况下也没什么必要修改.

    从追着源码看了它的实现,没什么特殊的, 不过有如下几个不太相关的问题:
    1, 有没有必要对这个退出单独配置一个访问时的filterChain, 即另加一个"
/j_acegi_logout=httpSessionContextIntegrationFilter,logoutFilter "?如此配置的出发点是这样的,上面配置2种的访问链接在 logoutFilter之后还有三个filter要通过, 执行时间上有些浪费了, 通过这样配置后, 当用户点"退出"时acegi处理完logoutFilter后就没什么filter要处理了.
         再者说, 也不至于让每一个请求都通过一次
logoutFilter,虽说logoutFilter里有这样一个requiresLogout判断.
         这样的配置里能不能不加
httpSessionContextIntegrationFilter? 为什么要加呢? 不是很清楚.

         后边看到"
sendRedirect(httpRequest, httpResponse, logoutSuccessUrl);"这样的语句, 原来Acegi通过它就可以让请求链接跳转到
    2,
LogoutFilter的构造方法有两个参数,String类型的 logoutSuccessUrl和LogoutHandler[]类型的handlers. 第一个参数没啥可说的, 第一个参数, 人家要的类型是一个数组, 而Spring里配置时是通过<list>标签来的, Spring内部是怎么转化的? 这是个大话题, 先不再下钻了.

 

Acegi(八): securityContextHolderAwareRequestFilter

  上篇 中我们说了下 LogoutFilter的配置, 这篇里接着看另一个常见的配置SecurityContextHolderAwareRequestFilter. 下面先看怎么把它配置到Acegi里.

    应该说对
securityContextHolderAwareRequestFilter 的配置要比LogoutFilter的更简单些. 有以下两步:
         Step1: 定义一个名为securityContextHolderAwareRequestFilter的bean, 如下所示:

             

Xml代码

 

             呵呵, 简单地我就有些不好意思往这写了. 就这么一个干巴巴的类, 别什么属性要配置的.


         Step2: 把定义好的 securityContextHolderAwareRequestFilter放到 filterInvocationDefinitionSource里, 如下所示:
               /**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter ,exceptionTranslationFilter,filterInvocationInterceptor
            


    上面是对它的配置, 这个配置有些干巴巴的, 那它究竟有什么用呢? 我们接着往下看, 那将是一个丰富多彩的世界.

   
SecurityContextHolderAwareRequestFilter类名怎么这么长? 也看不出什么有用的信息啊. 不过,看文档得知SecurityContextHolderAwareRequestFilter的作用是:将传来的request用一个HttpServletRequestWrapper封装下再传给 filterChain. 我们不禁要问? 为什么要这么做呢? 也就是说这么做有什么好处呢?

    要弄通这个问题, 我们得看这个Filter对传的
request做了些什么手脚, 也就是说是以什么样的一个HttpServletRequestWrapper来封装了传来的request. 在Acegi中我们发现就有两个HttpServletRequestWrapper的子类: SecurityContextHolderAwareRequestWrapper和SavedRequestAwareWrapper. 它们又有什么样的特质呢? 发现SavedRequestAwareWrapper是SecurityContextHolderAwareRequestWrapper的子类, 为了钻研的方便, 我们先看父类SecurityContextHolderAwareRequestWrapper , 所谓有其父必有其子嘛.

    先解剖下这个类, 这个类只有一个属性authenticationTrustResolver(其实它没什么多大的用处, 待会再细说), 覆盖了HttpServletRequestWrapper的三个方法:getRemoteUser, getUserPrincipal,isUserInRole.
    (这里先顺便说下, 我以前一直没注意到上面的这三个方法是接口里定义的, 看来Servlet规范里早就考虑到Security方面的问题了; 另一个没注意到的是, 原来
Principal是Java标准类security包下的定义的接口, 而Acegi里重要的接口Authentication是继承自Principal )
    于是问题就转为为什么要单独再覆盖这三个方法了.那得看这三个方法又都干了什么. 看方法的实现, 发现它们实际上是做了同样的事, 即通过
SecurityContextHolder.getContext().getAuthentication()拿到Authentication后再get出String类型的RemoteUser,或 Principal类型信息, 或看这个Authentication是否有指定的权限. 也就是说它们把Acegi里自身的验证信息放到Servlet规范里体现的对Security的支持!

    那为什么非要用Acegi的认证信息来替换或填充Servlet规范中
Security信息呢? 我想有这么几点好处:
         1, 替换掉Web容器自身的认证信息. 拿Tomca为例, 我们知道在Tomcat中可以配置管理权限的, 若Tomcat自身配置了权限管理, 而Acegi不做什么处理的话, 程序员从
HttpServletRequest里通过调用上面三种方法得到的Security方面的信息就不是通过Acegi配置的了, 那样岂不就乱套了?Acegi自身的Security作用也就给架空了, 这还得了? 于是Acegi就有果断地通过 SecurityContextHolderAwareRequestWrapper把漏洞给补上了.
         2, 还有一个可能的好处, 那就是通过这种覆盖处理, 程序员可以在Action或JSP中方便地得到当前登录用户的信息, 而不必在系统中揉合进Acegi自己的API. 项目中这样的反例太多了.

    通过上面的分析,再看类的名字
SecurityContextHolderAwareRequestFilter, 这样也就好理解了, 真是顾名思义! 这个名字表达的意思是说, 这个Filter是让Request 来aware下SecurityContextHolder里的信息, 这个让它Aware正是通过覆盖三个方法实现的.

    好了, 这篇就写到这里, 下一篇中我们将看
SecurityContextHolderAwareRequestFilter子类SavedRequestAwareWrapper又加了些什么功能. Until the next.

Acegi(九): 子类SavedRequestAwareWrapper

  上篇 中我们对 securityContextHolderAwareRequestFilter的丰富多彩有了个体验, 最后对这个类的名字也做了一个望文生义的解释. 本篇中我们将接着看上篇提到的子类,即SavedRequestAwareWrapper. 这个类在父亲的基业上又有什么新的突破呢? 这得从它的贤内助说起, 即这个子类的属性savedRequest . 呵呵, 这也正是组合的好处.

    我们看到SavedRequestAwareWrapper类里所有方法的实现都是基于这个贤内助的帮助. 拿我们熟悉的getCookies方法来说, 在执行这个方法时, 先要看贤内助的脸色: 如果savedRequest没什么话说,才能执行父亲所传授的,即父类的方法super.getCookies();

    刚才看天下足球的"疯狂的足球", 而上面的内容也仅是我的想像,希望它较为贴切.

     下面,我们看这个贤内助是怎么回事? Ta是何许人也呢? 读源码时发现, 这个
SavedRequest没什么背景, 没有继承, 只是实现了Serializable接口. 那有什么用? 单独地看这个类是不行了, 不过发现这么一条语句:SavedRequest saved = (SavedRequest) session.getAttribute(AbstractProcessingFilter.ACEGI_SAVED_REQUEST_KEY); 这里有一个get,那也应该有"人"set过, 难道说通往发现之路的入口?

    经过一段时间的侦查, 终于发现那别有洞天! 在描述这个洞天前, 先看这么一个实际例子.

    在CSDN网站里下载东西, 刚开始时我们先找, 找到一个一看评价不错, 那就下载吧, 可点下载按钮时,CSDN网站把我们引到登录页面, 噢, 原来还没登录呢. 很麻利地填写了登录信息后,点确定, 这时CSDN直接就把刚才要下载的东西拿出来了. 8错! 登录完后, 不必再从头找那个链接了. 可这是"8错"是怎么实现的呢?

    我们用Acegi里的SavedRequest来描述下这个功能的实现, 当然CSDN不一定是用Acegi来实现的, 这里只是借用下那个情景. 在点"下载"按钮前, CSDN是允许我们浏览的, 也就是我们有匿名浏览的权限, 可当下载时, CSDN发现这个人没有下载的权限, 得让Ta登录. 于是, CSDN里的安全机制让页面跳转到登录界面, 同时,为了用户的使用方便, 把用户刚才想下载的那个链接也记了下来, 放到了Session中, 而我们说的
SavedRequest正是干这个用的. 用户登录后, CSDN的安全机制再从Session中取出刚才那个想下载的链接, 于是下载顺利进行了.

    下面结合前面介绍的Acegi概念,把这个过程再描述一遍. 刚才开始用户找东西时用浏览的权限, 当Ta想下载时, Acegi的
FilterSecurityInterceptor在执行beforeInvocation方法时发现当前用户没有相应的权限, 于是FilterSecurityInterceptor抛出一个accessDeniedException异常, 这个异常在ExceptionTranslationFilter里给catch住了, 随后的handleException时, 由sendStartAuthentication方法负责 new了一个SavedRequest对象, 这个对象里正是装了刚才那个链接,随后把这个SavedRequest对象放进了Session . 于是用户登录, 登录妥当后, AbstractProcessingFilter里的successfulAuthentication方法出面通过determineTargetUrl方法从session中取出前面放到session中的SavedRequest对象, 最后再交由sendRedirect方法, 把用户想要的东东呈现在了眼前.

    说了半天了,
SavedRequest抢去了大多数的风头, 不过也好, 把这个 SavedRequest研究透了后, SavedRequestAwareWrapper的功能就不攻自破了. 也就是说从历史地角度看清了SavedRequest对象的来龙去脉,SavedRequestAwareWrapper对象的现在问题也就应刃而解了. 

 

Acegi(十): securityContextHolderAwareRequestFilter结

在Acegi(八) 和Acegi(九) 里, 我们对securityContextHolderAwareRequestFilter有了个较为全面的剖析.在这篇做个小结, 也把一些漏落补上.
    
    在上两篇里,我们实际上是介绍了三个类: SecurityContextHolderAwareRequestFilter、 SecurityContextHolderAwareRequestWrapper和SavedRequestAwareWrapper(及由些想到的 SavedRequest). 现在我们回过头再看时,对它们之间也有了个更清醒的了认识: Acegi通过这个SecurityContextHolderAwareRequestFilter把Request给Wrap下, 而这个wrapper就是SecurityContextHolderAwareRequestWrapper或 SavedRequestAwareWrapper. SavedRequestAwareWrapper继承自SecurityContextHolderAwareRequestWrapper类.

    我们再看在Acegi(八)配置,          

 

Xml代码

 


    通过这个配置, securityContextHolderAwareRequestFilter实际上用SavedRequestAwareWrapper来包装Acegi拦截下来的Request. 这是怎么回事? 配置中没体现出来呀. 看源码,发现原来SavedRequestAwareWrapper是通过"Class wrapperClass = SavedRequestAwareWrapper.class;"这个属性写死的. 这样默认情况下,securityContextHolderAwareRequestFilter就可以直接用了. 那怎么换呢? 有一个方法setWrapperClass,通过它可以替换究竟用哪个wrapper了. (这里又有一个问题了, 在Spring的配置文件中,用property标签设置wrapperClass时,value里应该是一个String类型的,那怎么转成Class类型的呢?).

    再看
securityContextHolderAwareRequestFilter类的文档, 发现,我们可以来自己的实现类来设置wrapperClass. 那自已定制的类又有什么特殊的要求呢?有, 自己定制的类就是要有一个公共的构造方法, 这个公共方法得接受两个参数:HttpServletRequest和PortResolver. 当然定制的类是要继承 HttpServletRequestWrapper类或实现HttpServletRequest接口.

    -------------------------------
    至此, 对
securityContextHolderAwareRequestFilter介绍告一段落了. 下面说下在此过程中发现的一个Filter,即HttpRequestIntegrationFilter. 看名字好像能看出点什么来, 文档介绍是这样的:"Populates SecurityContext with the Authentication obtained from the container's HttpServletRequest.getUserPrincipal()". 也就是说通过这个filter把Web(或应用服务器)容器里传来的Security信息传给Acegi放到 SecurityContext中.
    通过这个Filter, Acegi可以跟Java里的标准JAAS结合,或像Tomcat自身的Security管理信息? 还没见过, 以后留意.

 

Acegi(十一): 借鉴Acegi的Exception的异常处理

今天改项目中的一个问题时, 不由自住地想到了Acegi异常处理. 现在相比项目中的异常处理后, 很是感叹Acegi对异常的处理呀.
    先大致回忆下Acegi里的异常处理. 这里说Acegi的异常处理是指围绕ExceptionTranslationFilter展开的, 当然别的地方也有,暂时不做考虑.
     Acegi配置中在filterInvocationInterceptor前加了一个 exceptionTranslationFilter, 这样在整个方法调用栈里, exceptionTranslationFilter的doFilter是比filterInvocationInterceptor要低一层的, 而filterInvocationInterceptor的doFilter是很有可能抛出多种异常的, 这些异常即包含Acegi自身的认证时异常和权限不够异常,也包含Serlet规范中可能抛出ServletException或 IOException. 不过filterInvocationInterceptor自身并没有处理任何异常,  不管有什么异常都采取顺其自然的方式,而且它自己还抛出了AuthenticationException和AccessDeniedException 两个异常.
    异常由谁处理呢? 交给exceptionTranslationFilter来处理, 顾名思义, 这个Filter的本职工作就是来处理异常这些麻烦事的. 看源码, 我们知道, 这个filter除了异常处理外什么事都不做. 由handleException这个方法来专门跟来自filterInvocationInterceptor的异常打交道.
    这样做有什么好处? 好处大大的, 解耦合, "冲锋陷阵"的"冲锋陷阵",做"后勤"的做"后勤", 大家各司其职,有条不紊, 这样两方都能发挥出最大作用.最终的整体作战效果也会出奇地好.

    再看项目中的一个小例子, 看如下代码:
        

Java代码

 

   这段代码是用来发邮件的, 我们看下这里的异常处理. mailEngine的sendEmail会抛出异常的, 看了下文档它的抛出的异常为MailException, 而MailException有四个子类: MailAuthenticationException,MailParseException,MailPreparationException, MailSendException.而这里都给catch住了, 只把异常的message以String类型返回, 这样sendAlertMessage方法的调用者要想知道是邮件发送失败的原因就得烦些周折了. 本来MailException及其子类有很丰富的异常信息, 但由于sendAlertMessage方法的越俎代庖,画蛇添足了.
    现在假设这样一种情况, 程序要发邮件, 可收件人的地址不存在, 系统要求把报错信息反馈给终端用户, 即显示信息"由AAA发送给BBB的邮件,由于BBB的email不存在而发送失败", 这里为了更友好地显示, AAA用的是Ta的真名,而没有用Ta的邮件地址. 为了显示这样的报错信息, 调用sendAlertMessage就麻烦大了,因为这里是没有AAA的真名的.

    于是我们想,能不能像Acegi里处理异常那样用一个单独的方法来做呢?  这样的设计显然是行的通的.

-----------后记-------------
    1, 初学Java时, 知道OOP时异常处理的优势. 可项目中或自己写代码时,很少能全局地考虑异常处理, 大多情况下,发现IDE里显示有异常没处理了,想也没多想地给catch住了, 现在看来,异常处理时学问不小.
    2, 这篇也是钻研Acegi以来,除Security方面外,感觉对自己面向对象帮助最大也最明显的一次, 也让自己小小窃喜了下.
    3, 其实是昨天发现项目中sendAlertMessage方法的不妙的, 当时也想写下来, 可老是理不出思路来. 今天做完头给安排的工作后,趁着情绪高涨,再回头看这个问题时, 一下就想到Acegi时的异常处理. 于是有了这篇还算说得过去的记录. 头脑要清醒,否则要放一放了. 遇到问题,不要急也不要气馁, 办法总是有的, 静下心一遍两遍地去看,会有线索的. 呵呵, 有点想小学生作文.

    4,  不要只想着成功,更要想着失败后的信息的反馈.好像大多数情况下, 失败考虑的很少.

 

 

Acegi(十二): anonymousProcessingFilter有什么好玩的?

在这篇博客中, 我们接着看另一个Filter, anonymousProcessingFilter.

    1, 为什么要配置这个? 它能给我们带来什么好处?

    为了解决这个问题, 看了下Acegi的文档 , 但说实在的, 由于文档中用是"convenient"和"nice"这样的词来描述这个filter的好处, 我现在还感觉不到. 这里把我现在给想到的理由总结一下.

  • 为了理论上的完美. 用了这个fitler,就可以对系统中所有 的链接加权限管理了, 像login, logout和home这样的"非常规访问",也可以加一个默认的匿名访问Authentication. 像这样的"非常规访问"是可以在acegi的配置文件中写死的, 当然写死有写死的坏处. 由这个坏处,我们看下一个理由.   
  • 为了某个链接的动态设定. 可能会有这样的情况: 一个链接的访问权限刚开始时是有专门的访问权限的, 但由于业务逻辑的变更, 这个链接的权限改为匿名的了,或者说原来的匿名访问要改为具有一定权限访问了. 这时, 为了在不重启服务器情况下设定权限, 就预先配置一个anonymousProcessingFilter, 让它来处理那些默认的情况.
  • 别的我现在猜不出来了, 希望大家能补上.

    2, 怎么配置?
    虽说不是很理解, 但还是要配置的. 那怎么配置呢? 我们先看这个filter所涉及到的三个类: AnonymousProcessingFilter, AnonymousAuthenticationToken,AnonymousAuthenticationProvider. 第一个类没什么说的, 它就是这个filter的实现类, 没有它办不成事. 第二个类实际上是一个Authentication, acegi通过它来加一个默认的匿名Authentication. 第三个类实现了AuthenticationProvider接口, 有了一个匿名的Authentication, 相应地得给一个Provider, 以便在filterInvocationInterceptor检查权限时,被"卡"住. 呵呵, 看到这, 我觉得挺好笑的了: 本身是一个虚头八脑的东西, 为了"掩盖"它, 让在真实世界里行的通, 还得再给弄两个一样虚头八脑的东东陪着.
    有了一种大致的了解后, 我们看配置:
    2.1 配置anonymousProcessingFilter bean.

   
Xml代码
 
  •   这两个Property, 都是在生成AnonymousAuthenticationToken时用到.    userAttribute中的anonymous对应着Authentication(AnonymousAuthenticationToken也是一种Authentication,虽说有些虚)的principal, ROLE_ANONYMOUS对应着Authentication中的GrantedAuthority[],  key的anonymous生成AnonymousAuthenticationToken中的keyHash(通过String类的hashCode方法获得).
  • <property name="key" value="anonymous"/>与下面配置的anonymousAuthenticationProvider中的相应行对应.

    2.2  配置anonymousAuthenticationProvider

Xml代码

 

  • 配置好后, 把这个anonymousAuthenticationProvider配置到ProviderManager类下providers, 这样filterInvocationInterceptor碰到前面配置的匿名Authentication时, 才能在AccessDecisionVoter"投票"时, 由AnonymousAuthentication的自己人anonymousAuthenticationProvider"保护"着"逃过 "check.
  • 一个猜想: 这里的key跟上面anonymousProcessingFilter的key得一致, 不然在"投票"时, 没这个"暗号""自己人"也互相不认识了.想验证这个猜想, 看了下文档,发现这样的话: "The key is shared between the filter and authentication provider, so that tokens created by the former are accepted by the latter".自己的猜想不错!

   
  有了上面的配置分析, 运行机理稍看下源码就可以明白了, 这里也就不用再另写了.

-----------------------------------------
看文档时发现这么段话, 觉得很有必要记下来,虽说现在还没有切身体验:Rounding out the anonymous authentication discussion is the AuthenticationTrustResolver interface, with its corresponding AuthenticationTrustResolverImpl implementation. This interface provides an isAnonymous(Authentication) method, which allows interested classes to take into account this special type of authentication status. The ExceptionTranslationFilter uses this interface in processing AccessDeniedExceptions. If an AccessDeniedException is thrown, and the authentication is of an anonymous type, instead of throwing a 403 (forbidden) response, the filter will instead commence the AuthenticationEntryPoint so the principal can authenticate properly. This is a necessary distinction, otherwise principals would always be deemed "authenticated" and never be given an opportunity to login via form, basic, digest or some other normal authentication mechanism.

Acegi(十三): 让系统记住我

经过一段时间的总结, 对Acegi的初步研究终于到了最后一个常见Filter了,即 rememberMeProcessingFilter. 顾名思义,rememberMeProcessingFilter就是想让系统记下来当前登录的用户,不至于以后每次进系统时都要输入用户名密码.这方面的例子相信大家已有体会,我也就不必在些赘述.
      先简要地说下原理. 假定用户登录时选择了"让系统记住我"后, request进入Acegi后, Acegi会把用户名密码和相关信息记下来,再以Cookie的形式通过Response发往浏览器并存储到浏览器中,这样下次用户访问系统时,系统会通过
rememberMeProcessingFilter从浏览器传来的Cookie中取出先前的登录信息,做判断后以一个 remember-me authentication的形式放到SecurityContext中,接下来,跟anonymousProcessingFilter类似,在 filterInvocationInterceptor检查时,也由自己人投一票通过,于是就通过Acegi的盘问去做想做的操作. 这里我们也看来,其实是不系统记下来了,而浏览器以cookie的形式住了, Acegi只是围绕着cookie在合适的时候做些合适的事.
   
    有了这个对原理的简单介绍,下面我们来看怎么配置,以及这个原理又是由谁来具体实现的.

    按上面原理的顺序,以便用户选择是否让"系统记住我", 往login.jsp里加一个如下所示的一个input标签.
    
<input type="checkbox" name = "rememberMe " />让系统记住我
    注意下这里的
rememberMe,后面要用到的.

   再往以前定义好的
authenticationProcessingFilter里了加个属性,rememberMeServices,如下所示:
        <!--新加 start ,这个很重要,没有它系统就"记不住你"了-->
        <property name="rememberMeServices" ref="rememberMeServices"/>
        <!--新加 end -->
   
    通过个rememberMeServices, Acegi在得知用户登录成功后会把用户名和密码生成一个Cookie放入到response里,(这个cookie里有Acegi做的暗号,这样 acegi以后才能识得), 让它捎给浏览器保存下来,以备后用. 这步对应着上面用户第一次输入用户名密码成功登录的情景.

    接下就是用户不用登录访问系统了. 这要配置主角
rememberMeProcessingFilter了,其实现类为RememberMeProcessingFilter,这个类两个属性authenticationManager和rememberMeServices需要配置. 如下:
   
<!--新加 start-->
    <bean id="rememberMeProcessingFilter" class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="rememberMeServices" ref="rememberMeServices"/>
    </bean>
    <!--新加 end -->
    加了这个filter后, request经过这里时,acegi通过rememberMeServices(对,又是它,上面在登录成功时就用到了),把相应的cookie取出来,再生成一个remember-me_authentication放到securityContext中, authenticationManager在此的目的是为了检验rememberMeServices从cookies里取出的remember- me_authentication是否有效.

    先看rememberMeServices定义,其定义如下:   
    <!--新加 start-->
    <bean id="rememberMeServices" class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
        <property name="userDetailsService" ref="userDetailsService"/>
        <property name="key" value="23_*!cdU='612./e;NrI"/>
        <property name="parameter" value="rememberMe"/>
    </bean>
    <!--下面, 这个rememberMeAuthenticationProvider加到了已经定义好的authenticationManager bean的providers里-->
    <bean id="rememberMeAuthenticationProvider" class="org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider">
        <property name="key" value="23_*!cdU='612./e;NrI"/>
    </bean>  
    <!--新加 end -->
    三个属性,它们分别是用来干啥的? 实际上, 在上面rememberMeProcessingFilter里,rememberMeServices通过userDetailsService对 cookie里取出的userName进行了检验,一是看现在还有没这个user, 二是看密码是否已改.这也正是下面rememberMeServices里为什么要有userDetailsService的原因.那key是干啥的呢? 在上面authenticationManager验证remember-me_authentication是否有效时与provider里的key来做比较的,正是由于这个原因,下面两处的key值得一模一样. 那个parameter呢? 再回过头看第一步, 那里我们加了一个name为rememberMe的input标签,Acegi内部也正是通过个标签来决定是否要做进一步的装cookie处理.

    至此,结合背后的实现原理, rememberMeProcessingFilter的配置完成完毕,在filterInvocationInterceptor检查当前角色是否有足够的操作时,就会在 remember-me_authentication自己人rememberMeAuthenticationProvider的"保护"下顺利通过了.

    这里为了说明配置都起什么作用,这些配置很散,大家可从附件中看到这些配置都加到什么位置了,它们又是怎么跟原的filter及其支撑类配合的.