【IT168 专稿】对于任何一个完整的应用系统 完善的 认证和授权机制是必不可少的。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和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对象。SecurityContext这个半路杀出来的程咬金有什么特殊的用途呢 我们知道 Authentication是用户安全相关的信息 请求线程其它信息 如登录验证码等 则放置在SecurityContext中 构成了一个完整的安 全信息上下文。SecurityContext接口提供了获取和设置Authentication的方法
Authentication getAuthentication()
void setAuthentication(Authentication authentication)
图 5 认证用户信息存储器
线程绑定模式对于大多数应用来说是适合的 但是应用本身会创建其它的线程 那么只有主线程可以获得线程绑定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究竟如何完成这一过程呢 请回忆一下上节我们所介绍的 UserDetails和UserDetailsService 大多数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可以在结果返回前进行处理 更改结果或抛出异常。 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成为推荐的选择。
小结
Acegi是Spring项目下一个成熟的安全访问控制框架 它允许利用了Spring IoC的AOP的功能完成安全对象的访问控制。在Acegi框架中 SecurityContextHolder处于非常核心的位置 它是存放认证管理器 用户安全信息SecurityContext的“容器” SecurityContext保存着用户安全访问控制所需的信息 直接被访问决策管理器使用。 HttpSessionContextIntegrationFilter通过在SecurityContextHolder和HttpSession中 摆渡SecurityContext 使多个请求线程可以共享同一个SecurityContext。 点赞 评论
本文链接: http://acegi0.immuno-online.com/view-724512.html