概述
Acegi Security(以下简称Acegi)是一个能为基于Spring的企业应用提供强大而灵活安全访问控制解决方案的框架,Acegi已经成为 Spring官方的一个子项目,所以也称为Spring Security。它通过在Spring容器中配置一组Bean,充分利用Spring的IoC和AOP功能,提供声明式安全访问控制的功能。虽然,现在 Acegi也可以应用到非Spring的应用程序中,但在Spring中使用Acegi是最自然的方式。
Acegi可以实现业务对象方法级的安全访问控制粒度,它提供了以下三方面的应用程序的安全:
- URL 资源的访问控制
如所有用户(包括其名用户)可以访问 index.jsp 登录页面,而只有授权的用户可以访问 /user/addUser.jsp 页面。 Acegi 允许通过正则表达式或 Ant 风格的路径表达式定义 URL 模式,让授权用户访问某一 URL 匹配模式下的对应 URL 资源。
- 业务类方法的访问控制
Spring 容器中所有Bean 的方法都可以被Acegi 管理,如所有用户可以调用BbtForum#getRefinedTopicCount() 方法,而只有授权用户可以调用BbtForum#addTopic() 方法。
- 领域对象的访问控制
业务类方法代表一个具体的业务操作,比如更改、删除、审批等,业务类方法访问控制解决了用户是否有调用某种操作的权限,但并未对操作的客体(领域对象)进行控制。对于我们的论坛应用来说,用户可以调用BbtForum#updateUser(User user) 方法更改用户注册信息,但应该仅限于更改自己的用户信息,也即调用BbtForum#updateUser() 所操作的User 这个领域对象必须是受限的。
Acegi 通过多个不同用途的Servlet 过滤器对URL 资源进行保护,在请求受保护的URL 资源前,Acegi 的Servlet 过滤器判断用户是否有权访问目标资源,授权者被开放访问,而未未被授权者将被阻挡在大门之外。
Acegi 通过Spring AOP 对容器中Bean 的受控方法进行拦截,当用户的请求引发调用Bean 的受控方法时,Acegi 的方法拦截器开始工作,阻止未授权者的调用。
对领域对象的访问控制建立在对Bean 方法保护的基础上,在最终开放目标Bean 方法的执行前,Acegi 将检查用户的 ACL (Aeccess Control List :访问控制列表)是否包含正要进行操作的领域对象,只有领域对象被授权时,用户才可以使用Bean 方法对领域对象进行处理。此外,Acegi 还可以对Bean 方法返回的结果进行过滤,将一些不在当前用户访问权限范围内的领域对象剔除掉——即传统的数据可视域范围的控制。一般来说,使用Acegi 控制数据可视域并非理想的选择,相反通过传统的动态SQL 的解决方案往往更加简单易行。
从本质特性上来说,Servlet 过滤器就是最原始的原生态AOP ,所以我们可以说Acegi 不但对业务类方法、领域对象访问控制采用了AOP 技术方案,对URL 资源的访问控制也使用了AOP 的技术方案。使用AOP 技术方案的框架是令人振奋的,这意味着,开发者可以在应用程序业务功能开发完毕后,轻松地通过Acegi 给应用程序穿上安全保护的“铁布衫”。
Acegi 体系结构
Acegi 通过两个组件对象完成以上安全问题的处理:AuthenticationManager (认证管器)、AccessDecisionManager (访问控制管理器),如图 1 所示:
图 1 Acegi 体系结构
SecurityContextHolder 是框架级的容器,它保存着和所有用户关联SecurityContext 实例,SecurityContext 承载着用户(也称认证主体)的身份信息的权限信息, AuthenticationManager 、AccessDecisionManager 将据此进行安全访问控制。
SecurityContext 的认证主体安全信息在一个HTTP 请求线程的多个调用之间是共享的(通过ThreadLocal ),但它不能在多个请求之间保持共享。为了解决这个问题,Acegi 将认证主体安全信息缓存于HttpSession 中,当用户请求一个受限的资源时,Acegi 通过HttpSessionContextIntegrationFilter 将认证主体信息从HttpSession 中加载到SecurityContext 实例中,认证主体关联的SecurityContext 实例保存在Acegi 容器级的SecurityContextHolder 里。当请求结束之后,HttpSessionContextIntegrationFilter 执行相反的操作,将SecurityContext 中的认证主体安全信息重新转存到HttpSession 中,然后从SecurityContextHolder 中清除对应的SecurityContext 实例。通过HttpSession 转存机制,用户的安全信息就可以在多个HTTP 请求间共享,同时保证SecurityContextHolder 中仅保存当前有用的用户安全信息,其整体过程如图 2 所示:
图 2 SecurityContext 在HttpSession 和请求线程间的转交过程
当用户请求一个受限的资源时,AuthenticationManager 首先开始工作,它像一个安检入口,对用户身份进行核查,用户必须提供身份认证的凭证(一般是用户名/ 密码)。在进行身份认证时,AuthenticationManager 将身份认证的工作委托给多个AuthenticationProvider 。因为在具体的系统中,用户身份可能存储在不同的用户信息安全系统中(如数据库、CA 中心、LDAP 服务器),不同用户信息安全系统需要不同的AuthenticationProvider 执行诸如用户信息查询、用户身份判断、用户授权信息获取等工作。只要有一个AuthenticationProvider 可以识别用户的身份,AuthenticationManager 就通过用户身份认证,并将用户的授权信息放入到SecurityContext 中。
当用户通过身份认证后,试图访问某个受限的程序资源时,AccessDecisionManager 开始工作。AccessDecisionManager 采用民主决策机制判断用户是否有权访问目标程序资源,它包含了多个AccessDecisionVoter 。在访问决策时每个AccessDecisionVoter 都拥有投票权,AccessDecisionManager 统计投票结果,并按照某种决策方式根据这些投票结果决定最终是否向用户开放受限资源的访问。
重要组件类介绍
每个框架都有一些核心的概念,这些概念被固化为类和接口,成为框架的重要组件类。框架的管理类、操作类都在这些组件类的基础上进行操作。在进入Acegi 框架的具体学习前,有必要事先了解一下这些承载Acegi 框架重要概念的组件类。
首先,我们要接触是UserDetails 接口,它代表一个应用系统的用户,该接口定义了用户安全相关的信息,如用户名/ 密码,用户是否有效等信息,你可以根据以下接口方法进行相关信息的获取:
String getUsername() :获取用户名;
String getPassword() :获取密码;
boolean isAccountNonExpired() :用户账号是否过期;
boolean isAccountNonLocked() :用户账号是否锁定;
boolean isCredentialsNonExpired() :用户的凭证是否过期;
boolean isEnabled() :用户是否处于激活状态。
当以上任何一个判断用户状态的方法都返回false 时,用户凭证就被视为无效。
UserDetails 还定义了获取用户权限信息的方法:GrantedAuthority[] getAuthorities() ,GrantedAuthority 代表用户权限信息,它定义了一个获取权限描述信息(以字符串表示,如PRIV_COMMON )的方法:String getAuthority() 。
图 3 用户和权限
在未使用Acegi 之前,我们可能通过类似User 、Customer 等领域对象表示用户的概念,并在程序中编写相应的用户认证的逻辑。现在,你要做的一个调整是让原先这些代表用户概念的领域类实现UserDetails 接口,这样,Acegi 就可以通过UserDetails 接口访问到用户的信息了。
UserDetails 可能从数据库、LDAP 等用户信息资源中返回,这要求有一种机制来完成这项工作,UserDetailsService 正是充当这一角色的接口。UserDetailsService 接口很简单,仅有一个方法:UserDetails loadUserByUsername(String username) ,这个方法通过用户名获取整个UserDetails 对象。
Authentication 代表一个和应用程序交互的待认证用户,Acegi 从类似于登录页面、Cookie 等处获取待认证的用户信息(一般是用户名密码)自动构造Authentication 实例。
图 4 Acegi 的认证用户
Authentication 可以通过Object getPrincipal() 获取一个代表用户的对象,这个对象一般可以转换为UserDetails ,从中可以取得用户名/ 密码等信息。在Authentication 被AuthenticationManager 认证之前,没有任何权限的信息。在通过认证之后,Acegi 通过UserDetails 将用户对应的权限信息加载到Authentication 中。Authentication 拥有一个GrantedAuthority[] getAuthorities() 方法,通过该方法可以得到用户对应的权限信息。
Authentication 和UserDetails 很容易被混淆,因为两者都有用户名/ 密码及权限的信息,接口方法也很类似。其实Authentication 是Acegi 进行安全访问控制真正使用的用户安全信息的对象,它拥有两个状态:未认证和已认证。UserDetails 是代表一个从用户安全信息源(数据库、LDAP 服务器、CA 中心)返回的真正用户,Acegi 需要将未认证的Authentication 和代表真实用户的UserDetails 进行匹配比较,通过匹配比较(简单的情况下是用户名/ 密码是否一致)后, Acegi 将UserDetails 中的其它安全信息(如权限、ACL 等)拷贝到Authentication 中。这样, Acegi 安全控制组件在后续的安全访问控制中只和Authentication 进行交互。
由于Acegi 对程序资源进行访问安全控制时,一定要事先获取和请求用户对应的Authentication ,Acegi 框架必须为Authentication 提供一个“寓所”,以便在需要时直接从“寓所”把它请出来,作为各种安全管理器决策的依据。
SecurityContextHolder 就是Authentication 容身的“寓所”,你可以通过SecurityContextHolder.getContext().getAuthenication() 代码获取Authentication 。细心观察一下这句代码,你会发现在SecurityContextHolder 和Authentication 之间存在一个getContext() 中介,这个方法返回SecurityContext 对象。我们知道Authentication 是用户安全相关的信息,请求线程其它信息(如登录验证码等)则放置在SecurityContext 中,构成了一个完整的安全信息上下文。SecurityContext 接口提供了获取和设置Authentication 的方法:
- Authentication getAuthentication()
- void setAuthentication(Authentication authentication)
图 5 认证用户信息存储器
SecurityContextHolder 是Acegi 框架级的对象,它在内部通过ThreadLocal 为请求线程提供线程绑定的SecurityContext 对象。这样,任何参与当前请求线程的Acegi 安全管理组件、业务服务对象等都可以直接通过SecurityContextHolder.getContext() 获取线程绑定的SecurityContext ,避免通过方法入参的方式获取用户相关的SecurityContext 。
线程绑定模式对于大多数应用来说是适合的,但是应用本身会创建其它的线程,那么只有主线程可以获得线程绑定SecurityContext ,而主线程衍生出的新线程则无法得到线程绑定的SecurityContext 。Acegi 考虑到了这些不同应用情况,提供了三种绑定SecurityContext 的模式:
- SecurityContextHolder.MODE_THREADLOCAL :SecurityContext 绑定到主线程,这是默认的模式;
- SecurityContextHolder.MODE_GLOBAL :SecurityContext 绑定到JVM 中,所有线程都使用同一个SecurityContext ;
- SecurityContextHolder.MODE_INHERITABLETHREADLOCAL ::SecurityContext 绑定到主线程及由主线程衍生的线程中。
你可以通过SecurityContextHolder.setStrategyName(String strategyName) 方法指定SecurityContext 的绑定模式。
用户认证过程
Acegi 支持多种方式的用户认证:如典型的基于数据库的认证、基于LDAP 的认证、基于Yale 中心认证等方式。不同的认证环境拥有不同的用户认证方式,现在我们先抛开这些具体的细节,考察一下Acegi 对受限资源进行访问控制的典型过程:
1 .你点击一个链接访问一个网页;
2 .浏览器发送一个请求到服务器,服务器判断出你正在访问一个受保护的资源;
3 .如果此时你并未通过身份认证,服务器发回一个响应提示你进行认证——这个响应可能是一个HTTP 响应代码,抑或重定向到一个指定页面;
4 .根据系统使用认证机制的不同,浏览器或者重定向到一个登录页面中,或者由浏览器通过一些其它的方式获取你的身份信息(如通过BASIC 认证对话框、一个Cookie 或一个X509 证书);
5 .浏览器再次将用户身份信息发送到服务器上(可能是一个用户登录表单的HTTP POST 信息、也可能是包含认证信息的HTTP 报文头);
6 .服务器判断用户认证信息是否有效,如果无效,一般情况下,浏览器会要求你继续尝试,这意味着返回第3 步。如果有效,则到达下一步;
7 .服务器重新响应第2 步所提交的原始请求,并判断该请求所访问的程序资源是否在你的权限范围内,如果你有权访问,请求将得到正确的执行并返回结果。否则,你将收到一个HTTP 403 错误,这意味着你被禁止访问。
在Acegi 框架里,你可以找到对应以上大多数步骤的类,其中ExceptionTranslationFilter 、AuthenticationEntryPoint 、AuthenticationProvider 以及Acegi 的认证机制是其中的代表者。
ExceptionTranslationFilter 是一个Acegi 的Servlet 过滤器,它负责探测抛出的安全异常。当一个未认证用户访问服务器时,Acegi 将引发一个Java 异常。Java 异常本身对HTTP 请求以及如何认证用户是一无所知的,ExceptionTranslationFilter 适时登场,对这个异常进行处理,启动用户认证的步骤(第3 步)。如果已认证用户越权访问一个资源,Acegi 也将引发一个Java 异常,ExceptionTranslationFilter 则将这个异常转换为HTTP 403 响应码(第7 步)。可见,Acegi 通过异常进行通讯,
ExceptionTranslationFilter 接收这些异常并做出相应的动作。
当ExceptionTranslationFilter 通过Java 异常发现用户还未认证时,它到底会将请求重定向哪个页面以要求用户提供认证信息呢?这通过咨询AuthenticationEntryPoint 来达到目的——Acegi 通过AuthenticationEntryPoint 描述登录页面。
当你的浏览器通过HTTP 表单或HTTP 报文头向服务器提供用户认证信息时,Acegi 需要将这些信息收集到Authentication 中,Acegi 用“认证机制”描述这一过程。此时,这个新生成Authentication 只包含用户提供的认证信息,但并未通过认证。
AuthenticationProvider 负责对Authentication 进行认证。AuthenticationProvider 究竟如何完成这一过程呢?大多数AuthenticationProvider 通过UserDetailsService 获取和未认证的Authentication 对应的UserDetails 并进行匹配比较来完成这一任务。当用户认证信息匹配时,Authentication 被认为是有效的,AuthenticationProvider 进一步将UserDetails 中权限、ACL 等信息拷贝到Authentication 。
当Acegi 通过认证机制收集到用户认证信息并填充好Authentication 后,Authentication 将被保存到SecurityContextHolder 中并处理用户的原始请求(第7 步)。
你完全可以抛开Acegi 的安全机制,编写自己的Servlet 过滤器,使用自己的方案构建Authentication 对象并将其放置到SecurityContextHolder 中。也许你使用了CMA (Container Managed Authentication :容器管理认证),CMA 允许你从ThreadLocal 或JNDI 中获取用户认证信息,这时你只要获取这些信息并将其转换为Authentication 就可以了。
安全对象访问控制
Acegi 称受保护的应用资源为“安全对象”,这包括URL 资源和业务类方法。我们知道在Spring AOP 中有前置增强、后置增强、异常增强和环绕增强,其中环绕增强的功能最为强大——它不但可以在目标方法被访问前拦截调用,还可以在调用返回前改变返回的结果,甚至抛出异常。Acegi 使用环绕增强对安全对象进行保护。
Acegi 通过AbstractSecurityInterceptor 为安全对象访问提供一致的工作模型,它按照以下流程进行工作:
1 . 从SecurityContext 中取出已经认证过的Authentication (包括权限信息);
2 . 通过反射机制,根据目标安全对象和“配置属性”得到访问目标安全对象所需的权限;
3 . AccessDecisionManager 根据Authentication 的授权信息和目标安全对象所需权限做出是否有权访问的判断。如果无权访问,Acegi 将抛出AccessDeniedException 异常,否则到下一步;
4 . 访问安全对象并获取结果(返回值或HTTP 响应);
5 . AbstractSecurityInterceptor 可以在结果返回前进行处理:更改结果或抛出异常。
图 6 AbstractSecurityInterceptor 工作流程
安全对象和一般对象的区别在于前者通过Acegi 的“配置属性”进行了描述,如“/view.jsp=PRIV_COMMON ”配置属性就将“/view.jsp ”这个URL 资源标识为安全对象,它表示用户在访问/view.jsp 时,必须拥有PRIV_COMMON 这个权限。配置属性通过XML 配置文件,注解、数据库等方式提供。安全对象通过配置属性表示为一个权限,这样,Acegi 就可以根据Authentication 的权限信息获知用户可以访问的哪些安全对象。
根据安全对象的性质以及具体实现技术,AbstractSecurityInterceptor 拥有以下三个实现类:
- FilterSecurityInterceptor :对URL 资源的安全对象进行调用时,通过该拦截器实施环绕切面。该拦截器使用Servlet 过滤器实现AOP 切面,它本身就是一个Servlet 过滤器;
- MethodSecurityInterceptor :当调用业务类方法的安全对象时,可通过该拦截器类实施环绕切面;
- AspectJSecurityInterceptor :和MethodSecurityInterceptor 类似,它是针对业务类方法的拦截器,只不过它通过AspectJ 实施AOP 切面。
Acegi 版本升级的一些重大变化
Acegi 项目开始于2003 年,Acegi 团队在发布新版本时非常谨慎,在本书写作之时,Acegi 最新版本为1.0.3 。在此之前Acegi 已经发布了10 多个预览版本,由于Acegi 框架优异的表现,许多大型应用早在Acegi 1.0 正式版本发布之前(2006 年5 月),就已经采用Acegi 框架作为其安全访问控制的解决方案。
在Acegi 社区里,来自世界各地众多优秀的安全领域专家对Acegi 的改进和发展献计献策,Acegi 团队广泛听取并吸收各种有益的建议,将它们融入到Acegi 的框架中,使Acegi 成为构建在Spring 基础上企业应用的首选安全控制框架。
Acegi 1.0.3 版本相比于早期预览版本发生了很大的变化,对于需要进行Acegi 版本的项目来说,了解这一变化特别重要。下面,我们列出Acegi 的一些重大的升级更新:
- 包名的更新:在0.9.0 及之前的版本中,Acegi 采用net.sf.acegisecurity 包名前缀,在1.0.0 版本之后更改为org.acegisecurity (Hibernate 也走过相同的道路,好在Acegi 在正式版本发布之时就完成了这种转变);
- ACL 模块的调整:ACL 模块发生了重大的调整,Acegi 团队接收了社区大量关于ACL 模块的反馈意见,重新设计了ACL 模块的底层结构,在性能、封装性、灵活性上得到了质的提升。事实上,Acegi 使用org.acegisecurity.acls 包代替了原来的org.acegisecurity.acl 包,后者将在后期的版本中删除,由于这种伤筋动骨的变化,将很难兼容原来ACL 模块。不过,目前基于新框架的ACL 模块还没有进行充分的测试,Acegi 承诺在1.1.0 版本发布时提供最终的实现;
- 删除了ContextHolder 及其相关类:在Acegi 0.9 版本中,ContextHolder 及其相关类被彻底从Acegi 项目中删除。ContextHolder 可以在多个HTTP 请求中共享同一个ThreadLocal ,这和Spring 提倡的ThreadLocal 只应在同一线程中共享相悖。现在,Acegi 使用SecurityContextHolder 替换ContextHolder ,它的生命周期是一个HTTP 请求;
- 使用FilterChainProxy 同时代理多个过滤器:在早期的版本中,Acegi 通过FilterToBeanProxy 将web.xml 中的Servlet 过滤器定义转移到Spring 容器中。这比直接在web.xml 中配置Servlet 过滤器要方便一些,但是Acegi 框架往往需要定义多个Servlet 过滤器,使web.xml 配置文件变得冗长难看。在Acegi 0.8 版本中提供FilterChainProxy ,它可以同时代理多个Servlet 过滤器并保证过滤器的顺序。因此在新版本中,FilterChainProxy 成为推荐的选
- 择。